summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@verizonmedia.com>2019-10-07 09:45:48 +0200
committerJon Bratseth <bratseth@verizonmedia.com>2019-10-07 09:45:48 +0200
commit8729925b15b81bd3a5d0a0835c631843cd791178 (patch)
treec7e6dc8f3c2fb429644004f45429c8db5bf9e6ad
parent3188f79fdad37e3ea30f84f8c3be67b0c645386d (diff)
parent260e989c42beb61608f4e8ebbffbe54a59ef4602 (diff)
Merge with master
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java9
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java23
-rw-r--r--config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java2
-rw-r--r--container-dependencies-enforcer/pom.xml1
-rw-r--r--container-dependency-versions/pom.xml6
-rw-r--r--container-search/src/main/java/com/yahoo/prelude/cluster/ClusterSearcher.java9
-rw-r--r--container-search/src/main/java/com/yahoo/prelude/fastsearch/FastSearcher.java5
-rw-r--r--container-search/src/main/java/com/yahoo/prelude/fastsearch/VespaBackEndSearcher.java2
-rw-r--r--container-search/src/main/java/com/yahoo/search/cluster/ClusterMonitor.java23
-rw-r--r--container-search/src/main/java/com/yahoo/search/dispatch/Dispatcher.java4
-rw-r--r--container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/Node.java27
-rw-r--r--container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/SearchCluster.java22
-rw-r--r--container-search/src/test/java/com/yahoo/prelude/fastsearch/test/FastSearcherTestCase.java4
-rw-r--r--container-search/src/test/java/com/yahoo/prelude/fastsearch/test/MockDispatcher.java1
-rw-r--r--container-search/src/test/java/com/yahoo/search/dispatch/DispatcherTest.java2
-rw-r--r--container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java18
-rw-r--r--container-search/src/test/java/com/yahoo/search/dispatch/MockInvoker.java2
-rw-r--r--container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java5
-rw-r--r--container-search/src/test/java/com/yahoo/search/dispatch/rpc/RpcSearchInvokerTest.java2
-rw-r--r--container-search/src/test/java/com/yahoo/search/dispatch/searchcluster/SearchClusterTest.java4
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java7
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java1
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java164
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java49
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java48
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java12
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java39
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java40
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java53
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java182
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java76
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java93
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersions.java97
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java58
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java55
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java23
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java55
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json23
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java34
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json20
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java15
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java119
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json54
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-initial.json54
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json54
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java35
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java1
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java4
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java41
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java8
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java26
-rw-r--r--hosted-api/src/test/java/ai/vespa/hosted/api/TestConfigTest.java4
-rw-r--r--hosted-api/src/test/resources/test-config.json5
-rw-r--r--jdisc_core/pom.xml6
-rw-r--r--jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java137
-rw-r--r--jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java154
-rw-r--r--jdisc_core_test/integration_test/pom.xml1
-rw-r--r--jdisc_core_test/integration_test/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonIntegrationTest.java58
-rw-r--r--jdisc_core_test/integration_test/src/test/resources/config.properties1
-rw-r--r--jdisc_core_test/test_bundles/cert-k-pkgs/src/main/java/com/yahoo/jdisc/bundle/k/CertificateK.java2
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java13
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java2
-rw-r--r--searchcore/src/vespa/searchcore/proton/docsummary/docsumcontext.cpp5
-rw-r--r--searchcore/src/vespa/searchcore/proton/matching/docsum_matcher.cpp21
-rw-r--r--searchlib/src/tests/docstore/logdatastore/logdatastore_test.cpp3
-rw-r--r--searchlib/src/tests/fef/properties/properties_test.cpp8
-rw-r--r--searchlib/src/tests/fef/rank_program/rank_program_test.cpp31
-rw-r--r--searchlib/src/vespa/searchlib/attribute/attribute_blueprint_factory.cpp4
-rw-r--r--searchlib/src/vespa/searchlib/features/rankingexpressionfeature.cpp70
-rw-r--r--searchlib/src/vespa/searchlib/features/rankingexpressionfeature.h2
-rw-r--r--searchlib/src/vespa/searchlib/fef/indexproperties.cpp4
-rw-r--r--searchlib/src/vespa/searchlib/fef/indexproperties.h7
-rw-r--r--searchlib/src/vespa/searchlib/fef/rank_program.h1
-rw-r--r--searchlib/src/vespa/searchlib/queryeval/blueprint.h2
-rw-r--r--searchsummary/CMakeLists.txt1
-rw-r--r--searchsummary/src/tests/docsummary/matched_elements_filter/CMakeLists.txt10
-rw-r--r--searchsummary/src/tests/docsummary/matched_elements_filter/matched_elements_filter_test.cpp205
-rw-r--r--searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt31
-rw-r--r--searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.cpp87
-rw-r--r--searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.h27
-rw-r--r--storage/src/tests/distributor/bucketdbupdatertest.cpp147
-rw-r--r--storage/src/tests/distributor/distributortestutil.cpp3
-rw-r--r--storage/src/tests/distributor/externaloperationhandlertest.cpp1
-rw-r--r--storage/src/tests/distributor/getoperationtest.cpp5
-rw-r--r--storage/src/vespa/storage/bucketdb/btree_bucket_database.cpp5
-rw-r--r--storage/src/vespa/storage/distributor/CMakeLists.txt2
-rw-r--r--storage/src/vespa/storage/distributor/bucket_space_distribution_context.cpp81
-rw-r--r--storage/src/vespa/storage/distributor/bucket_space_distribution_context.h70
-rw-r--r--storage/src/vespa/storage/distributor/bucketdbupdater.cpp105
-rw-r--r--storage/src/vespa/storage/distributor/bucketdbupdater.h33
-rw-r--r--storage/src/vespa/storage/distributor/distributor.cpp12
-rw-r--r--storage/src/vespa/storage/distributor/distributor.h2
-rw-r--r--storage/src/vespa/storage/distributor/distributor_bucket_space.h6
-rw-r--r--storage/src/vespa/storage/distributor/distributorinterface.h3
-rw-r--r--storage/src/vespa/storage/distributor/externaloperationhandler.cpp50
-rw-r--r--storage/src/vespa/storage/distributor/externaloperationhandler.h9
-rw-r--r--storage/src/vespa/storage/distributor/operation_routing_snapshot.cpp30
-rw-r--r--storage/src/vespa/storage/distributor/operation_routing_snapshot.h60
-rw-r--r--storage/src/vespa/storage/distributor/operations/external/getoperation.cpp9
-rw-r--r--storage/src/vespa/storage/distributor/operations/external/getoperation.h13
-rw-r--r--storage/src/vespa/storage/distributor/operations/external/twophaseupdateoperation.cpp3
-rw-r--r--tenant-auth/src/main/java/ai/vespa/hosted/auth/ApiAuthenticator.java13
-rw-r--r--tenant-auth/src/main/java/ai/vespa/hosted/auth/EndpointAuthenticator.java48
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java9
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java3
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java13
144 files changed, 2554 insertions, 1217 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java
index 6d121657a40..447b6efb09b 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java
@@ -35,14 +35,17 @@ public class Certificates {
var now = clock.instant();
var notBefore = now.minus(Duration.ofHours(1));
var notAfter = now.plus(CERTIFICATE_TTL);
- return X509CertificateBuilder.fromCsr(csr,
+ var builder = X509CertificateBuilder.fromCsr(csr,
x500principal,
notBefore,
notAfter,
caPrivateKey,
SHA256_WITH_ECDSA,
- X509CertificateBuilder.generateRandomSerialNumber())
- .build();
+ X509CertificateBuilder.generateRandomSerialNumber());
+ for (var san : csr.getSubjectAlternativeNames()) {
+ builder = builder.addSubjectAlternativeName(san.getValue());
+ }
+ return builder.build();
}
/** Returns the DNS name field from Subject Alternative Names in given csr */
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java
index 4e306d9a70e..80940dcd02c 100644
--- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/ca/CertificatesTest.java
@@ -3,26 +3,32 @@ package com.yahoo.vespa.hosted.ca;
import com.yahoo.security.KeyAlgorithm;
import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SubjectAlternativeName;
import com.yahoo.test.ManualClock;
import org.junit.Test;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
import java.time.Duration;
+import java.util.List;
import static java.time.temporal.ChronoUnit.SECONDS;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
/**
* @author mpolden
*/
public class CertificatesTest {
+ private final KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
+ private final X509Certificate caCertificate = CertificateTester.createCertificate("CA", keyPair);
+
@Test
public void expiry() {
var clock = new ManualClock();
var certificates = new Certificates(clock);
var csr = CertificateTester.createCsr();
- var keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
- var caCertificate = CertificateTester.createCertificate("CA", keyPair);
var certificate = certificates.create(csr, caCertificate, keyPair.getPrivate());
var now = clock.instant();
@@ -30,4 +36,17 @@ public class CertificatesTest {
assertEquals(now.plus(Duration.ofDays(30)).truncatedTo(SECONDS), certificate.getNotAfter().toInstant());
}
+ @Test
+ public void add_san_from_csr() throws Exception {
+ var certificates = new Certificates(new ManualClock());
+ var dnsName = "host.example.com";
+ var csr = CertificateTester.createCsr(dnsName);
+ var certificate = certificates.create(csr, caCertificate, keyPair.getPrivate());
+
+ assertNotNull(certificate.getSubjectAlternativeNames());
+ assertEquals(1, certificate.getSubjectAlternativeNames().size());
+ assertEquals(List.of(SubjectAlternativeName.Type.DNS_NAME.getTag(), dnsName),
+ certificate.getSubjectAlternativeNames().iterator().next());
+ }
+
}
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java b/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java
index 09e67ed96cb..e579f736136 100644
--- a/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java
+++ b/config-model/src/main/java/com/yahoo/vespa/model/HostSystem.java
@@ -55,7 +55,7 @@ public class HostSystem extends AbstractConfigProducer<Host> {
}
if (! hostname.contains(".")) {
deployLogger.log(Level.WARNING, "Host named '" + hostname + "' may not receive any config " +
- "since it is not a canonical hostname." +
+ "since it is not a canonical hostname. " +
"Disregard this warning when testing in a Docker container.");
}
}
diff --git a/container-dependencies-enforcer/pom.xml b/container-dependencies-enforcer/pom.xml
index 92407aa9c68..05de643a116 100644
--- a/container-dependencies-enforcer/pom.xml
+++ b/container-dependencies-enforcer/pom.xml
@@ -89,7 +89,6 @@
<include>com.sun.activation:javax.activation:[1.2.0]:jar:provided</include>
<include>com.sun.xml.bind:jaxb-core:[${jaxb.version}]:jar:provided</include>
<include>com.sun.xml.bind:jaxb-impl:[${jaxb.version}]:jar:provided</include>
- <include>commons-daemon:commons-daemon:[${commons-daemon.version}]:jar:provided</include>
<include>commons-logging:commons-logging:[1.1.1]:jar:provided</include>
<include>javax.annotation:javax.annotation-api:[${javax.annotation-api.version}]:jar:provided</include>
<include>javax.inject:javax.inject:[${javax.inject.version}]:jar:provided</include>
diff --git a/container-dependency-versions/pom.xml b/container-dependency-versions/pom.xml
index 5fc148beaee..6b52c5023fc 100644
--- a/container-dependency-versions/pom.xml
+++ b/container-dependency-versions/pom.xml
@@ -115,11 +115,6 @@
<classifier>no_aop</classifier>
</dependency>
<dependency>
- <groupId>commons-daemon</groupId>
- <artifactId>commons-daemon</artifactId>
- <version>${commons-daemon.version}</version>
- </dependency>
- <dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<!-- This version is exported by jdisc via jcl-over-slf4j. -->
@@ -442,7 +437,6 @@
<properties>
<aopalliance.version>1.0</aopalliance.version>
<bouncycastle.version>1.63</bouncycastle.version>
- <commons-daemon.version>1.0.3</commons-daemon.version>
<felix.version>6.0.3</felix.version>
<felix.log.version>1.0.1</felix.log.version>
<findbugs.version>1.3.9</findbugs.version>
diff --git a/container-search/src/main/java/com/yahoo/prelude/cluster/ClusterSearcher.java b/container-search/src/main/java/com/yahoo/prelude/cluster/ClusterSearcher.java
index 1f621eb926c..0c8e564578b 100644
--- a/container-search/src/main/java/com/yahoo/prelude/cluster/ClusterSearcher.java
+++ b/container-search/src/main/java/com/yahoo/prelude/cluster/ClusterSearcher.java
@@ -7,7 +7,6 @@ import com.yahoo.component.chain.dependencies.After;
import com.yahoo.container.QrSearchersConfig;
import com.yahoo.container.handler.VipStatus;
import com.yahoo.jdisc.Metric;
-import com.yahoo.net.HostName;
import com.yahoo.prelude.IndexFacts;
import com.yahoo.prelude.fastsearch.ClusterParams;
import com.yahoo.prelude.fastsearch.DocumentdbInfoConfig;
@@ -27,8 +26,6 @@ import com.yahoo.vespa.config.search.DispatchConfig;
import com.yahoo.vespa.streamingvisitors.VdsStreamingSearcher;
import org.apache.commons.lang.StringUtils;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -371,6 +368,10 @@ public class ClusterSearcher extends Searcher {
}
@Override
- public void deconstruct() { }
+ public void deconstruct() {
+ if (server != null) {
+ server.shutDown();
+ }
+ }
}
diff --git a/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastSearcher.java b/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastSearcher.java
index b0b3a7800e9..9a4913b3840 100644
--- a/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastSearcher.java
+++ b/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastSearcher.java
@@ -173,4 +173,9 @@ public class FastSearcher extends VespaBackEndSearcher {
return getLogger().isLoggable(Level.FINE);
}
+ @Override
+ public void shutDown() {
+ super.shutDown();
+ dispatcher.shutDown();
+ }
}
diff --git a/container-search/src/main/java/com/yahoo/prelude/fastsearch/VespaBackEndSearcher.java b/container-search/src/main/java/com/yahoo/prelude/fastsearch/VespaBackEndSearcher.java
index 8f4b49ac71e..bc3ac6cdef1 100644
--- a/container-search/src/main/java/com/yahoo/prelude/fastsearch/VespaBackEndSearcher.java
+++ b/container-search/src/main/java/com/yahoo/prelude/fastsearch/VespaBackEndSearcher.java
@@ -392,4 +392,6 @@ public abstract class VespaBackEndSearcher extends PingableSearcher {
return getLogger().isLoggable(Level.FINE);
}
+ public void shutDown() { }
+
}
diff --git a/container-search/src/main/java/com/yahoo/search/cluster/ClusterMonitor.java b/container-search/src/main/java/com/yahoo/search/cluster/ClusterMonitor.java
index 22c7f59872c..a016f7d695c 100644
--- a/container-search/src/main/java/com/yahoo/search/cluster/ClusterMonitor.java
+++ b/container-search/src/main/java/com/yahoo/search/cluster/ClusterMonitor.java
@@ -9,7 +9,9 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -26,9 +28,9 @@ public class ClusterMonitor<T> {
private static Logger log = Logger.getLogger(ClusterMonitor.class.getName());
- private NodeManager<T> nodeManager;
+ private final NodeManager<T> nodeManager;
- private MonitorThread monitorThread;
+ private final MonitorThread monitorThread;
private volatile boolean shutdown = false;
@@ -119,28 +121,35 @@ public class ClusterMonitor<T> {
}
public void run() {
- log.fine("Starting cluster monitor thread");
+ log.info("Starting cluster monitor thread " + getName());
// Pings must happen in a separate thread from this to handle timeouts
// By using a cached thread pool we ensured that 1) a single thread will be used
// for all pings when there are no problems (important because it ensures that
// any thread local connections are reused) 2) a new thread will be started to execute
// new pings when a ping is not responding
- Executor pingExecutor=Executors.newCachedThreadPool(ThreadFactoryFactory.getDaemonThreadFactory("search.ping"));
+ ExecutorService pingExecutor=Executors.newCachedThreadPool(ThreadFactoryFactory.getDaemonThreadFactory("search.ping"));
while (!isInterrupted()) {
try {
Thread.sleep(configuration.getCheckInterval());
log.finest("Activating ping");
ping(pingExecutor);
}
- catch (Exception e) {
+ catch (Throwable e) {
if (shutdown && e instanceof InterruptedException) {
break;
+ } else if ( ! (e instanceof Exception) ) {
+ log.log(Level.WARNING,"Error in monitor thread, will quit", e);
+ break;
} else {
- log.log(Level.WARNING,"Error in monitor thread",e);
+ log.log(Level.WARNING,"Exception in monitor thread", e);
}
}
}
- log.fine("Stopped cluster monitor thread");
+ pingExecutor.shutdown();
+ try {
+ pingExecutor.awaitTermination(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) { }
+ log.info("Stopped cluster monitor thread " + getName());
}
}
diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/Dispatcher.java b/container-search/src/main/java/com/yahoo/search/dispatch/Dispatcher.java
index 7369b33e82d..ddd319b7bcb 100644
--- a/container-search/src/main/java/com/yahoo/search/dispatch/Dispatcher.java
+++ b/container-search/src/main/java/com/yahoo/search/dispatch/Dispatcher.java
@@ -195,6 +195,10 @@ public class Dispatcher extends AbstractComponent {
return Optional.empty();
}
+ public void shutDown() {
+ searchCluster.shutDown();
+ }
+
private void emitDispatchMetric(Optional<SearchInvoker> invoker) {
if (invoker.isEmpty()) {
metric.add(FDISPATCH_METRIC, 1, metricContext);
diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/Node.java b/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/Node.java
index b47f2fefa5b..09ad715b471 100644
--- a/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/Node.java
+++ b/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/Node.java
@@ -16,17 +16,15 @@ public class Node {
private final int key;
private int pathIndex;
private final String hostname;
- private final int fs4port;
- final int group;
+ private final int group;
private final AtomicBoolean statusIsKnown = new AtomicBoolean(false);
private final AtomicBoolean working = new AtomicBoolean(true);
private final AtomicLong activeDocuments = new AtomicLong(0);
- public Node(int key, String hostname, int fs4port, int group) {
+ public Node(int key, String hostname, int group) {
this.key = key;
this.hostname = hostname;
- this.fs4port = fs4port;
this.group = group;
}
@@ -41,14 +39,15 @@ public class Node {
public String hostname() { return hostname; }
- public int fs4port() { return fs4port; }
-
/** Returns the id of this group this node belongs to */
public int group() { return group; }
public void setWorking(boolean working) {
this.statusIsKnown.lazySet(true);
this.working.lazySet(working);
+ if ( ! working ) {
+ activeDocuments.set(0);
+ }
}
/** Returns whether this node is currently responding to requests, or null if status is not known */
@@ -57,17 +56,17 @@ public class Node {
}
/** Updates the active documents on this node */
- public void setActiveDocuments(long activeDocuments) {
+ void setActiveDocuments(long activeDocuments) {
this.activeDocuments.set(activeDocuments);
}
/** Returns the active documents on this node. If unknown, 0 is returned. */
- public long getActiveDocuments() {
- return this.activeDocuments.get();
+ long getActiveDocuments() {
+ return activeDocuments.get();
}
@Override
- public int hashCode() { return Objects.hash(hostname, fs4port); }
+ public int hashCode() { return Objects.hash(hostname, key, pathIndex, group); }
@Override
public boolean equals(Object o) {
@@ -75,11 +74,15 @@ public class Node {
if ( ! (o instanceof Node)) return false;
Node other = (Node)o;
if ( ! Objects.equals(this.hostname, other.hostname)) return false;
- if ( ! Objects.equals(this.fs4port, other.fs4port)) return false;
+ if ( ! Objects.equals(this.key, other.key)) return false;
+ if ( ! Objects.equals(this.pathIndex, other.pathIndex)) return false;
+ if ( ! Objects.equals(this.group, other.group)) return false;
+
return true;
}
@Override
- public String toString() { return "search node " + hostname + ":" + fs4port + " in group " + group; }
+ public String toString() { return "search node key = " + key + " hostname = "+ hostname + " path = " + pathIndex + " in group " + group +
+ " statusIsKnown = " + statusIsKnown.get() + " working = " + working.get() + " activeDocs = " + activeDocuments.get(); }
}
diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/SearchCluster.java b/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/SearchCluster.java
index a55a970e8ff..3595a24ca92 100644
--- a/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/SearchCluster.java
+++ b/container-search/src/main/java/com/yahoo/search/dispatch/searchcluster/SearchCluster.java
@@ -46,6 +46,7 @@ public class SearchCluster implements NodeManager<Node> {
private final ClusterMonitor<Node> clusterMonitor;
private final VipStatus vipStatus;
private PingFactory pingFactory;
+ private long nextLogTime = 0;
/**
* A search node on this local machine having the entire corpus, which we therefore
@@ -73,7 +74,7 @@ public class SearchCluster implements NodeManager<Node> {
}
this.groups = groupsBuilder.build();
LinkedHashMap<Integer, Group> groupIntroductionOrder = new LinkedHashMap<>();
- nodes.forEach(node -> groupIntroductionOrder.put(node.group(), groups.get(node.group)));
+ nodes.forEach(node -> groupIntroductionOrder.put(node.group(), groups.get(node.group())));
this.orderedGroups = ImmutableList.<Group>builder().addAll(groupIntroductionOrder.values()).build();
// Index nodes by host
@@ -91,6 +92,10 @@ public class SearchCluster implements NodeManager<Node> {
this.clusterMonitor = new ClusterMonitor<>(this);
}
+ public void shutDown() {
+ clusterMonitor.shutdown();
+ }
+
public void startClusterMonitoring(PingFactory pingFactory) {
this.pingFactory = pingFactory;
@@ -141,7 +146,7 @@ public class SearchCluster implements NodeManager<Node> {
}
for (DispatchConfig.Node node : dispatchConfig.node()) {
if (filter.test(node)) {
- nodesBuilder.add(new Node(node.key(), node.host(), node.fs4port(), node.group()));
+ nodesBuilder.add(new Node(node.key(), node.host(), node.group()));
}
}
return nodesBuilder.build();
@@ -409,14 +414,21 @@ public class SearchCluster implements NodeManager<Node> {
private void trackGroupCoverageChanges(int index, Group group, boolean fullCoverage, long averageDocuments) {
boolean changed = group.isFullCoverageStatusChanged(fullCoverage);
- if (changed) {
+ if (changed || (!fullCoverage && System.currentTimeMillis() > nextLogTime)) {
+ nextLogTime = System.currentTimeMillis() + 30 * 1000;
int requiredNodes = groupSize() - dispatchConfig.maxNodesDownPerGroup();
if (fullCoverage) {
log.info(() -> String.format("Group %d is now good again (%d/%d active docs, coverage %d/%d)",
index, group.getActiveDocuments(), averageDocuments, group.workingNodes(), groupSize()));
} else {
- log.warning(() -> String.format("Coverage of group %d is only %d/%d (requires %d)",
- index, group.workingNodes(), groupSize(), requiredNodes));
+ StringBuilder missing = new StringBuilder();
+ for (var node : group.nodes()) {
+ if (node.isWorking() != Boolean.TRUE) {
+ missing.append('\n').append(node.toString());
+ }
+ }
+ log.warning(() -> String.format("Coverage of group %d is only %d/%d (requires %d) (%d/%d active docs) Failed nodes are:%s",
+ index, group.workingNodes(), groupSize(), requiredNodes, group.getActiveDocuments(), averageDocuments, missing.toString()));
}
}
}
diff --git a/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/FastSearcherTestCase.java b/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/FastSearcherTestCase.java
index eb4d65693bb..4011611b049 100644
--- a/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/FastSearcherTestCase.java
+++ b/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/FastSearcherTestCase.java
@@ -81,7 +81,7 @@ public class FastSearcherTestCase {
@Test
public void testSinglePassGroupingIsForcedWithSingleNodeGroups() {
FastSearcher fastSearcher = new FastSearcher("container.0",
- MockDispatcher.create(Collections.singletonList(new Node(0, "host0", 123, 0))),
+ MockDispatcher.create(Collections.singletonList(new Node(0, "host0", 0))),
new SummaryParameters(null),
new ClusterParams("testhittype"),
documentdbInfoConfig);
@@ -102,7 +102,7 @@ public class FastSearcherTestCase {
@Test
public void testSinglePassGroupingIsNotForcedWithSingleNodeGroups() {
- MockDispatcher dispatcher = MockDispatcher.create(ImmutableList.of(new Node(0, "host0", 123, 0), new Node(2, "host1", 123, 0)));
+ MockDispatcher dispatcher = MockDispatcher.create(ImmutableList.of(new Node(0, "host0", 0), new Node(2, "host1", 0)));
FastSearcher fastSearcher = new FastSearcher("container.0",
dispatcher,
diff --git a/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/MockDispatcher.java b/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/MockDispatcher.java
index 0aa91442712..4fbbd9dd936 100644
--- a/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/MockDispatcher.java
+++ b/container-search/src/test/java/com/yahoo/prelude/fastsearch/test/MockDispatcher.java
@@ -39,7 +39,6 @@ class MockDispatcher extends Dispatcher {
for (Node node : nodes) {
DispatchConfig.Node.Builder dispatchConfigNodeBuilder = new DispatchConfig.Node.Builder();
dispatchConfigNodeBuilder.host(node.hostname());
- dispatchConfigNodeBuilder.fs4port(node.fs4port());
dispatchConfigNodeBuilder.port(0); // Mandatory, but currently not used here
dispatchConfigNodeBuilder.group(node.group());
dispatchConfigNodeBuilder.key(key++); // not used
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/DispatcherTest.java b/container-search/src/test/java/com/yahoo/search/dispatch/DispatcherTest.java
index 310f536f961..3d544f5c114 100644
--- a/container-search/src/test/java/com/yahoo/search/dispatch/DispatcherTest.java
+++ b/container-search/src/test/java/com/yahoo/search/dispatch/DispatcherTest.java
@@ -70,7 +70,7 @@ public class DispatcherTest {
SearchCluster cl = new MockSearchCluster("1", 0, 0) {
@Override
public Optional<Node> localCorpusDispatchTarget() {
- return Optional.of(new Node(1, "test", 123, 1));
+ return Optional.of(new Node(1, "test", 1));
}
};
MockInvokerFactory invokerFactory = new MockInvokerFactory(cl, (n, a) -> true);
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java b/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java
index 1ebf7940f25..0496194f8ed 100644
--- a/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java
+++ b/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java
@@ -28,7 +28,7 @@ import static org.junit.Assert.assertThat;
public class LoadBalancerTest {
@Test
public void requireThatLoadBalancerServesSingleNodeSetups() {
- Node n1 = new Node(0, "test-node1", 0, 0);
+ Node n1 = new Node(0, "test-node1", 0);
SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1), 1, null);
LoadBalancer lb = new LoadBalancer(cluster, true);
@@ -41,8 +41,8 @@ public class LoadBalancerTest {
@Test
public void requireThatLoadBalancerServesMultiGroupSetups() {
- Node n1 = new Node(0, "test-node1", 0, 0);
- Node n2 = new Node(1, "test-node2", 1, 1);
+ Node n1 = new Node(0, "test-node1", 0);
+ Node n2 = new Node(1, "test-node2", 1);
SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2), 1, null);
LoadBalancer lb = new LoadBalancer(cluster, true);
@@ -55,10 +55,10 @@ public class LoadBalancerTest {
@Test
public void requireThatLoadBalancerServesClusteredGroups() {
- Node n1 = new Node(0, "test-node1", 0, 0);
- Node n2 = new Node(1, "test-node2", 1, 0);
- Node n3 = new Node(0, "test-node3", 0, 1);
- Node n4 = new Node(1, "test-node4", 1, 1);
+ Node n1 = new Node(0, "test-node1", 0);
+ Node n2 = new Node(1, "test-node2", 0);
+ Node n3 = new Node(0, "test-node3", 1);
+ Node n4 = new Node(1, "test-node4", 1);
SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2, n3, n4), 2, null);
LoadBalancer lb = new LoadBalancer(cluster, true);
@@ -68,8 +68,8 @@ public class LoadBalancerTest {
@Test
public void requireThatLoadBalancerReturnsDifferentGroups() {
- Node n1 = new Node(0, "test-node1", 0, 0);
- Node n2 = new Node(1, "test-node2", 1, 1);
+ Node n1 = new Node(0, "test-node1", 0);
+ Node n2 = new Node(1, "test-node2", 1);
SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2), 1, null);
LoadBalancer lb = new LoadBalancer(cluster, true);
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/MockInvoker.java b/container-search/src/test/java/com/yahoo/search/dispatch/MockInvoker.java
index 2fe434d6f3f..c5fbda7c2f5 100644
--- a/container-search/src/test/java/com/yahoo/search/dispatch/MockInvoker.java
+++ b/container-search/src/test/java/com/yahoo/search/dispatch/MockInvoker.java
@@ -19,7 +19,7 @@ class MockInvoker extends SearchInvoker {
private List<Hit> hits;
protected MockInvoker(int key, Coverage coverage) {
- super(Optional.of(new Node(key, "?", 0, 0)));
+ super(Optional.of(new Node(key, "?", 0)));
this.coverage = coverage;
}
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java b/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java
index e3ff54102d4..0bcc30d9b10 100644
--- a/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java
+++ b/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java
@@ -38,7 +38,7 @@ public class MockSearchCluster extends SearchCluster {
for (int group = 0; group < groups; group++) {
List<Node> nodes = new ArrayList<>();
for (int node = 0; node < nodesPerGroup; node++) {
- Node n = new Node(dk, "host" + dk, -1, group);
+ Node n = new Node(dk, "host" + dk, group);
n.setWorking(true);
nodes.add(n);
hostBuilder.put(n.hostname(), n);
@@ -124,8 +124,9 @@ public class MockSearchCluster extends SearchCluster {
builder.minWaitAfterCoverageFactor(0);
builder.maxWaitAfterCoverageFactor(0.5);
}
+ int port = 10000;
for (Node n : nodes) {
- builder.node(new DispatchConfig.Node.Builder().key(n.key()).host(n.hostname()).port(n.fs4port()).group(n.group()));
+ builder.node(new DispatchConfig.Node.Builder().key(n.key()).host(n.hostname()).port(port++).group(n.group()));
}
return new DispatchConfig(builder);
}
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/rpc/RpcSearchInvokerTest.java b/container-search/src/test/java/com/yahoo/search/dispatch/rpc/RpcSearchInvokerTest.java
index d629bd36bb1..c07bf119782 100644
--- a/container-search/src/test/java/com/yahoo/search/dispatch/rpc/RpcSearchInvokerTest.java
+++ b/container-search/src/test/java/com/yahoo/search/dispatch/rpc/RpcSearchInvokerTest.java
@@ -36,7 +36,7 @@ public class RpcSearchInvokerTest {
var mockClient = parameterCollectorClient(compressionTypeHolder, payloadHolder, lengthHolder);
var mockPool = new RpcResourcePool(ImmutableMap.of(7, mockClient.createConnection("foo", 123)));
@SuppressWarnings("resource")
- var invoker = new RpcSearchInvoker(mockSearcher(), new Node(7, "seven", 77, 1), mockPool);
+ var invoker = new RpcSearchInvoker(mockSearcher(), new Node(7, "seven", 1), mockPool);
Query q = new Query("search/?query=test&hits=10&offset=3");
invoker.sendSearchRequest(q);
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/searchcluster/SearchClusterTest.java b/container-search/src/test/java/com/yahoo/search/dispatch/searchcluster/SearchClusterTest.java
index f29d6ddf324..f42185e955f 100644
--- a/container-search/src/test/java/com/yahoo/search/dispatch/searchcluster/SearchClusterTest.java
+++ b/container-search/src/test/java/com/yahoo/search/dispatch/searchcluster/SearchClusterTest.java
@@ -63,7 +63,7 @@ public class SearchClusterTest {
for (String name : nodeNames) {
int key = nodes.size() % nodesPergroup;
int group = nodes.size() / nodesPergroup;
- nodes.add(new Node(key, name, 13333, group));
+ nodes.add(new Node(key, name, group));
numDocsPerNode.add(new AtomicInteger(1));
pingCounts.add(new AtomicInteger(0));
}
@@ -132,7 +132,7 @@ public class SearchClusterTest {
@Override
public Callable<Pong> createPinger(Node node, ClusterMonitor<Node> monitor) {
- int index = node.group*numPerGroup + node.key();
+ int index = node.group() * numPerGroup + node.key();
return new Pinger(activeDocs.get(index), pingCounts.get(index));
}
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java
index fceecedb9fe..c2512c2032b 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java
@@ -10,10 +10,6 @@ public class ApplicationId extends NonDefaultIdentifier {
super(id);
}
- public static boolean isLegal(String id) {
- return strictPattern.matcher(id).matches();
- }
-
@Override
public void validate() {
super.validate();
@@ -21,9 +17,8 @@ public class ApplicationId extends NonDefaultIdentifier {
}
public static void validate(String id) {
- if (!isLegal(id)) {
+ if ( ! strictPattern.matcher(id).matches())
throwInvalidId(id, strictPatternExplanation);
- }
}
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
index 2067a88e5fb..4007ac2b9cd 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
@@ -15,7 +15,6 @@ public abstract class Identifier {
protected static final String strictPatternExplanation =
"New tenant or application names must start with a letter, may contain no more than 20 " +
"characters, and may only contain lowercase letters, digits or dashes, but no double-dashes.";
- // TODO: Use this also for instances, if they ever get proper support.
protected static final Pattern strictPattern = Pattern.compile("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$");
private static final Pattern serializedIdentifierPattern = Pattern.compile("[a-zA-Z0-9_-]+");
private static final Pattern serializedPattern = Pattern.compile("[a-zA-Z0-9_.-]+");
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java
index 6e3087cdcf6..8e14774b827 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java
@@ -16,4 +16,9 @@ public class InstanceId extends SerializedIdentifier {
validateNoUpperCase();
}
+ public static void validate(String id) {
+ if ( ! strictPattern.matcher(id).matches())
+ throwInvalidId(id, strictPatternExplanation);
+ }
+
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java
index f36107db228..606db8a0f2f 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java
@@ -58,6 +58,26 @@ public abstract class Role {
return new TenantRole(RoleDefinition.tenantOperator, tenant);
}
+ /** Returns a {@link RoleDefinition#reader} for the current system and given tenant. */
+ public static TenantRole reader(TenantName tenant) {
+ return new TenantRole(RoleDefinition.reader, tenant);
+ }
+
+ /** Returns a {@link RoleDefinition#developer} for the current system and given tenant. */
+ public static TenantRole developer(TenantName tenant) {
+ return new TenantRole(RoleDefinition.developer, tenant);
+ }
+
+ /** Returns a {@link RoleDefinition#administrator} for the current system and given tenant. */
+ public static TenantRole administrator(TenantName tenant) {
+ return new TenantRole(RoleDefinition.administrator, tenant);
+ }
+
+ /** Returns a {@link RoleDefinition#headless} for the current system, given tenant, and application */
+ public static ApplicationRole headless(TenantName tenant, ApplicationName application) {
+ return new ApplicationRole(RoleDefinition.headless, tenant, application);
+ }
+
/** Returns a {@link RoleDefinition#applicationAdmin} for the current system and given tenant and application. */
public static ApplicationRole applicationAdmin(TenantName tenant, ApplicationName application) {
return new ApplicationRole(RoleDefinition.applicationAdmin, tenant, application);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
index 7bbd89404c7..8e3754777ea 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
@@ -70,6 +70,29 @@ public enum RoleDefinition {
tenantOwner(tenantAdmin,
Policy.tenantDelete),
+ /** Reader — the base role for all tenant users */
+ reader(Policy.tenantRead,
+ Policy.applicationRead,
+ Policy.deploymentRead,
+ Policy.publicRead),
+
+ /** User — the dev.ops. role for normal Vespa tenant users */
+ developer(Policy.applicationCreate,
+ Policy.applicationUpdate,
+ Policy.applicationDelete,
+ Policy.applicationOperations,
+ Policy.developmentDeployment,
+ Policy.keyManagement,
+ Policy.submission),
+
+ /** Admin — the administrative function for user management etc. */
+ administrator(Policy.tenantUpdate,
+ Policy.tenantManager,
+ Policy.applicationManager),
+
+ /** Headless — the application specific role identified by deployment keys for production */
+ headless(Policy.submission),
+
/** Build and continuous delivery service. */ // TODO replace with buildService, when everyone is on new pipeline.
tenantPipeline(everyone,
Policy.submission,
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 c17ac044136..c83f366cb67 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
@@ -16,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
+import java.security.PublicKey;
import java.time.Instant;
import java.util.Collection;
import java.util.Comparator;
@@ -51,7 +52,7 @@ public class Application {
private final Optional<User> owner;
private final OptionalInt majorVersion;
private final ApplicationMetrics metrics;
- private final Set<String> pemDeployKeys;
+ private final Set<PublicKey> deployKeys;
private final Map<InstanceName, Instance> instances;
/** Creates an empty application. */
@@ -64,7 +65,7 @@ public class Application {
// DO NOT USE! For serialization purposes, only.
public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Change change, Change outstandingChange, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Set<String> pemDeployKeys,
+ OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys,
OptionalLong projectId, boolean internal, Collection<Instance> instances) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
@@ -77,7 +78,7 @@ public class Application {
this.owner = Objects.requireNonNull(owner, "owner cannot be null");
this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null");
this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
- this.pemDeployKeys = Objects.requireNonNull(pemDeployKeys, "pemDeployKeys cannot be null");
+ this.deployKeys = Objects.requireNonNull(deployKeys, "deployKeys cannot be null");
this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null");
this.internal = internal;
this.instances = ImmutableSortedMap.copyOf(instances.stream().collect(Collectors.toMap(Instance::name, Function.identity())));
@@ -191,7 +192,7 @@ public class Application {
}
/** Returns the set of deploy keys for this application. */
- public Set<String> pemDeployKeys() { return pemDeployKeys; }
+ public Set<PublicKey> deployKeys() { return deployKeys; }
@Override
public boolean equals(Object o) {
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 6f64237b2c4..0cf0f59102e 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
@@ -2,12 +2,12 @@
package com.yahoo.vespa.hosted.controller;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
@@ -27,6 +27,7 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ConfigChangeActions;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.Hostname;
+import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
import com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.ApplicationCertificate;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
@@ -149,7 +150,6 @@ public class ApplicationController {
// Update serialization format of all applications
Once.after(Duration.ofMinutes(1), () -> {
- curator.deleteOldApplicationData();
Instant start = clock.instant();
int count = 0;
for (Application application : curator.readApplications()) {
@@ -213,6 +213,14 @@ public class ApplicationController {
public ApplicationStore applicationStore() { return applicationStore; }
+ /** Returns all content clusters in all current deployments of the given application. */
+ public Map<ZoneId, List<String>> contentClustersByZone(ApplicationId id, Iterable<ZoneId> zones) {
+ ImmutableMap.Builder<ZoneId, List<String>> clusters = ImmutableMap.builder();
+ for (ZoneId zone : zones)
+ clusters.put(zone, ImmutableList.copyOf(configServer.getContentClusters(new DeploymentId(id, zone))));
+ return clusters.build();
+ }
+
/** Returns the oldest Vespa version installed on any active or reserved production node for the given application. */
public Version oldestInstalledPlatform(TenantAndApplicationId id) {
return requireApplication(id).instances().values().stream()
@@ -263,70 +271,86 @@ public class ApplicationController {
*
* @throws IllegalArgumentException if the application already exists
*/
- // TODO jonmv: split in create application and create instance
- public Application createApplication(ApplicationId id, Optional<Credentials> credentials) {
- if (id.instance().isTester())
- throw new IllegalArgumentException("'" + id + "' is a tester application!");
- try (Lock lock = lock(TenantAndApplicationId.from(id))) {
- // Validate only application names which do not already exist.
- if (getApplication(TenantAndApplicationId.from(id)).isEmpty())
- com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value());
+ public Application createApplication(TenantAndApplicationId id, Optional<Credentials> credentials) {
+ try (Lock lock = lock(id)) {
+ if (getApplication(id).isPresent())
+ throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
+ if (getApplication(dashToUnderscore(id)).isPresent()) // VESPA-1945
+ throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists");
+
+ com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value());
Optional<Tenant> tenant = controller.tenants().get(id.tenant());
if (tenant.isEmpty())
throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist");
- if (getInstance(id).isPresent())
- throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
- if (getInstance(dashToUnderscore(id)).isPresent()) // VESPA-1945
- throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists");
if (tenant.get().type() != Tenant.Type.user) {
if (credentials.isEmpty())
throw new IllegalArgumentException("Could not create '" + id + "': No credentials provided");
-
- if ( ! id.instance().isTester()) // Only store the application permits for non-user applications.
- accessControl.createApplication(id, credentials.get());
+ accessControl.createApplication(id, credentials.get());
}
- Application application = getApplication(TenantAndApplicationId.from(id)).orElse(new Application(TenantAndApplicationId.from(id),
- clock.instant()));
- LockedApplication locked = new LockedApplication(application, lock).withNewInstance(id.instance());
+
+ LockedApplication locked = new LockedApplication(new Application(id, clock.instant()), lock);
store(locked);
log.info("Created " + locked);
return locked.get();
}
}
+ /**
+ * Creates a new instance for an existing application.
+ *
+ * @throws IllegalArgumentException if the instance already exists, or has an invalid instance name.
+ */
+ public void createInstance(ApplicationId id) {
+ if (id.instance().isTester())
+ throw new IllegalArgumentException("'" + id + "' is a tester application!");
+ lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
+ InstanceId.validate(id.instance().value());
+
+ if (getInstance(id).isPresent())
+ throw new IllegalArgumentException("Could not create '" + id + "': Instance already exists");
+ if (getInstance(dashToUnderscore(id)).isPresent()) // VESPA-1945
+ throw new IllegalArgumentException("Could not create '" + id + "': Instance " + dashToUnderscore(id) + " already exists");
+
+ store(application.withNewInstance(id.instance()));
+ log.info("Created " + id);
+ });
+ }
+
public ActivateResult deploy(ApplicationId applicationId, ZoneId zone,
Optional<ApplicationPackage> applicationPackageFromDeployer,
DeployOptions options) {
- return deploy(applicationId, zone, applicationPackageFromDeployer, Optional.empty(), options, Optional.empty());
+ return deploy(applicationId, zone, applicationPackageFromDeployer, Optional.empty(), options);
}
/** Deploys an application. If the application does not exist it is created. */
// TODO: Get rid of the options arg
- // TODO(jvenstad): Split this, and choose between deployDirectly and deploy in handler, excluding internally built from the latter.
- public ActivateResult deploy(ApplicationId applicationId, ZoneId zone,
+ // TODO jonmv: Split this, and choose between deployDirectly and deploy in handler, excluding internally built from the latter.
+ public ActivateResult deploy(ApplicationId instanceId, ZoneId zone,
Optional<ApplicationPackage> applicationPackageFromDeployer,
Optional<ApplicationVersion> applicationVersionFromDeployer,
- DeployOptions options,
- Optional<Principal> deployingIdentity) {
- if (applicationId.instance().isTester())
- throw new IllegalArgumentException("'" + applicationId + "' is a tester application!");
-
- // TODO jonmv: Change this to create instances on demand.
- Tenant tenant = controller.tenants().require(applicationId.tenant());
- if (tenant.type() == Tenant.Type.user && getInstance(applicationId).isEmpty())
+ DeployOptions options) {
+ if (instanceId.instance().isTester())
+ throw new IllegalArgumentException("'" + instanceId + "' is a tester application!");
+
+ TenantAndApplicationId applicationId = TenantAndApplicationId.from(instanceId);
+ if ( getApplication(applicationId).isEmpty()
+ && controller.tenants().require(instanceId.tenant()).type() == Tenant.Type.user)
createApplication(applicationId, Optional.empty());
- try (Lock deploymentLock = lockForDeployment(applicationId, zone)) {
+ if (getInstance(instanceId).isEmpty())
+ createInstance(instanceId);
+
+ try (Lock deploymentLock = lockForDeployment(instanceId, zone)) {
Version platformVersion;
ApplicationVersion applicationVersion;
ApplicationPackage applicationPackage;
Set<ContainerEndpoint> endpoints;
Optional<ApplicationCertificate> applicationCertificate;
- try (Lock lock = lock(TenantAndApplicationId.from(applicationId))) {
- LockedApplication application = new LockedApplication(requireApplication(TenantAndApplicationId.from(applicationId)), lock);
- InstanceName instance = applicationId.instance();
+ try (Lock lock = lock(applicationId)) {
+ LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
+ InstanceName instance = instanceId.instance();
boolean manuallyDeployed = options.deployDirectly || zone.environment().isManuallyDeployed();
boolean preferOldestVersion = options.deployCurrentVersion;
@@ -348,25 +372,22 @@ public class ApplicationController {
if ( job.isEmpty()
|| job.get().lastTriggered().isEmpty()
|| job.get().lastCompleted().isPresent() && job.get().lastCompleted().get().at().isAfter(job.get().lastTriggered().get().at()))
- return unexpectedDeployment(applicationId, zone);
+ return unexpectedDeployment(instanceId, zone);
JobRun triggered = job.get().lastTriggered().get();
platformVersion = preferOldestVersion ? triggered.sourcePlatform().orElse(triggered.platform())
: triggered.platform();
applicationVersion = preferOldestVersion ? triggered.sourceApplication().orElse(triggered.application())
: triggered.application();
- applicationPackage = getApplicationPackage(applicationId, application.get().internal(), applicationVersion);
- applicationPackage = withTesterCertificate(applicationPackage, applicationId, jobType);
+ applicationPackage = getApplicationPackage(instanceId, application.get().internal(), applicationVersion);
+ applicationPackage = withTesterCertificate(applicationPackage, instanceId, jobType);
validateRun(application.get(), instance, zone, platformVersion, applicationVersion);
}
- // TODO jonmv: Remove this when all packages are validated upon submission, as in ApplicationApiHandler.submit(...).
- verifyApplicationIdentityConfiguration(applicationId.tenant(), applicationPackage, deployingIdentity);
-
if (zone.environment().isProduction()) // Assign and register endpoints
application = withRotation(applicationPackage.deploymentSpec(), application, instance);
- endpoints = registerEndpointsInDns(applicationPackage.deploymentSpec(), application.get().require(applicationId.instance()), zone);
+ endpoints = registerEndpointsInDns(applicationPackage.deploymentSpec(), application.get().require(instanceId.instance()), zone);
if (controller.zoneRegistry().zones().directlyRouted().ids().contains(zone)) {
// Provisions a new certificate if missing
@@ -385,11 +406,11 @@ public class ApplicationController {
// Carry out deployment without holding the application lock.
options = withVersion(platformVersion, options);
- ActivateResult result = deploy(applicationId, applicationPackage, zone, options, endpoints,
+ ActivateResult result = deploy(instanceId, applicationPackage, zone, options, endpoints,
applicationCertificate.orElse(null));
- lockApplicationOrThrow(TenantAndApplicationId.from(applicationId), application ->
- store(application.with(applicationId.instance(),
+ lockApplicationOrThrow(applicationId, application ->
+ store(application.with(instanceId.instance(),
instance -> instance.withNewDeployment(zone, applicationVersion, platformVersion,
clock.instant(), warningsFrom(result)))));
return result;
@@ -702,22 +723,24 @@ public class ApplicationController {
*
* @throws IllegalArgumentException if the application has deployments or the caller is not authorized
*/
- public void deleteApplication(TenantName tenantName, ApplicationName applicationName, Optional<Credentials> credentials) {
- Tenant tenant = controller.tenants().require(tenantName);
+ public void deleteApplication(TenantAndApplicationId id, Optional<Credentials> credentials) {
+ Tenant tenant = controller.tenants().require(id.tenant());
if (tenant.type() != Tenant.Type.user && credentials.isEmpty())
- throw new IllegalArgumentException("Could not delete application '" + tenantName + "." + applicationName + "': No credentials provided");
+ throw new IllegalArgumentException("Could not delete application '" + id + "': No credentials provided");
// Find all instances of the application
- TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
List<ApplicationId> instances = requireApplication(id).instances().keySet().stream()
.map(id::instance)
.collect(Collectors.toUnmodifiableList());
if (instances.size() > 1)
throw new IllegalArgumentException("Could not delete application; more than one instance present: " + instances);
- // TODO: Make this one transaction when database is moved to ZooKeeper
for (ApplicationId instance : instances)
- deleteInstance(instance, credentials);
+ deleteInstance(instance);
+
+ if (tenant.type() != Tenant.Type.user)
+ accessControl.deleteApplication(id, credentials.get());
+ curator.removeApplication(id);
}
/**
@@ -726,24 +749,20 @@ public class ApplicationController {
* @throws IllegalArgumentException if the application has deployments or the caller is not authorized
* @throws NotExistsException if the instance does not exist
*/
- public void deleteInstance(ApplicationId applicationId, Optional<Credentials> credentials) {
- Tenant tenant = controller.tenants().require(applicationId.tenant());
- if (tenant.type() != Tenant.Type.user && credentials.isEmpty())
- throw new IllegalArgumentException("Could not delete application '" + applicationId + "': No credentials provided");
-
- if (getInstance(applicationId).isEmpty())
- throw new NotExistsException("Could not delete application '" + applicationId + "': Application not found");
+ public void deleteInstance(ApplicationId instanceId) {
+ if (getInstance(instanceId).isEmpty())
+ throw new NotExistsException("Could not delete instance '" + instanceId + "': Instance not found");
- lockApplicationOrThrow(TenantAndApplicationId.from(applicationId), application -> {
- if ( ! application.get().require(applicationId.instance()).deployments().isEmpty())
+ lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> {
+ if ( ! application.get().require(instanceId.instance()).deployments().isEmpty())
throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments in: " +
- application.get().require(applicationId.instance()).deployments().keySet().stream().map(ZoneId::toString)
+ application.get().require(instanceId.instance()).deployments().keySet().stream().map(ZoneId::toString)
.sorted().collect(Collectors.joining(", ")));
- applicationStore.removeAll(applicationId);
- applicationStore.removeAll(TesterId.of(applicationId));
+ applicationStore.removeAll(instanceId);
+ applicationStore.removeAll(TesterId.of(instanceId));
- Instance instance = application.get().require(applicationId.instance());
+ Instance instance = application.get().require(instanceId.instance());
instance.rotations().forEach(assignedRotation -> {
var endpoints = instance.endpointsIn(controller.system(), assignedRotation.endpointId());
endpoints.asList().stream()
@@ -752,15 +771,10 @@ public class ApplicationController {
controller.nameServiceForwarder().removeRecords(Record.Type.CNAME, RecordName.from(name), Priority.normal);
});
});
- curator.storeWithoutInstance(application.without(applicationId.instance()).get());
+ curator.writeApplication(application.without(instanceId.instance()).get());
- log.info("Deleted " + application);
+ log.info("Deleted " + instanceId);
});
-
-
- if (tenant.type() != Tenant.Type.user && getApplication(applicationId).isEmpty())
- // TODO jonmv: Implementations ignore the instance — refactor to provide tenant and application names only.
- accessControl.deleteApplication(applicationId, credentials.get());
}
/**
@@ -845,10 +859,12 @@ public class ApplicationController {
public DeploymentTrigger deploymentTrigger() { return deploymentTrigger; }
+ private TenantAndApplicationId dashToUnderscore(TenantAndApplicationId id) {
+ return TenantAndApplicationId.from(id.tenant().value(), id.application().value().replaceAll("-", "_"));
+ }
+
private ApplicationId dashToUnderscore(ApplicationId id) {
- return ApplicationId.from(id.tenant().value(),
- id.application().value().replaceAll("-", "_"),
- id.instance().value());
+ return dashToUnderscore(TenantAndApplicationId.from(id)).instance(id.instance());
}
/**
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 5aa5a8e13de..19921595dc2 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
@@ -11,11 +11,10 @@ import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
+import java.security.PublicKey;
import java.time.Instant;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -43,7 +42,7 @@ public class LockedApplication {
private final Optional<User> owner;
private final OptionalInt majorVersion;
private final ApplicationMetrics metrics;
- private final Set<String> pemDeployKeys;
+ private final Set<PublicKey> deployKeys;
private final OptionalLong projectId;
private final boolean internal;
private final Map<InstanceName, Instance> instances;
@@ -58,14 +57,14 @@ public class LockedApplication {
this(Objects.requireNonNull(lock, "lock cannot be null"), application.id(), application.createdAt(),
application.deploymentSpec(), application.validationOverrides(), application.change(),
application.outstandingChange(), application.deploymentIssueId(), application.ownershipIssueId(),
- application.owner(), application.majorVersion(), application.metrics(), application.pemDeployKeys(),
+ application.owner(), application.majorVersion(), application.metrics(), application.deployKeys(),
application.projectId(), application.internal(), application.instances());
}
private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec,
ValidationOverrides validationOverrides, Change change, Change outstandingChange,
Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Set<String> pemDeployKeys,
+ OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys,
OptionalLong projectId, boolean internal,
Map<InstanceName, Instance> instances) {
this.lock = lock;
@@ -80,7 +79,7 @@ public class LockedApplication {
this.owner = owner;
this.majorVersion = majorVersion;
this.metrics = metrics;
- this.pemDeployKeys = pemDeployKeys;
+ this.deployKeys = deployKeys;
this.projectId = projectId;
this.internal = internal;
this.instances = Map.copyOf(instances);
@@ -89,7 +88,7 @@ public class LockedApplication {
/** Returns a read-only copy of this */
public Application get() {
return new Application(id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances.values());
}
@@ -97,7 +96,7 @@ public class LockedApplication {
var instances = new HashMap<>(this.instances);
instances.put(instance, new Instance(id.instance(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
@@ -105,7 +104,7 @@ public class LockedApplication {
var instances = new HashMap<>(this.instances);
instances.put(instance, modification.apply(instances.get(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
@@ -113,61 +112,61 @@ public class LockedApplication {
var instances = new HashMap<>(this.instances);
instances.remove(instance);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication withBuiltInternally(boolean builtInternally) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, builtInternally, instances);
}
public LockedApplication withProjectId(OptionalLong projectId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication withDeploymentIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication with(DeploymentSpec deploymentSpec) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication with(ValidationOverrides validationOverrides) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication withChange(Change change) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication withOutstandingChange(Change outstandingChange) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication withOwnershipIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
public LockedApplication withOwner(User owner) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
@@ -176,25 +175,25 @@ public class LockedApplication {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
deploymentIssueId, ownershipIssueId, owner,
majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion),
- metrics, pemDeployKeys, projectId, internal, instances);
+ metrics, deployKeys, projectId, internal, instances);
}
public LockedApplication with(ApplicationMetrics metrics) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, pemDeployKeys,
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
projectId, internal, instances);
}
- public LockedApplication withPemDeployKey(String pemDeployKey) {
- Set<String> keys = new LinkedHashSet<>(pemDeployKeys);
+ public LockedApplication withDeployKey(PublicKey pemDeployKey) {
+ Set<PublicKey> keys = new LinkedHashSet<>(deployKeys);
keys.add(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
projectId, internal, instances);
}
- public LockedApplication withoutPemDeployKey(String pemDeployKey) {
- Set<String> keys = new LinkedHashSet<>(pemDeployKeys);
+ public LockedApplication withoutDeployKey(PublicKey pemDeployKey) {
+ Set<PublicKey> keys = new LinkedHashSet<>(deployKeys);
keys.remove(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, change, outstandingChange,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
index ecc8bd65b72..6caf716aed4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
@@ -2,8 +2,10 @@
package com.yahoo.vespa.hosted.controller;
import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.security.KeyUtils;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
@@ -16,6 +18,7 @@ import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import java.security.Principal;
+import java.security.PublicKey;
import java.util.Optional;
import static java.util.Objects.requireNonNull;
@@ -126,44 +129,39 @@ public abstract class LockedTenant {
public static class Cloud extends LockedTenant {
private final BillingInfo billingInfo;
- private final BiMap<String, Principal> pemDeveloperKeys;
+ private final BiMap<PublicKey, Principal> developerKeys;
- private Cloud(TenantName name, BillingInfo billingInfo, BiMap<String, Principal> pemDeveloperKeys) {
+ private Cloud(TenantName name, BillingInfo billingInfo, BiMap<PublicKey, Principal> developerKeys) {
super(name);
this.billingInfo = billingInfo;
- this.pemDeveloperKeys = pemDeveloperKeys;
+ this.developerKeys = ImmutableBiMap.copyOf(developerKeys);
}
private Cloud(CloudTenant tenant) {
- this(tenant.name(), tenant.billingInfo(), tenant.pemDeveloperKeys());
+ this(tenant.name(), tenant.billingInfo(), tenant.developerKeys());
}
@Override
public CloudTenant get() {
- return new CloudTenant(name, billingInfo, pemDeveloperKeys);
+ return new CloudTenant(name, billingInfo, developerKeys);
}
public Cloud with(BillingInfo billingInfo) {
- return new Cloud(name, billingInfo, pemDeveloperKeys);
- }
-
- public Cloud withPemDeveloperKey(String pemKey, Principal principal) {
- ImmutableBiMap.Builder<String, Principal> keys = ImmutableBiMap.builder();
- pemDeveloperKeys.forEach((key, user) -> {
- if ( ! user.equals(principal))
- keys.put(key, user);
- });
- keys.put(pemKey, principal);
- return new Cloud(name, billingInfo, keys.build());
- }
-
- public Cloud withoutPemDeveloperKey(String pemKey) {
- ImmutableBiMap.Builder<String, Principal> keys = ImmutableBiMap.builder();
- pemDeveloperKeys.forEach((key, user) -> {
- if ( ! key.equals(pemKey))
- keys.put(key, user);
- });
- return new Cloud(name, billingInfo, keys.build());
+ return new Cloud(name, billingInfo, developerKeys);
+ }
+
+ public Cloud withDeveloperKey(PublicKey key, Principal principal) {
+ BiMap<PublicKey, Principal> keys = HashBiMap.create(developerKeys);
+ if (keys.containsKey(key))
+ throw new IllegalArgumentException("Key " + KeyUtils.toPem(key) + " is already owned by " + keys.get(key));
+ keys.put(key, principal);
+ return new Cloud(name, billingInfo, keys);
+ }
+
+ public Cloud withoutDeveloperKey(PublicKey key) {
+ BiMap<PublicKey, Principal> keys = HashBiMap.create(developerKeys);
+ keys.remove(key);
+ return new Cloud(name, billingInfo, keys);
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
index 9df918e3f20..5ff564f7ad3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
@@ -63,7 +63,7 @@ public enum SystemApplication {
.orElse(false);
}
- /** Returns the node types of this that should receive OS upgrades */
+ /** Returns whether this should receive OS upgrades */
public boolean isEligibleForOsUpgrades() {
return nodeType.isDockerHost();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java
index 0b537535315..b4f0d6e2487 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java
@@ -48,6 +48,10 @@ public class TenantAndApplicationId implements Comparable<TenantAndApplicationId
return instance(InstanceName.defaultName());
}
+ public ApplicationId instance(String instance) {
+ return instance(InstanceName.from(instance));
+ }
+
public ApplicationId instance(InstanceName instance) {
return ApplicationId.from(tenant, application, instance);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
index 91f9e2d56d7..304a47044a1 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
@@ -2,7 +2,6 @@
package com.yahoo.vespa.hosted.controller.athenz.impl;
import com.google.inject.Inject;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.log.LogLevel;
@@ -15,10 +14,12 @@ import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
import com.yahoo.vespa.athenz.client.zms.RoleAction;
import com.yahoo.vespa.athenz.client.zms.ZmsClient;
+import com.yahoo.vespa.athenz.client.zms.ZmsClientException;
import com.yahoo.vespa.athenz.client.zts.ZtsClient;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.security.AccessControl;
import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
@@ -142,7 +143,7 @@ public class AthenzFacade implements AccessControl {
}
@Override
- public void createApplication(ApplicationId id, Credentials credentials) {
+ public void createApplication(TenantAndApplicationId id, Credentials credentials) {
AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
createApplication(athenzCredentials.domain(), id.application(), athenzCredentials.token());
}
@@ -152,11 +153,19 @@ public class AthenzFacade implements AccessControl {
log("createProviderResourceGroup(" +
"tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s, roleActions=%s)",
domain, service.getDomain().getName(), service.getName(), application, tenantRoleActions);
- zmsClient.createProviderResourceGroup(domain, service, application.value(), tenantRoleActions, token);
+ try {
+ zmsClient.createProviderResourceGroup(domain, service, application.value(), tenantRoleActions, token);
+ }
+ catch (ZmsClientException e) {
+ if (e.getErrorCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN)
+ throw new ForbiddenException("Not authorized to create application", e);
+ else
+ throw e;
+ }
}
@Override
- public void deleteApplication(ApplicationId id, Credentials credentials) {
+ public void deleteApplication(TenantAndApplicationId id, Credentials credentials) {
AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
athenzCredentials.domain(), service.getDomain().getName(), service.getName(), id.application());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
index 1828a189cad..50af8bd8611 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
@@ -1,8 +1,6 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.deployment;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
@@ -468,7 +466,7 @@ public class InternalStepRunner implements StepRunner {
testConfigSerializer.configJson(id.application(),
id.type(),
endpoints,
- listClusters(id.application(), zones)));
+ controller.applications().contentClustersByZone(id.application(), zones)));
return Optional.of(running);
}
@@ -690,14 +688,6 @@ public class InternalStepRunner implements StepRunner {
throw new IllegalStateException("No step deploys to the zone this run is for!");
}
- /** Returns all content clusters in all current deployments of the given real application. */
- private Map<ZoneId, List<String>> listClusters(ApplicationId id, Iterable<ZoneId> zones) {
- ImmutableMap.Builder<ZoneId, List<String>> clusters = ImmutableMap.builder();
- for (ZoneId zone : zones)
- clusters.put(zone, ImmutableList.copyOf(controller.serviceRegistry().configServer().getContentClusters(new DeploymentId(id, zone))));
- return clusters.build();
- }
-
/** Returns the generated services.xml content for the tester application. */
static byte[] servicesXml(AthenzDomain domain, boolean useAthenzCredentials, boolean useTesterCertificate,
NodeResources resources) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
index 0ecce359a02..54b5c339159 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
@@ -30,6 +30,7 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.persistence.BufferedLogStore;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
+import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.net.URI;
import java.security.cert.X509Certificate;
@@ -351,12 +352,22 @@ public class JobController {
/** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment. */
public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage) {
+ if ( ! type.environment().isManuallyDeployed())
+ throw new IllegalArgumentException("Direct deployments are only allowed to manually deployed environments.");
+
+ if ( controller.tenants().require(id.tenant()).type() == Tenant.Type.user
+ && controller.applications().getApplication(TenantAndApplicationId.from(id)).isEmpty())
+ controller.applications().createApplication(TenantAndApplicationId.from(id), Optional.empty());
+
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
if ( ! application.get().internal())
- controller.applications().store(registered(application));
+ application = registered(application);
+
+ if ( ! application.get().instances().containsKey(id.instance()))
+ application = application.withNewInstance(id.instance());
+
+ controller.applications().store(application);
});
- if ( ! type.environment().isManuallyDeployed())
- throw new IllegalArgumentException("Direct deployments are only allowed to manually deployed environments.");
last(id, type).filter(run -> ! run.hasEnded()).ifPresent(run -> abortAndWait(run.id()));
locked(id, type, __ -> {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
index 4e13a1c25e5..1743cad32e4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
@@ -67,7 +67,7 @@ public class ControllerMaintenance extends AbstractComponent {
deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(controller, Duration.ofMinutes(5), jobControl);
applicationOwnershipConfirmer = new ApplicationOwnershipConfirmer(controller, Duration.ofHours(12), jobControl, controller.serviceRegistry().ownershipIssues());
systemUpgrader = new SystemUpgrader(controller, Duration.ofMinutes(1), jobControl);
- jobRunner = new JobRunner(controller, Duration.ofMinutes(2), jobControl);
+ jobRunner = new JobRunner(controller, Duration.ofSeconds(90), jobControl);
osUpgraders = osUpgraders(controller, jobControl);
osVersionStatusUpdater = new OsVersionStatusUpdater(controller, maintenanceInterval, jobControl);
contactInformationMaintainer = new ContactInformationMaintainer(controller, Duration.ofHours(12), jobControl);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
index 79ababd20d3..9253e249765 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
@@ -1,7 +1,6 @@
-// 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.vespa.hosted.controller.maintenance;
-import com.google.common.collect.ImmutableMap;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -13,17 +12,19 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobList;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.rotation.RotationLock;
+import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
+ * This calculates and reports system-wide metrics based on data from a {@link Controller}.
+ *
* @author mortent
* @author mpolden
*/
@@ -34,9 +35,12 @@ public class MetricsReporter extends Maintainer {
public static final String DEPLOYMENT_FAILING_UPGRADES = "deployment.failingUpgrades";
public static final String DEPLOYMENT_BUILD_AGE_SECONDS = "deployment.buildAgeSeconds";
public static final String DEPLOYMENT_WARNINGS = "deployment.warnings";
+ public static final String NODES_FAILING_SYSTEM_UPGRADE = "deployment.nodesFailingSystemUpgrade";
public static final String REMAINING_ROTATIONS = "remaining_rotations";
public static final String NAME_SERVICE_REQUESTS_QUEUED = "dns.queuedRequests";
+ private static final Duration NODE_UPGRADE_TIMEOUT = Duration.ofHours(1);
+
private final Metric metric;
private final Clock clock;
@@ -51,12 +55,13 @@ public class MetricsReporter extends Maintainer {
reportDeploymentMetrics();
reportRemainingRotations();
reportQueuedNameServiceRequests();
+ reportNodesFailingSystemUpgrade();
}
private void reportRemainingRotations() {
try (RotationLock lock = controller().applications().rotationRepository().lock()) {
int availableRotations = controller().applications().rotationRepository().availableRotations(lock).size();
- metric.set(REMAINING_ROTATIONS, availableRotations, metric.createContext(Collections.emptyMap()));
+ metric.set(REMAINING_ROTATIONS, availableRotations, metric.createContext(Map.of()));
}
}
@@ -66,7 +71,7 @@ public class MetricsReporter extends Maintainer {
.flatMap(application -> application.instances().values().stream())
.collect(Collectors.toUnmodifiableList());
- metric.set(DEPLOYMENT_FAIL_METRIC, deploymentFailRatio(instances) * 100, metric.createContext(Collections.emptyMap()));
+ metric.set(DEPLOYMENT_FAIL_METRIC, deploymentFailRatio(instances) * 100, metric.createContext(Map.of()));
averageDeploymentDurations(instances, clock.instant()).forEach((application, duration) -> {
metric.set(DEPLOYMENT_AVERAGE_DURATION, duration.getSeconds(), metric.createContext(dimensions(application)));
@@ -93,6 +98,24 @@ public class MetricsReporter extends Maintainer {
metric.set(NAME_SERVICE_REQUESTS_QUEUED, controller().curator().readNameServiceQueue().requests().size(),
metric.createContext(Map.of()));
}
+
+ private void reportNodesFailingSystemUpgrade() {
+ metric.set(NODES_FAILING_SYSTEM_UPGRADE, nodesFailingSystemUpgrade(), metric.createContext(Map.of()));
+ }
+
+ private int nodesFailingSystemUpgrade() {
+ if (!controller().versionStatus().isUpgrading()) return 0;
+ var nodesFailingUpgrade = 0;
+ var acceptableInstant = clock.instant().minus(NODE_UPGRADE_TIMEOUT);
+ for (var vespaVersion : controller().versionStatus().versions()) {
+ if (vespaVersion.confidence() == VespaVersion.Confidence.broken) continue;
+ for (var nodeVersion : vespaVersion.nodeVersions().asMap().values()) {
+ if (!nodeVersion.changing()) continue;
+ if (nodeVersion.changedAt().isBefore(acceptableInstant)) nodesFailingUpgrade++;
+ }
+ }
+ return nodesFailingUpgrade;
+ }
private static double deploymentFailRatio(List<Instance> instances) {
return instances.stream()
@@ -149,10 +172,8 @@ public class MetricsReporter extends Maintainer {
}
private static Map<String, String> dimensions(ApplicationId application) {
- return ImmutableMap.of(
- "tenant", application.tenant().value(),
- "app",application.application().value() + "." + application.instance().value()
- );
+ return Map.of("tenant", application.tenant().value(),
+ "app",application.application().value() + "." + application.instance().value());
}
}
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 08b3355587f..61fd0b67ec9 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
@@ -1,14 +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.vespa.hosted.controller.persistence;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.ImmutableBiMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.security.KeyUtils;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
@@ -21,7 +20,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
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.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -39,7 +37,7 @@ import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.rotation.RotationState;
import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
-import java.security.Principal;
+import java.security.PublicKey;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
@@ -54,7 +52,6 @@ import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* Serializes {@link Application}s to/from slime.
@@ -192,7 +189,7 @@ public class ApplicationSerializer {
application.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion));
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
- deployKeysToSlime(application.pemDeployKeys().stream(), root.setArray(pemDeployKeysField));
+ deployKeysToSlime(application.deployKeys(), root.setArray(pemDeployKeysField));
instancesToSlime(application, root.setArray(instancesField));
return slime;
}
@@ -208,8 +205,8 @@ public class ApplicationSerializer {
}
}
- private void deployKeysToSlime(Stream<String> pemDeployKeys, Cursor array) {
- pemDeployKeys.forEach(array::addString);
+ private void deployKeysToSlime(Set<PublicKey> deployKeys, Cursor array) {
+ deployKeys.forEach(key -> array.addString(KeyUtils.toPem(key)));
}
private void deploymentsToSlime(Collection<Deployment> deployments, Cursor array) {
@@ -384,14 +381,14 @@ public class ApplicationSerializer {
OptionalInt majorVersion = Serializers.optionalInteger(root.field(majorVersionField));
ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
root.field(writeQualityField).asDouble());
- Set<String> pemDeployKeys = pemDeployKeysFromSlime(root.field(pemDeployKeysField));
+ Set<PublicKey> deployKeys = deployKeysFromSlime(root.field(pemDeployKeysField));
List<Instance> instances = instancesFromSlime(id, deploymentSpec, root.field(instancesField));
OptionalLong projectId = Serializers.optionalLong(root.field(projectIdField));
boolean builtInternally = root.field(builtInternallyField).asBool();
return new Application(id, createdAt, deploymentSpec, validationOverrides, deploying, outstandingChange,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics,
- pemDeployKeys, projectId, builtInternally, instances);
+ deployKeys, projectId, builtInternally, instances);
}
private List<Instance> instancesFromSlime(TenantAndApplicationId id, DeploymentSpec deploymentSpec, Inspector field) {
@@ -411,9 +408,9 @@ public class ApplicationSerializer {
return instances;
}
- private Set<String> pemDeployKeysFromSlime(Inspector array) {
- Set<String> keys = new LinkedHashSet<>();
- array.traverse((ArrayTraverser) (__, key) -> keys.add(key.asString()));
+ private Set<PublicKey> deployKeysFromSlime(Inspector array) {
+ Set<PublicKey> keys = new LinkedHashSet<>();
+ array.traverse((ArrayTraverser) (__, key) -> keys.add(KeyUtils.fromPemEncodedPublicKey(key.asString())));
return keys;
}
@@ -428,7 +425,7 @@ public class ApplicationSerializer {
applicationVersionFromSlime(deploymentObject.field(applicationPackageRevisionField)),
Version.fromString(deploymentObject.field(versionField).asString()),
Instant.ofEpochMilli(deploymentObject.field(deployTimeField).asLong()),
- clusterUtilsMapFromSlime(deploymentObject.field(clusterUtilsField)),
+ Map.of(),
clusterInfoMapFromSlime(deploymentObject.field(clusterInfoField)),
deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)),
DeploymentActivity.create(Serializers.optionalInstant(deploymentObject.field(lastQueriedField)),
@@ -484,21 +481,6 @@ public class ApplicationSerializer {
return map;
}
- private Map<ClusterSpec.Id, ClusterUtilization> clusterUtilsMapFromSlime(Inspector object) {
- Map<ClusterSpec.Id, ClusterUtilization> map = new HashMap<>();
- object.traverse((String name, Inspector value) -> map.put(new ClusterSpec.Id(name), clusterUtililzationFromSlime(value)));
- return map;
- }
-
- private ClusterUtilization clusterUtililzationFromSlime(Inspector object) {
- double cpu = object.field(clusterUtilsCpuField).asDouble();
- double mem = object.field(clusterUtilsMemField).asDouble();
- double disk = object.field(clusterUtilsDiskField).asDouble();
- double diskBusy = object.field(clusterUtilsDiskBusyField).asDouble();
-
- return new ClusterUtilization(mem, cpu, disk, diskBusy);
- }
-
private ClusterInfo clusterInfoFromSlime(Inspector inspector) {
String flavor = inspector.field(clusterInfoFlavorField).asString();
int cost = (int)inspector.field(clusterInfoCostField).asLong();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
index 9501ac5a7f9..357dbb37b27 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
@@ -360,22 +360,11 @@ public class CuratorDb {
private Stream<TenantAndApplicationId> readApplicationIds() {
return curator.getChildren(applicationRoot).stream()
- .filter(id -> id.split(":").length == 2)
.map(TenantAndApplicationId::fromSerialized);
}
- public void deleteOldApplicationData() {
- curator.getChildren(applicationRoot).stream()
- .filter(id -> id.split(":").length == 3)
- .forEach(id -> curator.delete(applicationRoot.append(id)));
- }
-
- // TODO jonmv: Refactor when instance split operation is done
- public void storeWithoutInstance(Application application) {
- if (application.instances().isEmpty())
- curator.delete(applicationPath(application.id()));
- else
- writeApplication(application);
+ public void removeApplication(TenantAndApplicationId id) {
+ curator.delete(applicationPath(id));
}
// -------------- Job Runs ------------------------------------------------
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
index 78d166607df..35128466e4d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.persistence;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.security.KeyUtils;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
@@ -22,6 +23,7 @@ import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import java.net.URI;
import java.security.Principal;
+import java.security.PublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -91,14 +93,14 @@ public class TenantSerializer {
}
private void toSlime(CloudTenant tenant, Cursor root) {
- pemDeveloperKeysToSlime(tenant.pemDeveloperKeys(), root.setArray(pemDeveloperKeysField));
+ developerKeysToSlime(tenant.developerKeys(), root.setArray(pemDeveloperKeysField));
toSlime(tenant.billingInfo(), root.setObject(billingInfoField));
}
- private void pemDeveloperKeysToSlime(BiMap<String, Principal> keys, Cursor array) {
+ private void developerKeysToSlime(BiMap<PublicKey, Principal> keys, Cursor array) {
keys.forEach((key, user) -> {
Cursor object = array.addObject();
- object.setString("key", key);
+ object.setString("key", KeyUtils.toPem(key));
object.setString("user", user.getName());
});
}
@@ -139,15 +141,16 @@ public class TenantSerializer {
private CloudTenant cloudTenantFrom(Inspector tenantObject) {
TenantName name = TenantName.from(tenantObject.field(nameField).asString());
BillingInfo billingInfo = billingInfoFrom(tenantObject.field(billingInfoField));
- BiMap<String, Principal> pemDeveloperKeys = pemDeveloperKeysFromSlime(tenantObject.field(pemDeveloperKeysField));
- return new CloudTenant(name, billingInfo, pemDeveloperKeys);
+ BiMap<PublicKey, Principal> developerKeys = developerKeysFromSlime(tenantObject.field(pemDeveloperKeysField));
+ return new CloudTenant(name, billingInfo, developerKeys);
}
- private BiMap<String, Principal> pemDeveloperKeysFromSlime(Inspector array) {
- ImmutableBiMap.Builder<String, Principal> keys = ImmutableBiMap.builder();
- array.traverse((ArrayTraverser) (__, keyObject) -> {
- keys.put(keyObject.field("key").asString(), new SimplePrincipal(keyObject.field("user").asString()));
- });
+ private BiMap<PublicKey, Principal> developerKeysFromSlime(Inspector array) {
+ ImmutableBiMap.Builder<PublicKey, Principal> keys = ImmutableBiMap.builder();
+ array.traverse((ArrayTraverser) (__, keyObject) ->
+ keys.put(KeyUtils.fromPemEncodedPublicKey(keyObject.field("key").asString()),
+ new SimplePrincipal(keyObject.field("user").asString())));
+
return keys.build();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
index 207a5f8dcf9..5061f32da68 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
@@ -1,6 +1,7 @@
-// Copyright 2018 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.vespa.hosted.controller.persistence;
+import com.google.common.collect.ImmutableMap;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
@@ -9,6 +10,8 @@ import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics;
+import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
+import com.yahoo.vespa.hosted.controller.versions.NodeVersions;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
@@ -47,6 +50,14 @@ public class VersionStatusSerializer {
private static final String confidenceField = "confidence";
private static final String configServersField = "configServerHostnames";
+ // NodeVersions fields
+ private static final String nodeVersionsField = "nodeVersions";
+
+ // NodeVersion fields
+ private static final String hostnameField = "hostname";
+ private static final String wantedVersionField = "wantedVersion";
+ private static final String changedAtField = "changedAt";
+
// DeploymentStatistics fields
private static final String versionField = "version";
private static final String failingField = "failing";
@@ -77,9 +88,20 @@ public class VersionStatusSerializer {
object.setBool(isReleasedField, version.isReleased());
deploymentStatisticsToSlime(version.statistics(), object.setObject(deploymentStatisticsField));
object.setString(confidenceField, version.confidence().name());
- configServersToSlime(version.systemApplicationHostnames(), object.setArray(configServersField));
+ configServersToSlime(version.nodeVersions().hostnames(), object.setArray(configServersField));
+ nodeVersionsToSlime(version.nodeVersions(), object.setArray(nodeVersionsField));
+ }
+
+ private void nodeVersionsToSlime(NodeVersions nodeVersions, Cursor array) {
+ for (NodeVersion nodeVersion : nodeVersions.asMap().values()) {
+ var nodeVersionObject = array.addObject();
+ nodeVersionObject.setString(hostnameField, nodeVersion.hostname().value());
+ nodeVersionObject.setString(wantedVersionField, nodeVersion.wantedVersion().toFullString());
+ nodeVersionObject.setLong(changedAtField, nodeVersion.changedAt().toEpochMilli());
+ }
}
+ // TODO(mpolden): Remove after October 2019
private void configServersToSlime(Set<HostName> configServerHostnames, Cursor array) {
configServerHostnames.stream().map(HostName::value).forEach(array::addString);
}
@@ -102,17 +124,38 @@ public class VersionStatusSerializer {
}
private VespaVersion vespaVersionFromSlime(Inspector object) {
- return new VespaVersion(deploymentStatisticsFromSlime(object.field(deploymentStatisticsField)),
+ var deploymentStatistics = deploymentStatisticsFromSlime(object.field(deploymentStatisticsField));
+ return new VespaVersion(deploymentStatistics,
object.field(releaseCommitField).asString(),
Instant.ofEpochMilli(object.field(committedAtField).asLong()),
object.field(isControllerVersionField).asBool(),
object.field(isSystemVersionField).asBool(),
- object.field(isReleasedField).valid() ? object.field(isReleasedField).asBool() : true,
- configServersFromSlime(object.field(configServersField)),
+ object.field(isReleasedField).asBool(),
+ nodeVersionsFromSlime(object, deploymentStatistics.version()),
VespaVersion.Confidence.valueOf(object.field(confidenceField).asString())
);
}
+ private NodeVersions nodeVersionsFromSlime(Inspector root, Version version) {
+ var nodeVersions = ImmutableMap.<HostName, NodeVersion>builder();
+ var nodeVersionsRoot = root.field(nodeVersionsField);
+ if (nodeVersionsRoot.valid()) {
+ nodeVersionsRoot.traverse((ArrayTraverser) (i, entry) -> {
+ var hostname = HostName.from(entry.field(hostnameField).asString());
+ var wantedVersion = Version.fromString(entry.field(wantedVersionField).asString());
+ var changedAt = Instant.ofEpochMilli(entry.field(changedAtField).asLong());
+ nodeVersions.put(hostname, new NodeVersion(hostname, version, wantedVersion, changedAt));
+ });
+ } else {
+ // TODO(mpolden): Remove after October 2019
+ var configServerHostnames = configServersFromSlime(root.field(configServersField));
+ for (var hostname : configServerHostnames) {
+ nodeVersions.put(hostname, NodeVersion.empty(hostname));
+ }
+ }
+ return new NodeVersions(nodeVersions.build());
+ }
+
private Set<HostName> configServersFromSlime(Inspector array) {
Set<HostName> configServerHostnames = new LinkedHashSet<>();
array.traverse((ArrayTraverser) (i, entry) -> configServerHostnames.add(HostName.from(entry.asString())));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
index 4c4478c9af6..24819fda261 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
@@ -6,8 +6,8 @@ import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import com.yahoo.component.Version;
+import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
@@ -16,19 +16,23 @@ import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.LoggingRequestHandler;
import com.yahoo.io.IOUtils;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.Path;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.restapi.ResourceResponse;
+import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzPrincipal;
import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.client.zms.ZmsClientException;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.AlreadyExistsException;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.NotExistsException;
import com.yahoo.vespa.hosted.controller.api.ActivateResult;
@@ -69,10 +73,6 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
import com.yahoo.vespa.hosted.controller.deployment.TestConfigSerializer;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.ResourceResponse;
-import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.vespa.hosted.controller.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.rotation.RotationState;
import com.yahoo.vespa.hosted.controller.rotation.RotationStatus;
@@ -96,12 +96,12 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.security.DigestInputStream;
import java.security.Principal;
+import java.security.PublicKey;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
-import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -132,6 +132,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private final Controller controller;
private final AccessControlRequests accessControlRequests;
+ private final TestConfigSerializer testConfigSerializer;
@Inject
public ApplicationApiHandler(LoggingRequestHandler.Context parentCtx,
@@ -140,6 +141,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
super(parentCtx);
this.controller = controller;
this.accessControlRequests = accessControlRequests;
+ this.testConfigSerializer = new TestConfigSerializer(controller.system());
}
@Override
@@ -240,14 +242,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse handlePOST(Path path, HttpRequest request) {
if (path.matches("/application/v4/tenant/{tenant}")) return createTenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/key")) return addDeveloperKey(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), "default", request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), "default", false, request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), "default", true, request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), "default", request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/jobreport")) return notifyJobCompletion(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return addDeployKey(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return submit(path.get("tenant"), path.get("application"), "default", request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return createApplication(path.get("tenant"), path.get("application"), path.get("instance"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return createInstance(path.get("tenant"), path.get("application"), path.get("instance"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{jobtype}")) return jobDeploy(appIdFromPath(path), jobTypeFromPath(path), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), false, request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), true, request);
@@ -377,9 +379,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Principal user = request.getJDiscRequest().getUserPrincipal();
String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString();
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant ->
- controller.tenants().store(tenant.withPemDeveloperKey(pemDeveloperKey, user)));
- return new MessageResponse("Set developer key " + pemDeveloperKey + " for " + user);
+ PublicKey developerKey = KeyUtils.fromPemEncodedPublicKey(pemDeveloperKey);
+ Slime root = new Slime();
+ controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
+ tenant = tenant.withDeveloperKey(developerKey, user);
+ toSlime(root.setObject().setArray("keys"), tenant.get().developerKeys());
+ controller.tenants().store(tenant);
+ });
+ return new SlimeJsonResponse(root);
}
private HttpResponse removeDeveloperKey(String tenantName, HttpRequest request) {
@@ -387,26 +394,51 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString();
- Principal user = ((CloudTenant) controller.tenants().require(TenantName.from(tenantName))).pemDeveloperKeys().get(pemDeveloperKey);
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant ->
- controller.tenants().store(tenant.withoutPemDeveloperKey(pemDeveloperKey)));
- return new MessageResponse("Removed developer key " + pemDeveloperKey + " for " + user);
+ PublicKey developerKey = KeyUtils.fromPemEncodedPublicKey(pemDeveloperKey);
+ Principal user = ((CloudTenant) controller.tenants().require(TenantName.from(tenantName))).developerKeys().get(developerKey);
+ Slime root = new Slime();
+ controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
+ tenant = tenant.withoutDeveloperKey(developerKey);
+ toSlime(root.setObject().setArray("keys"), tenant.get().developerKeys());
+ controller.tenants().store(tenant);
+ });
+ return new SlimeJsonResponse(root);
+ }
+
+ private void toSlime(Cursor keysArray, Map<PublicKey, Principal> keys) {
+ keys.forEach((key, principal) -> {
+ Cursor keyObject = keysArray.addObject();
+ keyObject.setString("key", KeyUtils.toPem(key));
+ keyObject.setString("user", principal.getName());
+ });
}
private HttpResponse addDeployKey(String tenantName, String applicationName, HttpRequest request) {
String pemDeployKey = toSlime(request.getData()).get().field("key").asString();
+ PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey);
+ Slime root = new Slime();
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
- controller.applications().store(application.withPemDeployKey(pemDeployKey));
+ application = application.withDeployKey(deployKey);
+ application.get().deployKeys().stream()
+ .map(KeyUtils::toPem)
+ .forEach(root.setObject().setArray("keys")::addString);
+ controller.applications().store(application);
});
- return new MessageResponse("Added deploy key " + pemDeployKey);
+ return new SlimeJsonResponse(root);
}
private HttpResponse removeDeployKey(String tenantName, String applicationName, HttpRequest request) {
String pemDeployKey = toSlime(request.getData()).get().field("key").asString();
+ PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey);
+ Slime root = new Slime();
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
- controller.applications().store(application.withoutPemDeployKey(pemDeployKey));
+ application = application.withoutDeployKey(deployKey);
+ application.get().deployKeys().stream()
+ .map(KeyUtils::toPem)
+ .forEach(root.setObject().setArray("keys")::addString);
+ controller.applications().store(application);
});
- return new MessageResponse("Removed deploy key " + pemDeployKey);
+ return new SlimeJsonResponse(root);
}
private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) {
@@ -424,7 +456,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Inspector pemDeployKeyField = requestObject.field("pemDeployKey");
if (pemDeployKeyField.valid()) {
String pemDeployKey = pemDeployKeyField.asString();
- application = application.withPemDeployKey(pemDeployKey);
+ PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey);
+ application = application.withDeployKey(deployKey);
messageBuilder.add("Added deploy key " + pemDeployKey);
}
@@ -654,9 +687,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
// TODO jonmv: Remove when clients are updated
- application.pemDeployKeys().stream().findFirst().ifPresent(key -> object.setString("pemDeployKey", key));
+ application.deployKeys().stream().findFirst().ifPresent(key -> object.setString("pemDeployKey", KeyUtils.toPem(key)));
- application.pemDeployKeys().forEach(object.setArray("pemDeployKeys")::addString);
+ application.deployKeys().stream().map(KeyUtils::toPem).forEach(object.setArray("pemDeployKeys")::addString);
// Metrics
Cursor metricsObject = object.setObject("metrics");
@@ -1017,25 +1050,30 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return tenant(controller.tenants().require(TenantName.from(tenantName)), request);
}
- private HttpResponse createApplication(String tenantName, String applicationName, String instanceName, HttpRequest request) {
+ private HttpResponse createApplication(String tenantName, String applicationName, HttpRequest request) {
Inspector requestObject = toSlime(request.getData()).get();
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- try {
- Optional<Credentials> credentials = controller.tenants().require(id.tenant()).type() == Tenant.Type.user
- ? Optional.empty()
- : Optional.of(accessControlRequests.credentials(id.tenant(), requestObject, request.getJDiscRequest()));
- Application application = controller.applications().createApplication(id, credentials);
-
- Slime slime = new Slime();
- toSlime(id, slime.setObject(), request);
- return new SlimeJsonResponse(slime);
- }
- catch (ZmsClientException e) { // TODO: Push conversion down
- if (e.getErrorCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN)
- throw new ForbiddenException("Not authorized to create application", e);
- else
- throw e;
- }
+ TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
+ Optional<Credentials> credentials = controller.tenants().require(id.tenant()).type() == Tenant.Type.user
+ ? Optional.empty()
+ : Optional.of(accessControlRequests.credentials(id.tenant(), requestObject, request.getJDiscRequest()));
+ Application application = controller.applications().createApplication(id, credentials);
+
+ Slime slime = new Slime();
+ toSlime(id, slime.setObject(), request);
+ return new SlimeJsonResponse(slime);
+ }
+
+ // TODO jonmv: Remove when clients are updated.
+ private HttpResponse createInstance(String tenantName, String applicationName, String instanceName, HttpRequest request) {
+ TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenantName, applicationName);
+ if (controller.applications().getApplication(applicationId).isEmpty())
+ createApplication(tenantName, applicationName, request);
+
+ controller.applications().createInstance(applicationId.instance(instanceName));
+
+ Slime slime = new Slime();
+ toSlime(applicationId.instance(instanceName), slime.setObject(), request);
+ return new SlimeJsonResponse(slime);
}
/** Trigger deployment of the given Vespa version if a valid one is given, e.g., "7.8.9". */
@@ -1227,12 +1265,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
deployOptions.field("ignoreValidationErrors").asBool(),
deployOptions.field("deployCurrentVersion").asBool());
+ applicationPackage.ifPresent(aPackage -> controller.applications().verifyApplicationIdentityConfiguration(applicationId.tenant(),
+ aPackage,
+ Optional.of(requireUserPrincipal(request))));
+
ActivateResult result = controller.applications().deploy(applicationId,
zone,
applicationPackage,
applicationVersion,
- deployOptionsJsonClass,
- Optional.of(requireUserPrincipal(request)));
+ deployOptionsJsonClass);
return new SlimeJsonResponse(toSlime(result));
}
@@ -1255,22 +1296,23 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse deleteApplication(String tenantName, String applicationName, HttpRequest request) {
- TenantName tenant = TenantName.from(tenantName);
- ApplicationName application = ApplicationName.from(applicationName);
- Optional<Credentials> credentials = controller.tenants().require(tenant).type() == Tenant.Type.user
+ TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
+ Optional<Credentials> credentials = controller.tenants().require(id.tenant()).type() == Tenant.Type.user
? Optional.empty()
- : Optional.of(accessControlRequests.credentials(tenant, toSlime(request.getData()).get(), request.getJDiscRequest()));
- controller.applications().deleteApplication(tenant, application, credentials);
- return new MessageResponse("Deleted application " + tenant + "." + application);
+ : Optional.of(accessControlRequests.credentials(id.tenant(), toSlime(request.getData()).get(), request.getJDiscRequest()));
+ controller.applications().deleteApplication(id, credentials);
+ return new MessageResponse("Deleted application " + id);
}
private HttpResponse deleteInstance(String tenantName, String applicationName, String instanceName, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
+ TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
Optional<Credentials> credentials = controller.tenants().require(id.tenant()).type() == Tenant.Type.user
? Optional.empty()
: Optional.of(accessControlRequests.credentials(id.tenant(), toSlime(request.getData()).get(), request.getJDiscRequest()));
- controller.applications().deleteInstance(id, credentials);
- return new MessageResponse("Deleted instance " + id.toFullString());
+ controller.applications().deleteInstance(id.instance(instanceName));
+ if (controller.applications().requireApplication(id).instances().isEmpty())
+ controller.applications().deleteApplication(id, credentials);
+ return new MessageResponse("Deleted instance " + id.instance(instanceName).toFullString());
}
private HttpResponse deactivate(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
@@ -1300,11 +1342,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse testConfig(ApplicationId id, JobType type) {
- var endpoints = controller.applications().clusterEndpoints(id, controller.jobController().testedZoneAndProductionZones(id, type));
- return new SlimeJsonResponse(new TestConfigSerializer(controller.system()).configSlime(id,
- type,
- endpoints,
- Collections.emptyMap()));
+ Set<ZoneId> zones = controller.jobController().testedZoneAndProductionZones(id, type);
+ return new SlimeJsonResponse(testConfigSerializer.configSlime(id,
+ type,
+ controller.applications().clusterEndpoints(id, zones),
+ controller.applications().contentClustersByZone(id, zones)));
}
private static DeploymentJobs.JobReport toJobReport(String tenantName, String applicationName, Inspector report) {
@@ -1366,18 +1408,10 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
case cloud: {
CloudTenant cloudTenant = (CloudTenant) tenant;
- Cursor pemDeployKeysArray = object.setArray("pemDeployKeys");
- for (Application application : applications)
- for (String key : application.pemDeployKeys()) {
- Cursor keyObject = pemDeployKeysArray.addObject();
- keyObject.setString("key", key);
- keyObject.setString("application", application.id().application().value());
- }
-
Cursor pemDeveloperKeysArray = object.setArray("pemDeveloperKeys");
- cloudTenant.pemDeveloperKeys().forEach((key, user) -> {
+ cloudTenant.developerKeys().forEach((key, user) -> {
Cursor keyObject = pemDeveloperKeysArray.addObject();
- keyObject.setString("key", key);
+ keyObject.setString("key", KeyUtils.toPem(key));
keyObject.setString("user", user.getName());
});
@@ -1470,6 +1504,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return Joiner.on("/").join(elements);
}
+ private void toSlime(TenantAndApplicationId id, Cursor object, HttpRequest request) {
+ object.setString("tenant", id.tenant().value());
+ object.setString("application", id.application().value());
+ object.setString("url", withPath("/application/v4" +
+ "/tenant/" + id.tenant().value() +
+ "/application/" + id.application().value(),
+ request.getUri()).toString());
+ }
+
private void toSlime(ApplicationId id, Cursor object, HttpRequest request) {
object.setString("tenant", id.tenant().value());
object.setString("application", id.application().value());
@@ -1639,6 +1682,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
long projectId = Math.max(1, submitOptions.field("projectId").asLong());
ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get(EnvironmentResource.APPLICATION_ZIP));
+ if (DeploymentSpec.empty.equals(applicationPackage.deploymentSpec()))
+ throw new IllegalArgumentException("Missing required file 'deployment.xml'");
+
controller.applications().verifyApplicationIdentityConfiguration(TenantName.from(tenant),
applicationPackage,
Optional.of(requireUserPrincipal(request)));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
index ab92e38ee4b..49015f16cce 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
@@ -64,6 +64,9 @@ import static java.util.stream.Collectors.toMap;
*
* @see JobController
* @see ApplicationApiHandler
+ *
+ * @author smorgrav
+ * @author jonmv
*/
class JobControllerApiHandlerHelper {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
index 86310ca2f6b..2adf6ce95e1 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
@@ -1,4 +1,4 @@
-// Copyright 2018 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.vespa.hosted.controller.restapi.deployment;
import com.yahoo.component.Version;
@@ -91,7 +91,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
versionObject.setBool("systemVersion", version.isSystemVersion());
Cursor configServerArray = versionObject.setArray("configServers");
- for (HostName hostname : version.systemApplicationHostnames()) {
+ for (HostName hostname : version.nodeVersions().hostnames()) {
Cursor configServerObject = configServerArray.addObject();
configServerObject.setString("hostname", hostname.value());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
index 6755110bb49..7ad2e03ef1d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
@@ -10,19 +10,27 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
import com.yahoo.log.LogLevel;
+import com.yahoo.security.KeyUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.yolean.Exceptions;
import java.security.Principal;
+import java.security.PublicKey;
+import java.util.Base64;
+import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import java.util.logging.Logger;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
/**
* Assigns the {@link Role#buildService(TenantName, ApplicationName)} role to requests with a
* Authorization header signature matching the public key of the indicated application.
@@ -46,25 +54,11 @@ public class SignatureFilter extends JsonSecurityRequestFilterBase {
if ( request.getAttribute(SecurityContext.ATTRIBUTE_NAME) == null
&& request.getHeader("X-Authorization") != null)
try {
- ApplicationId id = ApplicationId.fromSerializedForm(request.getHeader("X-Key-Id"));
- boolean verified = controller.applications().getApplication(TenantAndApplicationId.from(id)).stream()
- .flatMap(application -> application.pemDeployKeys().stream())
- .map(key -> new RequestVerifier(key, controller.clock()))
- .anyMatch(verifier -> verifier.verify(Method.valueOf(request.getMethod()),
- request.getUri(),
- request.getHeader("X-Timestamp"),
- request.getHeader("X-Content-Hash"),
- request.getHeader("X-Authorization")));
-
- if (verified) {
- Principal principal = new SimplePrincipal("buildService@" + id.tenant() + "." + id.application());
- request.setUserPrincipal(principal);
- request.setRemoteUser(principal.getName());
- request.setAttribute(SecurityContext.ATTRIBUTE_NAME,
- new SecurityContext(principal,
- Set.of(Role.buildService(id.tenant(), id.application()),
- Role.applicationDeveloper(id.tenant(), id.application()))));
- }
+ getSecurityContext(request).ifPresent(securityContext -> {
+ request.setUserPrincipal(securityContext.principal());
+ request.setRemoteUser(securityContext.principal().getName());
+ request.setAttribute(SecurityContext.ATTRIBUTE_NAME, securityContext);
+ });
}
catch (Exception e) {
logger.log(LogLevel.DEBUG, () -> "Exception verifying signed request: " + Exceptions.toMessageString(e));
@@ -72,4 +66,48 @@ public class SignatureFilter extends JsonSecurityRequestFilterBase {
return Optional.empty();
}
+ // TODO jonmv: Remove after October 2019.
+ private boolean anyDeployKeyMatches(TenantAndApplicationId id, DiscFilterRequest request) {
+ return controller.applications().getApplication(id).stream()
+ .map(Application::deployKeys)
+ .flatMap(Set::stream)
+ .anyMatch(key -> keyVerifies(key, request));
+ }
+
+ private boolean keyVerifies(PublicKey key, DiscFilterRequest request) {
+ return new RequestVerifier(key, controller.clock()).verify(Method.valueOf(request.getMethod()),
+ request.getUri(),
+ request.getHeader("X-Timestamp"),
+ request.getHeader("X-Content-Hash"),
+ request.getHeader("X-Authorization"));
+ }
+
+ private Optional<SecurityContext> getSecurityContext(DiscFilterRequest request) {
+ ApplicationId id = ApplicationId.fromSerializedForm(request.getHeader("X-Key-Id"));
+ if (request.getHeader("X-Key") != null) { // TODO jonmv: Remove check and else branch after Oct 2019.
+ PublicKey key = KeyUtils.fromPemEncodedPublicKey(new String(Base64.getDecoder().decode(request.getHeader("X-Key")), UTF_8));
+ if (keyVerifies(key, request)) {
+ Optional<CloudTenant> tenant = controller.tenants().get(id.tenant())
+ .filter(CloudTenant.class::isInstance)
+ .map(CloudTenant.class::cast);
+ if (tenant.isPresent() && tenant.get().developerKeys().containsKey(key))
+ return Optional.of(new SecurityContext(tenant.get().developerKeys().get(key),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant()))));
+
+ Optional <Application> application = controller.applications().getApplication(TenantAndApplicationId.from(id));
+ if (application.isPresent() && application.get().deployKeys().contains(key))
+ return Optional.of(new SecurityContext(new SimplePrincipal("headless@" + id.tenant() + "." + id.application()),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant())))); // TODO jonmv: Change to headless after Oct 10 2019.
+ }
+ }
+ else if (anyDeployKeyMatches(TenantAndApplicationId.from(id), request))
+ return Optional.of(new SecurityContext(new SimplePrincipal("headless@" + id.tenant() + "." + id.application()),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant()))));
+
+ return Optional.empty();
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index 807e74b7c75..77622df4c4a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -26,10 +26,9 @@ import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.yolean.Exceptions;
-import java.security.Principal;
+import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -200,9 +199,9 @@ public class UserApiHandler extends LoggingRequestHandler {
// TODO jonmv: Change to developer role, when this exists.
if (role.definition().equals(RoleDefinition.tenantOperator))
controller.tenants().lockIfPresent(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
- String key = tenant.get().pemDeveloperKeys().inverse().get(new SimplePrincipal(user.value()));
+ PublicKey key = tenant.get().developerKeys().inverse().get(new SimplePrincipal(user.value()));
if (key != null)
- controller.tenants().store(tenant.withoutPemDeveloperKey(key));
+ controller.tenants().store(tenant.withoutDeveloperKey(key));
});
users.removeUsers(role, List.of(user));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
index 77ccce873fe..66c87a8eefd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.security;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.util.List;
@@ -52,7 +53,7 @@ public interface AccessControl {
* @param id the ID of the application to create
* @param credentials the credentials for the entity requesting the creation
*/
- void createApplication(ApplicationId id, Credentials credentials);
+ void createApplication(TenantAndApplicationId id, Credentials credentials);
/**
* Deletes access control for the given tenant.
@@ -60,7 +61,7 @@ public interface AccessControl {
* @param id the ID of the application to delete
* @param credentials the credentials for the entity requesting the deletion
*/
- void deleteApplication(ApplicationId id, Credentials credentials);
+ void deleteApplication(TenantAndApplicationId id, Credentials credentials);
/**
* Returns the list of tenants to which a user has access.
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
index 7da3e43c9a5..a88e38e5f89 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
@@ -1,10 +1,8 @@
package com.yahoo.vespa.hosted.controller.security;
import com.google.inject.Inject;
-import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
@@ -12,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
import com.yahoo.vespa.hosted.controller.api.role.ApplicationRole;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
@@ -58,14 +57,14 @@ public class CloudAccessControl implements AccessControl {
}
@Override
- public void createApplication(ApplicationId id, Credentials credentials) {
+ public void createApplication(TenantAndApplicationId id, Credentials credentials) {
for (Role role : Roles.applicationRoles(id.tenant(), id.application()))
userManagement.createRole(role);
userManagement.addUsers(Role.applicationAdmin(id.tenant(), id.application()), List.of(new UserId(credentials.user().getName())));
}
@Override
- public void deleteApplication(ApplicationId id, Credentials credentials) {
+ public void deleteApplication(TenantAndApplicationId id, Credentials credentials) {
for (ApplicationRole role : Roles.applicationRoles(id.tenant(), id.application()))
userManagement.deleteRole(role);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
index 6ef9b5e6a4f..e230daf0c50 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
@@ -6,6 +6,7 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import java.security.Principal;
+import java.security.PublicKey;
import java.util.Objects;
import java.util.Optional;
@@ -17,13 +18,13 @@ import java.util.Optional;
public class CloudTenant extends Tenant {
private final BillingInfo billingInfo;
- private final BiMap<String, Principal> pemDeveloperKeys;
+ private final BiMap<PublicKey, Principal> developerKeys;
/** Public for the serialization layer — do not use! */
- public CloudTenant(TenantName name, BillingInfo info, BiMap<String, Principal> pemDeveloperKeys) {
+ public CloudTenant(TenantName name, BillingInfo info, BiMap<PublicKey, Principal> developerKeys) {
super(name, Optional.empty());
billingInfo = info;
- this.pemDeveloperKeys = pemDeveloperKeys;
+ this.developerKeys = developerKeys;
}
/** Creates a tenant with the given name, provided it passes validation. */
@@ -37,7 +38,7 @@ public class CloudTenant extends Tenant {
public BillingInfo billingInfo() { return billingInfo; }
/** Returns the set of developer keys and their corresponding developers for this tenant. */
- public BiMap<String, Principal> pemDeveloperKeys() { return pemDeveloperKeys; }
+ public BiMap<PublicKey, Principal> developerKeys() { return developerKeys; }
@Override
public Type type() {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java
new file mode 100644
index 00000000000..0a690b90410
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java
@@ -0,0 +1,93 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.versions;
+
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.HostName;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * Version information for a node allocated to a {@link com.yahoo.vespa.hosted.controller.application.SystemApplication}.
+ *
+ * This is immutable.
+ *
+ * @author mpolden
+ */
+public class NodeVersion {
+
+ private final HostName hostname;
+ private final Version currentVersion;
+ private final Version wantedVersion;
+ private final Instant changedAt;
+
+ public NodeVersion(HostName hostname, Version currentVersion, Version wantedVersion, Instant changedAt) {
+ this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null");
+ this.currentVersion = Objects.requireNonNull(currentVersion, "version must be non-null");
+ this.wantedVersion = Objects.requireNonNull(wantedVersion, "wantedVersion must be non-null");
+ this.changedAt = Objects.requireNonNull(changedAt, "changedAt must be non-null");
+ }
+
+ /** Hostname of this */
+ public HostName hostname() {
+ return hostname;
+ }
+
+ /** Current version of this */
+ public Version currentVersion() {
+ return currentVersion;
+ }
+
+ /** Wanted version of this */
+ public Version wantedVersion() {
+ return wantedVersion;
+ }
+
+ /** Returns whether this is changing (upgrading or downgrading) */
+ public boolean changing() {
+ return !currentVersion.equals(wantedVersion);
+ }
+
+ /** The most recent time the version of this changed */
+ public Instant changedAt() {
+ return changedAt;
+ }
+
+ /** Returns a copy of this with current version set to given version */
+ public NodeVersion withCurrentVersion(Version version, Instant changedAt) {
+ if (currentVersion.equals(version)) return this;
+ return new NodeVersion(hostname, version, wantedVersion, changedAt);
+ }
+
+ /** Returns a copy of this with wanted version set to given version */
+ public NodeVersion withWantedVersion(Version version) {
+ if (wantedVersion.equals(version)) return this;
+ return new NodeVersion(hostname, currentVersion, version, changedAt);
+ }
+
+ @Override
+ public String toString() {
+ return hostname + ": " + currentVersion + " -> " + wantedVersion + " [changedAt=" + changedAt + "]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ NodeVersion that = (NodeVersion) o;
+ return hostname.equals(that.hostname) &&
+ currentVersion.equals(that.currentVersion) &&
+ wantedVersion.equals(that.wantedVersion) &&
+ changedAt.equals(that.changedAt);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hostname, currentVersion, wantedVersion, changedAt);
+ }
+
+ public static NodeVersion empty(HostName hostname) {
+ return new NodeVersion(hostname, Version.emptyVersion, Version.emptyVersion, Instant.EPOCH);
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersions.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersions.java
new file mode 100644
index 00000000000..3ab96e03bcd
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersions.java
@@ -0,0 +1,97 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.versions;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import com.yahoo.component.Version;
+import com.yahoo.config.provision.HostName;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * A filterable list of {@link NodeVersion}s. This is immutable.
+ *
+ * @author mpolden
+ */
+public class NodeVersions {
+
+ public static final NodeVersions EMPTY = new NodeVersions(ImmutableMap.of());
+
+ private final ImmutableMap<HostName, NodeVersion> nodeVersions;
+
+ public NodeVersions(ImmutableMap<HostName, NodeVersion> nodeVersions) {
+ this.nodeVersions = Objects.requireNonNull(nodeVersions);
+ }
+
+ public Map<HostName, NodeVersion> asMap() {
+ return nodeVersions;
+ }
+
+ /** Returns host names in this, grouped by version */
+ public ListMultimap<Version, HostName> asVersionMap() {
+ var versions = ImmutableListMultimap.<Version, HostName>builder();
+ for (var kv : nodeVersions.entrySet()) {
+ versions.put(kv.getValue().currentVersion(), kv.getKey());
+ }
+ return versions.build();
+ }
+
+ /** Returns host names in this */
+ public Set<HostName> hostnames() {
+ return nodeVersions.keySet();
+ }
+
+ /** Returns a copy of this containing only node versions of given version */
+ public NodeVersions matching(Version version) {
+ return filter(nodeVersion -> nodeVersion.currentVersion().equals(version));
+ }
+
+ /** Returns number of node versions in this */
+ public int size() {
+ return nodeVersions.size();
+ }
+
+ /** Returns a copy of this containing only the given node versions */
+ public NodeVersions with(List<NodeVersion> nodeVersions) {
+ var newNodeVersions = ImmutableMap.<HostName, NodeVersion>builder();
+ for (var nodeVersion : nodeVersions) {
+ var existing = this.nodeVersions.get(nodeVersion.hostname());
+ if (existing != null) {
+ newNodeVersions.put(nodeVersion.hostname(), existing.withCurrentVersion(nodeVersion.currentVersion(),
+ nodeVersion.changedAt())
+ .withWantedVersion(nodeVersion.wantedVersion()));
+ } else {
+ newNodeVersions.put(nodeVersion.hostname(), nodeVersion);
+ }
+ }
+ return new NodeVersions(newNodeVersions.build());
+ }
+
+ private NodeVersions filter(Predicate<NodeVersion> predicate) {
+ var newNodeVersions = ImmutableMap.<HostName, NodeVersion>builder();
+ for (var kv : nodeVersions.entrySet()) {
+ if (!predicate.test(kv.getValue())) continue;
+ newNodeVersions.put(kv.getKey(), kv.getValue());
+ }
+ return new NodeVersions(newNodeVersions.build());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ NodeVersions that = (NodeVersions) o;
+ return nodeVersions.equals(that.nodeVersions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nodeVersions);
+ }
+
+}
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 9dc6b86e4be..bb43ec20234 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,12 +6,10 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.yahoo.component.Version;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.log.LogLevel;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
+import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.JobList;
@@ -70,7 +68,8 @@ public class VersionStatus {
/** Returns whether the system is currently upgrading */
public boolean isUpgrading() {
return systemVersion().map(VespaVersion::versionNumber).orElse(Version.emptyVersion)
- .isBefore(controllerVersion().map(VespaVersion::versionNumber).orElse(Version.emptyVersion));
+ .isBefore(controllerVersion().map(VespaVersion::versionNumber)
+ .orElse(Version.emptyVersion));
}
/**
@@ -91,14 +90,14 @@ public class VersionStatus {
/** Create a full, updated version status. This is expensive and should be done infrequently */
public static VersionStatus compute(Controller controller) {
- ListMultimap<Version, HostName> systemApplicationVersions = findSystemApplicationVersions(controller);
- ListMultimap<ControllerVersion, HostName> controllerVersions = findControllerVersions(controller);
+ var systemApplicationVersions = findSystemApplicationVersions(controller);
+ var controllerVersions = findControllerVersions(controller);
- ListMultimap<Version, HostName> infrastructureVersions = ArrayListMultimap.create();
+ var infrastructureVersions = ArrayListMultimap.<Version, HostName>create();
for (var kv : controllerVersions.asMap().entrySet()) {
infrastructureVersions.putAll(kv.getKey().version(), kv.getValue());
}
- infrastructureVersions.putAll(systemApplicationVersions);
+ infrastructureVersions.putAll(systemApplicationVersions.asVersionMap());
// The controller version is the lowest controller version of all controllers
ControllerVersion controllerVersion = controllerVersions.keySet().stream()
@@ -138,7 +137,7 @@ public class VersionStatus {
controllerVersion,
systemVersion,
isReleased,
- systemApplicationVersions.get(statistics.version()),
+ systemApplicationVersions.matching(statistics.version()),
controller);
versions.add(vespaVersion);
} catch (IllegalArgumentException e) {
@@ -152,29 +151,32 @@ public class VersionStatus {
return new VersionStatus(versions);
}
- private static ListMultimap<Version, HostName> findSystemApplicationVersions(Controller controller) {
- ListMultimap<Version, HostName> versions = ArrayListMultimap.create();
- for (ZoneApi zone : controller.zoneRegistry().zones().controllerUpgraded().zones()) {
- for (SystemApplication application : SystemApplication.all()) {
- List<Node> eligibleForUpgradeApplicationNodes = controller.serviceRegistry().configServer().nodeRepository()
- .list(zone.getId(), application.id()).stream()
- .filter(SystemUpgrader::eligibleForUpgrade)
- .collect(Collectors.toList());
- if (eligibleForUpgradeApplicationNodes.isEmpty())
- continue;
-
- boolean configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty());
+ private static NodeVersions findSystemApplicationVersions(Controller controller) {
+ var nodeVersions = controller.versionStatus().systemVersion()
+ .map(VespaVersion::nodeVersions)
+ .orElse(NodeVersions.EMPTY);
+ var newNodeVersions = new ArrayList<NodeVersion>();
+ for (var zone : controller.zoneRegistry().zones().controllerUpgraded().zones()) {
+ for (var application : SystemApplication.all()) {
+ var nodes = controller.serviceRegistry().configServer().nodeRepository()
+ .list(zone.getId(), application.id()).stream()
+ .filter(SystemUpgrader::eligibleForUpgrade)
+ .collect(Collectors.toList());
+ if (nodes.isEmpty()) continue;
+ var configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty());
if (!configConverged) {
- log.log(LogLevel.WARNING, "Config for " + application.id() + " in " + zone.getId() + " has not converged");
+ log.log(LogLevel.WARNING, "Config for " + application.id() + " in " + zone.getId() +
+ " has not converged");
}
- for (Node node : eligibleForUpgradeApplicationNodes) {
+ var now = controller.clock().instant();
+ for (var node : nodes) {
// Only use current node version if config has converged
- Version nodeVersion = configConverged ? node.currentVersion() : controller.systemVersion();
- versions.put(nodeVersion, node.hostname());
+ Version version = configConverged ? node.currentVersion() : controller.systemVersion();
+ newNodeVersions.add(new NodeVersion(node.hostname(), version, node.wantedVersion(), now));
}
}
}
- return versions;
+ return nodeVersions.with(newNodeVersions);
}
private static ListMultimap<ControllerVersion, HostName> findControllerVersions(Controller controller) {
@@ -241,7 +243,7 @@ public class VersionStatus {
ControllerVersion controllerVersion,
Version systemVersion,
boolean isReleased,
- Collection<HostName> configServerHostnames,
+ NodeVersions nodeVersions,
Controller controller) {
var isSystemVersion = statistics.version().equals(systemVersion);
var isControllerVersion = statistics.version().equals(controllerVersion.version());
@@ -279,7 +281,7 @@ public class VersionStatus {
isControllerVersion,
isSystemVersion,
isReleased,
- configServerHostnames,
+ nodeVersions,
confidence);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
index dc0b2c12d5c..0d144913022 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
@@ -1,16 +1,12 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.versions;
-import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
-import com.yahoo.config.provision.HostName;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import java.time.Instant;
import java.time.ZoneOffset;
-import java.util.Collection;
-import java.util.Set;
import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
@@ -30,12 +26,12 @@ public class VespaVersion implements Comparable<VespaVersion> {
private final boolean isSystemVersion;
private final boolean isReleased;
private final DeploymentStatistics statistics;
- private final ImmutableSet<HostName> systemApplicationHostnames;
+ private final NodeVersions nodeVersions;
private final Confidence confidence;
public VespaVersion(DeploymentStatistics statistics, String releaseCommit, Instant committedAt,
boolean isControllerVersion, boolean isSystemVersion, boolean isReleased,
- Collection<HostName> systemApplicationHostnames,
+ NodeVersions nodeVersions,
Confidence confidence) {
this.statistics = statistics;
this.releaseCommit = releaseCommit;
@@ -43,7 +39,7 @@ public class VespaVersion implements Comparable<VespaVersion> {
this.isControllerVersion = isControllerVersion;
this.isSystemVersion = isSystemVersion;
this.isReleased = isReleased;
- this.systemApplicationHostnames = ImmutableSet.copyOf(systemApplicationHostnames);
+ this.nodeVersions = nodeVersions;
this.confidence = confidence;
}
@@ -108,9 +104,11 @@ public class VespaVersion implements Comparable<VespaVersion> {
/** Returns whether the artifacts of this release are available in the configured maven repository. */
public boolean isReleased() { return isReleased; }
- /** Returns the hosts allocated to system applications (across all zones) which are currently of this version */
- public Set<HostName> systemApplicationHostnames() { return systemApplicationHostnames; }
-
+ /** Returns the versions of nodes allocated to system applications (across all zones) */
+ public NodeVersions nodeVersions() {
+ return nodeVersions;
+ }
+
/** Returns the confidence we have in this versions suitability for production */
public Confidence confidence() { return confidence; }
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
index fab1ed2ab20..e3682a78b7d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
@@ -507,7 +507,7 @@ public class ControllerTest {
tester.deployAndNotify(tester.defaultInstance(app1.id()).id(), Optional.of(applicationPackage), true, systemTest);
tester.applications().deactivate(app1.id().defaultInstance(), ZoneId.from(Environment.test, RegionName.from("us-east-1")));
tester.applications().deactivate(app1.id().defaultInstance(), ZoneId.from(Environment.staging, RegionName.from("us-east-3")));
- tester.applications().deleteApplication(app1.id().tenant(), app1.id().application(), tester.controllerTester().credentialsFor(app1.id()));
+ tester.applications().deleteApplication(app1.id(), tester.controllerTester().credentialsFor(app1.id()));
try (RotationLock lock = tester.applications().rotationRepository().lock()) {
assertTrue("Rotation is unassigned",
tester.applications().rotationRepository().availableRotations(lock)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index cefdc3bed61..2c88d122e8f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -233,11 +233,12 @@ public final class ControllerTester {
}
public Application createApplication(TenantName tenant, String applicationName, String instanceName, long projectId) {
- ApplicationId applicationId = ApplicationId.from(tenant.value(), applicationName, instanceName);
- controller().applications().createApplication(applicationId, credentialsFor(TenantAndApplicationId.from(applicationId)));
- controller().applications().lockApplicationOrThrow(TenantAndApplicationId.from(applicationId), application ->
+ TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenant.value(), applicationName);
+ controller().applications().createApplication(applicationId, credentialsFor(applicationId));
+ controller().applications().lockApplicationOrThrow(applicationId, application ->
controller().applications().store(application.withProjectId(OptionalLong.of(projectId))));
- Application application = controller().applications().requireApplication(TenantAndApplicationId.from(applicationId));
+ controller().applications().createInstance(applicationId.instance(instanceName));
+ Application application = controller().applications().requireApplication(applicationId);
assertTrue(application.projectId().isPresent());
return application;
}
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 5dc6fb183a2..61b393efbff 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
@@ -148,8 +148,13 @@ public class DeploymentTester {
/** Upgrade system applications in all zones to given version */
public void upgradeSystemApplications(Version version) {
+ upgradeSystemApplications(version, SystemApplication.all());
+ }
+
+ /** Upgrade given system applications in all zones to version */
+ public void upgradeSystemApplications(Version version, List<SystemApplication> systemApplications) {
for (ZoneApi zone : tester.zoneRegistry().zones().all().zones()) {
- for (SystemApplication application : SystemApplication.all()) {
+ for (SystemApplication application : systemApplications) {
tester.configServer().setVersion(application.id(), zone.getId(), version);
tester.configServer().convergeServices(application.id(), zone.getId());
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
index 4a7ee8bcb63..6da77a967f1 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
@@ -1,4 +1,4 @@
-// Copyright 2018 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.vespa.hosted.controller.integration;
import com.google.inject.Inject;
@@ -110,7 +110,8 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
List<Node> nodes = IntStream.rangeClosed(1, 3)
.mapToObj(i -> new Node(
HostName.from("node-" + i + "-" + application.id().application()
- .value()),
+ .value()
+ + "-" + zone.value()),
Node.State.active, application.nodeType(),
Optional.of(application.id()),
initialVersion,
@@ -150,9 +151,16 @@ public class ConfigServerMock extends AbstractComponent implements ConfigServer
/** Set version for an application in a given zone */
public void setVersion(ApplicationId application, ZoneId zone, Version version) {
+ setVersion(application, zone, version, -1);
+ }
+
+ /** Set version for nodeCount number of nodes in application in a given zone */
+ public void setVersion(ApplicationId application, ZoneId zone, Version version, int nodeCount) {
+ int n = 0;
for (Node node : nodeRepository().list(zone, application)) {
nodeRepository().putByHostname(zone, new Node(node.hostname(), node.state(), node.type(), node.owner(),
version, version));
+ if (++n == nodeCount) break;
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
index ff245e2e488..c6bd4bde410 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
@@ -218,7 +218,7 @@ public class JobRunnerTest {
// Thread is still trying to deploy tester -- delete application, and see all data is garbage collected.
assertEquals(Collections.singletonList(runId), jobs.active().stream().map(run -> run.id()).collect(Collectors.toList()));
- tester.controllerTester().controller().applications().deleteApplication(id.tenant(), id.application(), tester.controllerTester().credentialsFor(TenantAndApplicationId.from(id)));
+ tester.controllerTester().controller().applications().deleteApplication(TenantAndApplicationId.from(id), tester.controllerTester().credentialsFor(TenantAndApplicationId.from(id)));
assertEquals(Collections.emptyList(), jobs.active());
assertEquals(runId, jobs.last(id, systemTest).get().id());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
index 4fc952b0b15..9cb40d60677 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
@@ -1,9 +1,10 @@
-// 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.vespa.hosted.controller.maintenance;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.zone.UpgradePolicy;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -11,14 +12,17 @@ import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.deployment.InternalDeploymentTester;
import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
+import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import org.junit.Test;
import java.time.Duration;
+import java.util.List;
import java.util.Optional;
import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.component;
@@ -213,6 +217,51 @@ public class MetricsReporterTest {
assertEquals("Queue consumed", 0, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue());
}
+ @Test
+ public void test_nodes_failing_system_upgrade() {
+ var tester = new DeploymentTester();
+ var reporter = createReporter(tester.controller());
+ var zone1 = ZoneApiMock.fromId("prod.eu-west-1");
+ tester.controllerTester().zoneRegistry().setUpgradePolicy(UpgradePolicy.create().upgrade(zone1));
+ var systemUpgrader = new SystemUpgrader(tester.controller(), Duration.ofDays(1),
+ new JobControl(tester.controllerTester().curator()));
+ tester.configServer().bootstrap(List.of(zone1.getId()), SystemApplication.configServer);
+
+ // System on initial version
+ var version0 = Version.fromString("7.0");
+ tester.upgradeSystem(version0);
+ reporter.maintain();
+ assertEquals(0, getNodesFailingUpgrade());
+
+ for (var version : List.of(Version.fromString("7.1"), Version.fromString("7.2"))) {
+ // System starts upgrading to next version
+ tester.upgradeController(version);
+ reporter.maintain();
+ assertEquals(0, getNodesFailingUpgrade());
+ systemUpgrader.maintain();
+
+ // 30 minutes pass and nothing happens
+ tester.clock().advance(Duration.ofMinutes(30));
+ tester.computeVersionStatus();
+ reporter.maintain();
+ assertEquals(0, getNodesFailingUpgrade());
+
+ // 1/3 nodes upgrade within timeout
+ tester.configServer().setVersion(SystemApplication.configServer.id(), zone1.getId(), version, 1);
+ tester.clock().advance(Duration.ofMinutes(30).plus(Duration.ofSeconds(1)));
+ tester.computeVersionStatus();
+ reporter.maintain();
+ assertEquals(2, getNodesFailingUpgrade());
+
+ // 3/3 nodes upgrade
+ tester.configServer().setVersion(SystemApplication.configServer.id(), zone1.getId(), version);
+ tester.computeVersionStatus();
+ reporter.maintain();
+ assertEquals(0, getNodesFailingUpgrade());
+ assertEquals(version, tester.controller().systemVersion());
+ }
+ }
+
private Duration getAverageDeploymentDuration(ApplicationId id) {
return Duration.ofSeconds(getMetric(MetricsReporter.DEPLOYMENT_AVERAGE_DURATION, id).longValue());
}
@@ -225,6 +274,10 @@ public class MetricsReporterTest {
return getMetric(MetricsReporter.DEPLOYMENT_WARNINGS, id).intValue();
}
+ private int getNodesFailingUpgrade() {
+ return metrics.getMetric(MetricsReporter.NODES_FAILING_SYSTEM_UPGRADE).intValue();
+ }
+
private Number getMetric(String name, ApplicationId id) {
return metrics.getMetric((dimensions) -> id.tenant().value().equals(dimensions.get("tenant")) &&
appDimension(id).equals(dimensions.get("app")),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java
index 9677df6fd18..72b26aca588 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java
@@ -1,10 +1,9 @@
-// Copyright 2018 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.vespa.hosted.controller.maintenance;
import com.yahoo.component.Version;
import com.yahoo.config.provision.zone.UpgradePolicy;
import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
@@ -59,6 +58,7 @@ public class SystemUpgraderTest {
systemUpgrader.maintain();
assertCurrentVersion(SystemApplication.configServer, version1, zone1, zone2, zone3, zone4);
assertCurrentVersion(SystemApplication.proxy, version1, zone1, zone2, zone3, zone4);
+ assertSystemVersion(version1);
// Controller upgrades
Version version2 = Version.fromString("6.6");
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 3ba1181f762..08963b9fec7 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
@@ -1,13 +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.vespa.hosted.controller.persistence;
-import com.google.common.collect.ImmutableBiMap;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.security.KeyUtils;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Instance;
@@ -16,7 +16,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
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.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -37,6 +36,7 @@ import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.security.PublicKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -48,7 +48,6 @@ import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.stream.Collectors;
import static com.yahoo.config.provision.SystemName.main;
import static java.util.Optional.empty;
@@ -64,6 +63,15 @@ public class ApplicationSerializerTest {
private static final Path testData = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/");
private static final ZoneId zone1 = ZoneId.from("prod", "us-west-1");
private static final ZoneId zone2 = ZoneId.from("prod", "us-east-3");
+ private static final PublicKey publicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
+ "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
+ "-----END PUBLIC KEY-----\n");
+ private static final PublicKey otherPublicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
+ "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
+ "-----END PUBLIC KEY-----\n");
+
@Test
public void testSerialization() {
@@ -134,7 +142,7 @@ public class ApplicationSerializerTest {
Optional.of(User.from("by-username")),
OptionalInt.of(7),
new ApplicationMetrics(0.5, 0.9),
- Set.of("-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n\n-----END PUBLIC KEY-----", "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"),
+ Set.of(publicKey, otherPublicKey),
projectId,
true,
instances);
@@ -178,17 +186,14 @@ public class ApplicationSerializerTest {
assertEquals(original.owner(), serialized.owner());
assertEquals(original.majorVersion(), serialized.majorVersion());
assertEquals(original.change(), serialized.change());
- assertEquals(original.pemDeployKeys(), serialized.pemDeployKeys());
+ assertEquals(original.deployKeys(), serialized.deployKeys());
assertEquals(original.require(id1.instance()).rotations(), serialized.require(id1.instance()).rotations());
assertEquals(original.require(id1.instance()).rotationStatus(), serialized.require(id1.instance()).rotationStatus());
// Test cluster utilization
assertEquals(0, serialized.require(id1.instance()).deployments().get(zone1).clusterUtils().size());
- assertEquals(3, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().size());
- assertEquals(0.4, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id2")).getCpu(), 0.01);
- assertEquals(0.2, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id1")).getCpu(), 0.01);
- assertEquals(0.2, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().get(ClusterSpec.Id.from("id1")).getMemory(), 0.01);
+ assertEquals(0, serialized.require(id1.instance()).deployments().get(zone2).clusterUtils().size());
// Test cluster info
assertEquals(3, serialized.require(id1.instance()).deployments().get(zone2).clusterInfo().size());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
index 51df0e4b08b..ff1c952c2a5 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.persistence;// Copyright 2018 Yahoo Ho
import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.security.KeyUtils;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
@@ -15,6 +16,7 @@ import com.yahoo.vespa.hosted.controller.tenant.UserTenant;
import org.junit.Test;
import java.net.URI;
+import java.security.PublicKey;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -29,6 +31,14 @@ import static org.junit.Assert.assertTrue;
public class TenantSerializerTest {
private static final TenantSerializer serializer = new TenantSerializer();
+ private static final PublicKey publicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
+ "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
+ "-----END PUBLIC KEY-----\n");
+ private static final PublicKey otherPublicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
+ "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
+ "-----END PUBLIC KEY-----\n");
@Test
public void athenz_tenant() {
@@ -78,12 +88,12 @@ public class TenantSerializerTest {
public void cloud_tenant() {
CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
new BillingInfo("old cat lady", "vespa"),
- ImmutableBiMap.of("-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n\n-----END PUBLIC KEY-----", new SimplePrincipal("joe"),
- "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----", new SimplePrincipal("jane")));
+ ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
+ otherPublicKey, new SimplePrincipal("jane")));
CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
assertEquals(tenant.name(), serialized.name());
assertEquals(tenant.billingInfo(), serialized.billingInfo());
- assertEquals(tenant.pemDeveloperKeys(), serialized.pemDeveloperKeys());
+ assertEquals(tenant.developerKeys(), serialized.developerKeys());
}
private Contact contact() {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
index a1e22b4fc64..5d65cf0381e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
@@ -1,20 +1,23 @@
-// Copyright 2018 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.vespa.hosted.controller.persistence;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
+import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics;
+import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
+import com.yahoo.vespa.hosted.controller.versions.NodeVersions;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import org.junit.Test;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
import static java.time.temporal.ChronoUnit.MILLIS;
import static org.junit.Assert.assertEquals;
@@ -36,9 +39,11 @@ public class VersionStatusSerializerTest {
ApplicationId.from("tenant2", "success2", "default"))
);
vespaVersions.add(new VespaVersion(statistics, "dead", Instant.now(), false, false,
- true, asHostnames("cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal));
+ true, nodeVersions(Version.fromString("5.0"), Version.fromString("5.1"),
+ Instant.ofEpochMilli(123), "cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal));
vespaVersions.add(new VespaVersion(statistics, "cafe", Instant.now(), true, true,
- false, asHostnames("cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal));
+ false, nodeVersions(Version.fromString("5.0"), Version.fromString("5.1"),
+ Instant.ofEpochMilli(456), "cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal));
VersionStatus status = new VersionStatus(vespaVersions);
VersionStatusSerializer serializer = new VersionStatusSerializer();
VersionStatus deserialized = serializer.fromSlime(serializer.toSlime(status));
@@ -53,14 +58,48 @@ public class VersionStatusSerializerTest {
assertEquals(a.isSystemVersion(), b.isSystemVersion());
assertEquals(a.isReleased(), b.isReleased());
assertEquals(a.statistics(), b.statistics());
- assertEquals(a.systemApplicationHostnames(), b.systemApplicationHostnames());
+ assertEquals(a.nodeVersions(), b.nodeVersions());
assertEquals(a.confidence(), b.confidence());
}
}
- private static List<HostName> asHostnames(String... hostname) {
- return Arrays.stream(hostname).map(HostName::from).collect(Collectors.toList());
+ @Test
+ public void testLegacySerialization() throws Exception {
+ var data = Files.readAllBytes(Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json"));
+ var serializer = new VersionStatusSerializer();
+ var deserializedStatus = serializer.fromSlime(SlimeUtils.jsonToSlime(data));
+
+ var statistics = new DeploymentStatistics(
+ Version.fromString("7.0"),
+ List.of(),
+ List.of(),
+ List.of()
+ );
+ var vespaVersion = new VespaVersion(statistics, "badc0ffee",
+ Instant.ofEpochMilli(123), true,
+ true, true,
+ nodeVersions(Version.emptyVersion, Version.emptyVersion,
+ Instant.EPOCH, "cfg1", "cfg2", "cfg3"),
+ VespaVersion.Confidence.normal);
+
+ VespaVersion deserialized = deserializedStatus.versions().get(0);
+ assertEquals(vespaVersion.releaseCommit(), deserialized.releaseCommit());
+ assertEquals(vespaVersion.committedAt().truncatedTo(MILLIS), deserialized.committedAt());
+ assertEquals(vespaVersion.isControllerVersion(), deserialized.isControllerVersion());
+ assertEquals(vespaVersion.isSystemVersion(), deserialized.isSystemVersion());
+ assertEquals(vespaVersion.isReleased(), deserialized.isReleased());
+ assertEquals(vespaVersion.statistics(), deserialized.statistics());
+ assertEquals(vespaVersion.nodeVersions(), deserialized.nodeVersions());
+ assertEquals(vespaVersion.confidence(), deserialized.confidence());
+ }
+
+ private static NodeVersions nodeVersions(Version version, Version wantedVersion, Instant changedAt, String... hostnames) {
+ var nodeVersions = new ArrayList<NodeVersion>();
+ for (var hostname : hostnames) {
+ nodeVersions.add(new NodeVersion(HostName.from(hostname), version, wantedVersion, changedAt));
+ }
+ return NodeVersions.EMPTY.with(nodeVersions);
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
index 8ab277a3795..1c660726d61 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
@@ -17,11 +17,11 @@
"queryQuality": 100,
"writeQuality": 99.99894341115082,
"pemDeployKeys": [
- "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"
+ "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----"
],
"pemDeveloperKeys": [
{
- "key": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----",
"user": "joe@dev"
}
],
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json
new file mode 100644
index 00000000000..96ca22e1c1a
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/version-status-legacy-format.json
@@ -0,0 +1,23 @@
+{
+ "versions": [
+ {
+ "releaseCommit": "badc0ffee",
+ "releasedAt": 123,
+ "isCurrentControllerVersion": true,
+ "isCurrentSystemVersion": true,
+ "isReleased": true,
+ "deploymentStatistics": {
+ "version": "7.0",
+ "failing": [],
+ "production": [],
+ "deploying": []
+ },
+ "confidence": "normal",
+ "configServerHostnames": [
+ "cfg1",
+ "cfg2",
+ "cfg3"
+ ]
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
index 2d8c937097a..80e52f373d7 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
@@ -79,8 +79,10 @@ public class ContainerControllerTester {
Optional.of(new PropertyId("1234")));
controller().tenants().create(tenantSpec, credentials);
- ApplicationId app = ApplicationId.from(tenant, application, instance);
- return controller().applications().createApplication(app, Optional.of(credentials));
+ TenantAndApplicationId id = TenantAndApplicationId.from(tenant, application);
+ controller().applications().createApplication(id, Optional.of(credentials));
+ controller().applications().createInstance(id.instance(instance));
+ return controller().applications().requireApplication(id);
}
public void deploy(ApplicationId id, ApplicationPackage applicationPackage, ZoneId zone) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index 3ff21bb2261..307496ace5a 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -29,6 +29,9 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
@@ -49,11 +52,8 @@ 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.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.BuildJob;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
@@ -111,6 +111,11 @@ import static org.junit.Assert.assertTrue;
public class ApplicationApiTest extends ControllerContainerTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/";
+ private static final String pemPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
+ "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
+ "-----END PUBLIC KEY-----\n";
+ private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n");
private static final ApplicationPackage applicationPackageDefault = new ApplicationPackageBuilder()
.instances("default")
@@ -320,7 +325,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
.region("us-west-1")
.build();
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", POST)
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", POST)
.userIdentity(USER_ID)
.oktaAccessToken(OKTA_AT),
new File("application-reference-2.json"));
@@ -360,14 +365,14 @@ public class ApplicationApiTest extends ControllerContainerTest {
// POST a pem deploy key
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", POST)
.userIdentity(USER_ID)
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Added deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+ .data("{\"key\":\"" + pemPublicKey + "\"}"),
+ "{\"keys\":[\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\\n-----END PUBLIC KEY-----\\n\"]}");
// PATCH in a pem deploy key at deprecated path
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", PATCH)
.userIdentity(USER_ID)
- .data("{\"pemDeployKey\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Added deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+ .data("{\"pemDeployKey\":\"" + pemPublicKey + "\"}"),
+ "{\"message\":\"Added deploy key " + quotedPemPublicKey + "\"}");
// GET an application with a major version override
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
@@ -383,8 +388,8 @@ public class ApplicationApiTest extends ControllerContainerTest {
// DELETE the pem deploy key
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", DELETE)
.userIdentity(USER_ID)
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Removed deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+ .data("{\"key\":\"" + pemPublicKey + "\"}"),
+ "{\"keys\":[]}");
tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
.userIdentity(USER_ID),
@@ -556,6 +561,11 @@ public class ApplicationApiTest extends ControllerContainerTest {
.oktaAccessToken(OKTA_AT),
new File("delete-with-active-deployments.json"), 400);
+ // GET test-config for local tests against a prod deployment
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-central-1/test-config", GET)
+ .userIdentity(USER_ID),
+ new File("test-config.json"));
+
// DELETE (deactivate) a deployment - dev
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/instance1", DELETE)
.userIdentity(USER_ID),
@@ -1061,7 +1071,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
.oktaAccessToken(OKTA_AT)
.userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create 'tenant1.application1.instance1': Application already exists\"}",
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create 'tenant1.application1.instance1': Instance already exists\"}",
400);
ConfigServerMock configServer = serviceRegistry().configServerMock();
@@ -1111,7 +1121,7 @@ public class ApplicationApiTest extends ControllerContainerTest {
tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", DELETE)
.oktaAccessToken(OKTA_AT)
.userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete application 'tenant1.application1.instance1': Application not found\"}",
+ "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete instance 'tenant1.application1.instance1': Instance not found\"}",
404);
// DELETE tenant
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
index 331aabd32d0..9d76654fbc0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
@@ -80,8 +80,8 @@
"majorVersion": 7,
"globalRotations": [],
"instances": [],
- "pemDeployKey": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
- "pemDeployKeys": ["-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----"],
+ "pemDeployKey": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
+ "pemDeployKeys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n"],
"metrics": {
"queryServiceQuality": 0.0,
"writeServiceQuality": 0.0
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
index 25948e998f1..d62e39e42e7 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
@@ -28,40 +28,10 @@
"lastWritesPerSecond": 2.0
},
"cost": {
- "tco": 74,
+ "tco": 0,
"waste": 0,
- "utilization": 2.9999999999999996,
- "cluster": {
- "cluster1": {
- "count": 2,
- "resource": "cpu",
- "utilization": 2.9999999999999996,
- "tco": 74,
- "waste": 0,
- "flavor": "flavor1",
- "flavorCost":37.0,
- "flavorCpu":2.0,
- "flavorMem":4.0,
- "flavorDisk":50.0,
- "type": "content",
- "util": {
- "cpu": 2.9999999999999996,
- "mem": 0.4285714285714286,
- "disk": 0.5714285714285715,
- "diskBusy": 1.0
- },
- "usage": {
- "cpu": 0.6,
- "mem": 0.3,
- "disk": 0.4,
- "diskBusy": 0.3
- },
- "hostnames": [
- "host1",
- "host2"
- ]
- }
- }
+ "utilization": 0.0,
+ "cluster": {}
},
"metrics": {
"queriesPerSecond": 1.0,
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json
index 1a2025e4de2..c56a269b9d4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1.json
@@ -25,40 +25,10 @@
"lastWritesPerSecond": 2.0
},
"cost": {
- "tco": 74,
+ "tco": 0,
"waste": 0,
- "utilization": 2.9999999999999996,
- "cluster": {
- "cluster1": {
- "count": 2,
- "resource": "cpu",
- "utilization": 2.9999999999999996,
- "tco": 74,
- "waste": 0,
- "flavor": "flavor1",
- "flavorCost": 37.0,
- "flavorCpu": 2.0,
- "flavorMem": 4.0,
- "flavorDisk": 50.0,
- "type": "content",
- "util": {
- "cpu": 2.9999999999999996,
- "mem": 0.4285714285714286,
- "disk": 0.5714285714285715,
- "diskBusy": 1.0
- },
- "usage": {
- "cpu": 0.6,
- "mem": 0.3,
- "disk": 0.4,
- "diskBusy": 0.3
- },
- "hostnames": [
- "host1",
- "host2"
- ]
- }
- }
+ "utilization": 0.0,
+ "cluster": {}
},
"metrics": {
"queriesPerSecond": 1.0,
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json
index bb68904bee6..140be562fe9 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-us-central-1.json
@@ -37,40 +37,10 @@
"lastWritesPerSecond": 2.0
},
"cost": {
- "tco": 74,
+ "tco": 0,
"waste": 0,
- "utilization": 2.9999999999999996,
- "cluster": {
- "cluster1": {
- "count": 2,
- "resource": "cpu",
- "utilization": 2.9999999999999996,
- "tco": 74,
- "waste": 0,
- "flavor": "flavor1",
- "flavorCost": 37.0,
- "flavorCpu": 2.0,
- "flavorMem": 4.0,
- "flavorDisk": 50.0,
- "type": "content",
- "util": {
- "cpu": 2.9999999999999996,
- "mem": 0.4285714285714286,
- "disk": 0.5714285714285715,
- "diskBusy": 1.0
- },
- "usage": {
- "cpu": 0.6,
- "mem": 0.3,
- "disk": 0.4,
- "diskBusy": 0.3
- },
- "hostnames": [
- "host1",
- "host2"
- ]
- }
- }
+ "utilization": 0.0,
+ "cluster": {}
},
"metrics": {
"queriesPerSecond": 1.0,
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json
new file mode 100644
index 00000000000..2338543b019
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json
@@ -0,0 +1,20 @@
+{
+ "application": "tenant1:application1:instance1",
+ "zone": "prod.us-central-1",
+ "system": "main",
+ "endpoints": {
+ "prod.us-central-1": [
+ "http://old-endpoint.vespa.yahooapis.com:4080"
+ ]
+ },
+ "zoneEndpoints": {
+ "prod.us-central-1": {
+ "default": "http://old-endpoint.vespa.yahooapis.com:4080"
+ }
+ },
+ "clusters": {
+ "prod.us-central-1": [
+ "music"
+ ]
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java
index 084b235943e..0a4d046e318 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java
@@ -1,26 +1,26 @@
-// Copyright 2018 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.vespa.hosted.controller.restapi.deployment;
-import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
+import com.yahoo.vespa.hosted.controller.versions.NodeVersions;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import org.junit.Test;
import java.io.File;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
/**
* @author bratseth
@@ -69,16 +69,15 @@ public class DeploymentApiTest extends ControllerContainerTest {
private VersionStatus censorConfigServers(VersionStatus versionStatus, Controller controller) {
List<VespaVersion> censored = new ArrayList<>();
for (VespaVersion version : versionStatus.versions()) {
- if (!version.systemApplicationHostnames().isEmpty()) {
+ if (version.nodeVersions().size() > 0) {
version = new VespaVersion(version.statistics(),
version.releaseCommit(),
version.committedAt(),
version.isControllerVersion(),
version.isSystemVersion(),
version.isReleased(),
- ImmutableSet.of("config1.test", "config2.test").stream()
- .map(HostName::from)
- .collect(Collectors.toSet()),
+ NodeVersions.EMPTY.with(List.of(new NodeVersion(HostName.from("config1.test"), version.versionNumber(), version.versionNumber(), Instant.EPOCH),
+ new NodeVersion(HostName.from("config2.test"), version.versionNumber(), version.versionNumber(), Instant.EPOCH))),
VespaVersion.confidenceFrom(version.statistics(), controller)
);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
index 3b7d55f8cef..0a1e996696b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
@@ -3,15 +3,21 @@ package com.yahoo.vespa.hosted.controller.restapi.filter;
import ai.vespa.hosted.api.Method;
import ai.vespa.hosted.api.RequestSigner;
+import com.google.common.collect.ImmutableBiMap;
import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
+import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper;
+import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import org.junit.Before;
import org.junit.Test;
@@ -19,6 +25,8 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpRequest;
+import java.security.PrivateKey;
+import java.security.PublicKey;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -27,21 +35,21 @@ import static org.junit.Assert.assertTrue;
public class SignatureFilterTest {
- private static final String publicKey = "-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
- "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
- "-----END PUBLIC KEY-----\n";
+ private static final PublicKey publicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
+ "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
+ "-----END PUBLIC KEY-----\n");
- private static final String otherPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
- "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
- "-----END PUBLIC KEY-----\n";
+ private static final PublicKey otherPublicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
+ "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
+ "-----END PUBLIC KEY-----\n");
- private static final String privateKey = "-----BEGIN EC PRIVATE KEY-----\n" +
- "MHcCAQEEIJUmbIX8YFLHtpRgkwqDDE3igU9RG6JD9cYHWAZii9j7oAoGCCqGSM49\n" +
- "AwEHoUQDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9z/4jKSTHwbYR8wdsOSrJGVEU\n" +
- "PbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
- "-----END EC PRIVATE KEY-----\n";
+ private static final PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey("-----BEGIN EC PRIVATE KEY-----\n" +
+ "MHcCAQEEIJUmbIX8YFLHtpRgkwqDDE3igU9RG6JD9cYHWAZii9j7oAoGCCqGSM49\n" +
+ "AwEHoUQDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9z/4jKSTHwbYR8wdsOSrJGVEU\n" +
+ "PbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
+ "-----END EC PRIVATE KEY-----\n");
private static final TenantAndApplicationId appId = TenantAndApplicationId.from("my-tenant", "my-app");
private static final ApplicationId id = appId.defaultInstance();
@@ -58,10 +66,10 @@ public class SignatureFilterTest {
filter = new SignatureFilter(tester.controller());
signer = new RequestSigner(privateKey, id.serializedForm(), tester.clock());
- tester.createApplication(tester.createTenant(id.tenant().value(), "unused", 496L),
- id.application().value(),
- id.instance().value(),
- 28L);
+ tester.curator().writeTenant(new CloudTenant(appId.tenant(),
+ new BillingInfo("id", "code"),
+ ImmutableBiMap.of()));
+ tester.curator().writeApplication(new Application(appId, tester.clock().instant()));
}
@Test
@@ -69,42 +77,57 @@ public class SignatureFilterTest {
// Unsigned request gets no role.
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create("https://host:123/path/./..//..%2F?query=empty&%3F=%26"));
byte[] emptyBody = new byte[0];
- DiscFilterRequest unsigned = requestOf(request.method("GET", HttpRequest.BodyPublishers.ofByteArray(emptyBody)).build(), emptyBody);
- filter.filter(unsigned);
- assertNull(unsigned.getAttribute(SecurityContext.ATTRIBUTE_NAME));
+ verifySecurityContext(requestOf(request.copy().method("GET", HttpRequest.BodyPublishers.ofByteArray(emptyBody)).build(), emptyBody),
+ null);
// Signed request gets no role when no key is stored for the application.
- DiscFilterRequest signed = requestOf(signer.signed(request, Method.GET, InputStream::nullInputStream), emptyBody);
- filter.filter(signed);
- assertNull(signed.getAttribute(SecurityContext.ATTRIBUTE_NAME));
-
- // Signed request gets no role when a non-matching key is stored for the application.
- applications.lockApplicationOrThrow(appId, application -> applications.store(application.withPemDeployKey(otherPublicKey)));
- filter.filter(signed);
- assertNull(signed.getAttribute(SecurityContext.ATTRIBUTE_NAME));
-
- // Signed request gets a build service role when a matching key is stored for the application.
- applications.lockApplicationOrThrow(appId, application -> applications.store(application.withPemDeployKey(publicKey)));
- assertTrue(filter.filter(signed).isEmpty());
- SecurityContext securityContext = (SecurityContext) signed.getAttribute(SecurityContext.ATTRIBUTE_NAME);
- assertEquals("buildService@my-tenant.my-app", securityContext.principal().getName());
- assertEquals(Set.of(Role.buildService(id.tenant(), id.application()),
- Role.applicationDeveloper(id.tenant(), id.application())),
- securityContext.roles());
-
- // Signed POST request also gets a build service role.
+ verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
+ null);
+
+ // Signed request gets no role when only non-matching keys are stored for the application.
+ applications.lockApplicationOrThrow(appId, application -> applications.store(application.withDeployKey(otherPublicKey)));
+ // Signed request gets no role when no key is stored for the application.
+ verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
+ null);
+
+ // Signed request gets a headless role when a matching key is stored for the application.
+ applications.lockApplicationOrThrow(appId, application -> applications.store(application.withDeployKey(publicKey)));
+ verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
+ new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant())))); // TODO jonmv: Change to headless.
+
+ // TODO jonmv: remove after Oct 2019.
+ // Signed request gets a build service role when a matching key is stored for the application and no X-Key header is provided.
+ verifySecurityContext(requestOf(signer.legacySigned(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
+ new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant()))));
+
+ // Signed POST request with X-Key header gets a headless role.
byte[] hiBytes = new byte[]{0x48, 0x69};
- signed = requestOf(signer.signed(request, Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes);
- filter.filter(signed);
- securityContext = (SecurityContext) signed.getAttribute(SecurityContext.ATTRIBUTE_NAME);
- assertEquals("buildService@my-tenant.my-app", securityContext.principal().getName());
- assertEquals(Set.of(Role.buildService(id.tenant(), id.application()),
- Role.applicationDeveloper(id.tenant(), id.application())),
- securityContext.roles());
+ verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
+ new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant())))); // TODO jonmv: Change to headless.
+
+ // Signed request gets a developer role when a matching developer key is stored for the tenant.
+ tester.curator().writeTenant(new CloudTenant(appId.tenant(),
+ new BillingInfo("id", "code"),
+ ImmutableBiMap.of(publicKey, () -> "user")));
+ verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
+ new SecurityContext(new SimplePrincipal("user"),
+ Set.of(Role.reader(id.tenant()),
+ Role.developer(id.tenant()))));
// Unsigned requests still get no roles.
- filter.filter(unsigned);
- assertNull(unsigned.getAttribute(SecurityContext.ATTRIBUTE_NAME));
+ verifySecurityContext(requestOf(request.copy().method("GET", HttpRequest.BodyPublishers.ofByteArray(emptyBody)).build(), emptyBody),
+ null);
+ }
+
+ private void verifySecurityContext(DiscFilterRequest request, SecurityContext securityContext) {
+ assertTrue(filter.filter(request).isEmpty());
+ assertEquals(securityContext, request.getAttribute(SecurityContext.ATTRIBUTE_NAME));
}
private static DiscFilterRequest requestOf(HttpRequest request, byte[] body) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
index 17f90259fa8..01af1bd70dd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
@@ -6,92 +6,92 @@
"cloud": "cloud1",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-2-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-1-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-1-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-2-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-2-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-3-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-1-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-2-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-1-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-3-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-3-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-2-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-1-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-3-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-2-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-1-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
}
@@ -103,47 +103,47 @@
"cloud": "cloud2",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-1-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-2-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-1-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-3-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-2-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-1-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-3-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-2-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-initial.json
index 86bc272fcd1..dbaa6623fae 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-initial.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-initial.json
@@ -6,92 +6,92 @@
"cloud": "cloud1",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-2-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-1-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-1-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-2-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-2-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-3-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-1-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-2-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-1-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-3-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-3-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-2-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-1-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-3-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-2-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-1-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
}
@@ -103,47 +103,47 @@
"cloud": "cloud2",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-1-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-2-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-1-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-3-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-2-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-1-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-3-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-2-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
index e8007fbf6c5..2b907c1156c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
@@ -6,47 +6,47 @@
"cloud": "cloud1",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-1-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-2-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-2-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-1-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-3-proxy-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-3-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-2-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-1-tenant-host-prod.us-west-1",
"environment": "prod",
"region": "us-west-1"
}
@@ -58,47 +58,47 @@
"cloud": "cloud1",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-2-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-1-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-2-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-3-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-1-proxy-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-3-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-2-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-1-tenant-host-prod.us-east-3",
"environment": "prod",
"region": "us-east-3"
}
@@ -110,47 +110,47 @@
"cloud": "cloud2",
"nodes": [
{
- "hostname": "node-1-configserver-host",
+ "hostname": "node-1-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-configserver-host",
+ "hostname": "node-2-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-configserver-host",
+ "hostname": "node-3-configserver-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-proxy-host",
+ "hostname": "node-1-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-proxy-host",
+ "hostname": "node-3-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-1-proxy-host",
+ "hostname": "node-2-proxy-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-2-tenant-host",
+ "hostname": "node-1-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-1-tenant-host",
+ "hostname": "node-3-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
},
{
- "hostname": "node-3-tenant-host",
+ "hostname": "node-2-tenant-host-prod.eu-west-1",
"environment": "prod",
"region": "eu-west-1"
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index b17dda7f810..b1f5f33b960 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -12,7 +12,6 @@ import java.io.File;
import java.util.Set;
import static com.yahoo.application.container.handler.Request.Method.DELETE;
-import static com.yahoo.application.container.handler.Request.Method.PATCH;
import static com.yahoo.application.container.handler.Request.Method.POST;
import static com.yahoo.application.container.handler.Request.Method.PUT;
import static org.junit.Assert.assertEquals;
@@ -23,6 +22,17 @@ import static org.junit.Assert.assertEquals;
public class UserApiTest extends ControllerContainerCloudTest {
private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/";
+ private static final String pemPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
+ "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
+ "-----END PUBLIC KEY-----\n";
+ private static final String otherPemPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
+ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
+ "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
+ "-----END PUBLIC KEY-----\n";
+ private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n");
+ private static final String otherQuotedPemPublicKey = otherPemPublicKey.replaceAll("\\n", "\\\\n");
+
@Test
public void testUserManagement() {
@@ -132,30 +142,30 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a pem deploy key
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST)
.roles(Set.of(Role.tenantOperator(id.tenant())))
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Added deploy key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}");
+ .data("{\"key\":\"" + pemPublicKey + "\"}"),
+ new File("first-deploy-key.json"));
// POST a pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
.user("joe@dev")
.roles(Set.of(Role.tenantOperator(id.tenant())))
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Set developer key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY----- for joe@dev\"}");
+ .data("{\"key\":\"" + pemPublicKey + "\"}"),
+ new File("first-developer-key.json"));
// POST the same pem developer key for a different user is forbidden
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
.user("operator@tenant")
.roles(Set.of(Role.tenantOperator(id.tenant())))
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Multiple entries with same key: -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----=operator@tenant and -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----=joe@dev\"}",
+ .data("{\"key\":\"" + pemPublicKey + "\"}"),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key "+ quotedPemPublicKey + " is already owned by joe@dev\"}",
400);
// PATCH in a different pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
.user("operator@tenant")
.roles(Set.of(Role.tenantOperator(id.tenant())))
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Set developer key -----BEGIN PUBLIC KEY-----\\nƪ(`▿▿▿▿´ƪ)\\n-----END PUBLIC KEY----- for operator@tenant\"}");
+ .data("{\"key\":\"" + otherPemPublicKey + "\"}"),
+ new File("both-developer-keys.json"));
// GET tenant information with keys
tester.assertResponse(request("/application/v4/tenant/my-tenant/")
@@ -165,8 +175,8 @@ public class UserApiTest extends ControllerContainerCloudTest {
// DELETE a pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", DELETE)
.roles(Set.of(Role.tenantOperator(id.tenant())))
- .data("{\"key\":\"-----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY-----\"}"),
- "{\"message\":\"Removed developer key -----BEGIN PUBLIC KEY-----\\n∠( ᐛ 」∠)_\\n-----END PUBLIC KEY----- for joe@dev\"}");
+ .data("{\"key\":\"" + pemPublicKey + "\"}"),
+ new File("second-developer-key.json"));
// DELETE an application role is allowed for an application admin.
tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", DELETE)
@@ -180,8 +190,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
"{\"message\":\"Deleted application my-tenant.my-app\"}");
// DELETE a tenant role is available to tenant admins.
- // DELETE the tenantOperator role clears any developer key.
- // TODO jonmv: Change to developer, when this role exists.
+ // DELETE the developer role clears any developer key.
tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
.roles(Set.of(Role.tenantAdmin(id.tenant())))
.data("{\"user\":\"operator@tenant\",\"roleName\":\"tenantOperator\"}"),
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json
index 6cf4dc76173..31bdb07b26b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json
@@ -1,6 +1,5 @@
{
"tenant": "my-tenant",
"application": "my-app",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/my-tenant/application/my-app/instance/default"
+ "url": "http://localhost:8080/application/v4/tenant/my-tenant/application/my-app"
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
new file mode 100644
index 00000000000..2ff1c29fe29
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
@@ -0,0 +1,12 @@
+{
+ "keys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
+ "user": "joe@dev"
+ },
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
+ "user": "operator@tenant"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json
new file mode 100644
index 00000000000..1c86877b77d
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json
@@ -0,0 +1,5 @@
+{
+ "keys": [
+ "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n"
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json
new file mode 100644
index 00000000000..b7d48f283f3
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json
@@ -0,0 +1,9 @@
+{
+ "keys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
+ "user": "joe@dev"
+ }
+ ]
+}
+
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
new file mode 100644
index 00000000000..f7d90f31116
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
@@ -0,0 +1,8 @@
+{
+ "keys": [
+ {
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
+ "user": "operator@tenant"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
index 5aaa900c3f0..b7970a48963 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
@@ -1,27 +1,14 @@
{
"tenant": "my-tenant",
"type": "CLOUD",
- "pemDeployKeys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
- "application": "my-app"
- }
- ],
"pemDeveloperKeys": [
{
- "key": "-----BEGIN PUBLIC KEY-----\n∠( ᐛ 」∠)_\n-----END PUBLIC KEY-----",
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
"user": "joe@dev"
},
{
- "key": "-----BEGIN PUBLIC KEY-----\nƪ(`▿▿▿▿´ƪ)\n-----END PUBLIC KEY-----",
+ "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
"user": "operator@tenant"
}],
- "applications": [
- {
- "tenant":"my-tenant",
- "application":"my-app",
- "instance":"default",
- "url":"http://localhost:8080/application/v4/tenant/my-tenant/application/my-app/instance/default"
- }
- ]
+ "applications": []
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
index a89a0f5360c..39b6cccbab0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
@@ -1,7 +1,6 @@
{
"tenant": "my-tenant",
"type": "CLOUD",
- "pemDeployKeys": [],
"pemDeveloperKeys": [],
"applications": []
}
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 ba8309de286..83223b0e041 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
@@ -8,7 +8,6 @@ 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.Instance;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java b/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java
index 61893a30e7e..0ca1b3e5603 100644
--- a/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java
@@ -38,8 +38,8 @@ public class Properties {
return Paths.get(requireNonBlankProperty("privateKeyFile"));
}
- public static Path certificateFile() {
- return Paths.get(requireNonBlankProperty("certificateFile"));
+ public static Optional<Path> certificateFile() {
+ return getNonBlankProperty("certificateFile").map(Paths::get);
}
/** Returns the system property with the given name if it is set, or empty. */
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java
index b2fd16b7975..5d314d90356 100644
--- a/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestSigner.java
@@ -4,9 +4,10 @@ package ai.vespa.hosted.api;
import com.yahoo.security.KeyUtils;
import com.yahoo.security.SignatureUtils;
-import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.http.HttpRequest;
+import java.security.PrivateKey;
+import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.time.Clock;
@@ -15,6 +16,7 @@ import java.util.function.Supplier;
import static ai.vespa.hosted.api.Signatures.sha256Digest;
import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA;
+import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Signs HTTP request headers using a private key, for verification by the indicated public key.
@@ -25,6 +27,7 @@ public class RequestSigner {
private final Signature signer;
private final String keyId;
+ private final String base64PemPublicKey;
private final Clock clock;
/** Creates a new request signer from the given PEM encoded ECDSA key, with a public key with the given ID. */
@@ -34,8 +37,15 @@ public class RequestSigner {
/** Creates a new request signer with a custom clock. */
public RequestSigner(String pemPrivateKey, String keyId, Clock clock) {
- this.signer = SignatureUtils.createSigner(KeyUtils.fromPemEncodedPrivateKey(pemPrivateKey), SHA256_WITH_ECDSA);
+ this(KeyUtils.fromPemEncodedPrivateKey(pemPrivateKey), keyId, clock);
+ }
+
+ /** Creates a new request signer with a custom clock. */
+ public RequestSigner(PrivateKey privateKey, String keyId, Clock clock) {
+ this.signer = SignatureUtils.createSigner(privateKey, SHA256_WITH_ECDSA);
this.keyId = keyId;
+ this.base64PemPublicKey = Base64.getEncoder().encodeToString(KeyUtils.toPem(KeyUtils.extractPublicKey(privateKey)).getBytes(UTF_8));
+ PublicKey key = KeyUtils.extractPublicKey(privateKey);
this.clock = clock;
}
@@ -44,8 +54,8 @@ public class RequestSigner {
* <br>
* The request builder's method and data are set to the given arguments, and a hash of the
* content is computed and added to a header, together with other meta data, like the URI
- * of the request, the current UTC time, and the id of the public key which shall be used to
- * verify this signature.
+ * of the request, the current UTC time, and the id and value of the public key which shall
+ * be used to * verify this signature.
* Finally, a signature is computed from these fields, based on the private key of this, and
* added to the request as another header.
*/
@@ -60,6 +70,29 @@ public class RequestSigner {
request.setHeader("X-Timestamp", timestamp);
request.setHeader("X-Content-Hash", contentHash);
request.setHeader("X-Key-Id", keyId);
+ request.setHeader("X-Key", base64PemPublicKey);
+ request.setHeader("X-Authorization", signature);
+
+ request.method(method.name(), HttpRequest.BodyPublishers.ofInputStream(data));
+ return request.build();
+ }
+ catch (SignatureException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ // TODO jonmv: Simulates old clients — remove shortly (2 Oct 2019).
+ public HttpRequest legacySigned(HttpRequest.Builder request, Method method, Supplier<InputStream> data) {
+ try {
+ String timestamp = clock.instant().toString();
+ String contentHash = Base64.getEncoder().encodeToString(sha256Digest(data::get));
+ byte[] canonicalMessage = Signatures.canonicalMessageOf(method.name(), request.copy().build().uri(), timestamp, contentHash);
+ signer.update(canonicalMessage);
+ String signature = Base64.getEncoder().encodeToString(signer.sign());
+
+ request.setHeader("X-Timestamp", timestamp);
+ request.setHeader("X-Content-Hash", contentHash);
+ request.setHeader("X-Key-Id", keyId);
request.setHeader("X-Authorization", signature);
request.method(method.name(), HttpRequest.BodyPublishers.ofInputStream(data));
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java
index 9d85ec9bf6b..5a6bea54bce 100644
--- a/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/RequestVerifier.java
@@ -5,6 +5,7 @@ import com.yahoo.security.KeyUtils;
import com.yahoo.security.SignatureUtils;
import java.net.URI;
+import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.time.Clock;
@@ -31,7 +32,12 @@ public class RequestVerifier {
/** Creates a new request verifier from the given PEM encoded ECDSA public key, with the given clock. */
public RequestVerifier(String pemPublicKey, Clock clock) {
- this.verifier = SignatureUtils.createVerifier(KeyUtils.fromPemEncodedPublicKey(pemPublicKey), SHA256_WITH_ECDSA);
+ this(KeyUtils.fromPemEncodedPublicKey(pemPublicKey), clock);
+ }
+
+ /** Creates a new request verifier from the given PEM encoded ECDSA public key, with the given clock. */
+ public RequestVerifier(PublicKey publicKey, Clock clock) {
+ this.verifier = SignatureUtils.createVerifier(publicKey, SHA256_WITH_ECDSA);
this.clock = clock;
}
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java b/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java
index b8698eab15f..c1104c649f2 100644
--- a/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java
@@ -3,13 +3,16 @@ package ai.vespa.hosted.api;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.JsonDecoder;
import com.yahoo.slime.ObjectTraverser;
import com.yahoo.slime.Slime;
import java.net.URI;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -26,8 +29,10 @@ public class TestConfig {
private final ZoneId zone;
private final SystemName system;
private final Map<ZoneId, Map<String, URI>> deployments;
+ private final Map<ZoneId, List<String>> contentClusters;
- public TestConfig(ApplicationId application, ZoneId zone, SystemName system, Map<ZoneId, Map<String, URI>> deployments) {
+ public TestConfig(ApplicationId application, ZoneId zone, SystemName system, Map<ZoneId, Map<String, URI>> deployments,
+ Map<ZoneId, List<String>> contentClusters) {
if ( ! deployments.containsKey(zone))
throw new IllegalArgumentException("Config must contain a deployment for its zone, but only does for " + deployments.keySet());
this.application = requireNonNull(application);
@@ -36,12 +41,15 @@ public class TestConfig {
this.deployments = deployments.entrySet().stream()
.collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(),
entry -> Map.copyOf(entry.getValue())));
+ this.contentClusters = contentClusters.entrySet().stream()
+ .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(),
+ entry -> List.copyOf(entry.getValue())));
}
/**
* Parses the given test config JSON and returns a new config instance.
*
- * If the given JSON has a "clusters" element, a config object with default values
+ * If the given JSON has a "localEndpoints" element, a config object with default values
* is returned, using {@link #fromEndpointsOnly}. Otherwise, all config attributes are parsed.
*/
public static TestConfig fromJson(byte[] jsonBytes) {
@@ -56,7 +64,13 @@ public class TestConfig {
config.field("zoneEndpoints").traverse((ObjectTraverser) (zoneId, clustersObject) -> {
deployments.put(ZoneId.from(zoneId), toClusterMap(clustersObject));
});
- return new TestConfig(application, zone, system, deployments);
+ Map<ZoneId, List<String>> contentClusters = new HashMap<>();
+ config.field("clusters").traverse(((ObjectTraverser) (zoneId, clustersArray) -> {
+ List<String> clusters = new ArrayList<>();
+ clustersArray.traverse((ArrayTraverser) (__, cluster) -> clusters.add(cluster.asString()));
+ contentClusters.put(ZoneId.from(zoneId), clusters);
+ }));
+ return new TestConfig(application, zone, system, deployments, contentClusters);
}
static Map<String, URI> toClusterMap(Inspector clustersObject) {
@@ -73,7 +87,8 @@ public class TestConfig {
return new TestConfig(ApplicationId.defaultId(),
ZoneId.defaultId(),
SystemName.defaultSystem(),
- Map.of(ZoneId.defaultId(), endpoints));
+ Map.of(ZoneId.defaultId(), endpoints),
+ Map.of());
}
/** Returns the full id of the application to test. */
@@ -85,6 +100,9 @@ public class TestConfig {
/** Returns an immutable view of deployments, per zone, of the application to test. */
public Map<ZoneId, Map<String, URI>> deployments() { return deployments; }
+ /** Returns an immutable view of content clusters, per zone, of the application to test. */
+ public Map<ZoneId, List<String>> contentClusters() { return contentClusters; }
+
/** Returns the hosted Vespa system this is run against. */
public SystemName system() { return system; }
diff --git a/hosted-api/src/test/java/ai/vespa/hosted/api/TestConfigTest.java b/hosted-api/src/test/java/ai/vespa/hosted/api/TestConfigTest.java
index bad838f0579..5ed008cc2ec 100644
--- a/hosted-api/src/test/java/ai/vespa/hosted/api/TestConfigTest.java
+++ b/hosted-api/src/test/java/ai/vespa/hosted/api/TestConfigTest.java
@@ -9,6 +9,7 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
+import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
@@ -32,6 +33,9 @@ public class TestConfigTest {
ZoneId.from("prod", "aws-us-east-1a"),
Map.of("default", URI.create("https://prod.endpoint:443/"))),
config.deployments());
+ assertEquals(Map.of(ZoneId.from("prod", "aws-us-east-1c"),
+ List.of("documents")),
+ config.contentClusters());
}
@Test
diff --git a/hosted-api/src/test/resources/test-config.json b/hosted-api/src/test/resources/test-config.json
index 9d36f9496a0..bd337e1c28a 100644
--- a/hosted-api/src/test/resources/test-config.json
+++ b/hosted-api/src/test/resources/test-config.json
@@ -9,5 +9,10 @@
"prod.aws-us-east-1a": {
"default": "https://prod.endpoint:443/"
}
+ },
+ "clusters": {
+ "prod.aws-us-east-1c": [
+ "documents"
+ ]
}
}
diff --git a/jdisc_core/pom.xml b/jdisc_core/pom.xml
index ca035ec1e2e..0bedd1d1704 100644
--- a/jdisc_core/pom.xml
+++ b/jdisc_core/pom.xml
@@ -104,11 +104,6 @@
</exclusions>
</dependency>
<dependency>
- <groupId>commons-daemon</groupId>
- <artifactId>commons-daemon</artifactId>
- <scope>compile</scope>
- </dependency>
- <dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.framework</artifactId>
<scope>compile</scope>
@@ -243,7 +238,6 @@
<classpath />
<argument>com.yahoo.jdisc.core.ExportPackages</argument>
<argument>${exportPackagesFile}</argument>
- <argument>${project.build.directory}/dependency/commons-daemon.jar</argument>
<argument>__REPLACE_VERSION__${project.build.directory}/dependency/guava.jar</argument>
<argument>${project.build.directory}/dependency/guice-no_aop.jar</argument>
<argument>${project.build.directory}/dependency/guice-assistedinject.jar</argument>
diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java b/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java
deleted file mode 100644
index de6d5c5073f..00000000000
--- a/jdisc_core/src/main/java/com/yahoo/jdisc/core/BootstrapDaemon.java
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.jdisc.core;
-
-import com.yahoo.protect.Process;
-import org.apache.commons.daemon.Daemon;
-import org.apache.commons.daemon.DaemonContext;
-
-import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * @author Simon Thoresen Hult
- */
-public class BootstrapDaemon implements Daemon {
-
- private static final Logger log = Logger.getLogger(BootstrapDaemon.class.getName());
- private final BootstrapLoader loader;
- private final boolean privileged;
- private String bundleLocation;
-
- static {
- // force load slf4j to avoid other logging frameworks from initializing before it
- org.slf4j.LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
- }
-
- public BootstrapDaemon() {
- this(new ApplicationLoader(Main.newOsgiFramework(), Main.newConfigModule()),
- Boolean.valueOf(System.getProperty("jdisc.privileged")));
- }
-
- BootstrapDaemon(BootstrapLoader loader, boolean privileged) {
- this.loader = loader;
- this.privileged = privileged;
- }
-
- BootstrapLoader loader() {
- return loader;
- }
-
- private static class WatchDog implements Runnable {
- final String name;
- final CountDownLatch complete;
- final long timeout;
- final TimeUnit timeUnit;
- WatchDog(String name, CountDownLatch complete, long timeout, TimeUnit timeUnit) {
- this.name = name;
- this.complete = complete;
- this.timeout = timeout;
- this.timeUnit = timeUnit;
- }
- @Override
- public void run() {
- boolean dumpStack;
- try {
- dumpStack = !complete.await(timeout, timeUnit);
- } catch (InterruptedException e) {
- return;
- }
- if (dumpStack) {
- log.warning("The watchdog for BootstrapDaemon." + name + " detected that it had not completed in "
- + timeUnit.toMillis(timeout) + "ms. Dumping stack.");
- Process.dumpThreads();
- }
- }
- }
- private interface MyRunnable {
- void run() throws Exception;
- }
- private void startWithWatchDog(String name, long timeout, TimeUnit timeUnit, MyRunnable task) throws Exception {
- CountDownLatch complete = new CountDownLatch(1);
- Thread thread = new Thread(new WatchDog(name, complete, timeout, timeUnit), name);
- thread.setDaemon(true);
- thread.start();
- try {
- task.run();
- } catch (Exception e) {
- log.log(Level.WARNING, "Exception caught during BootstrapDaemon." + name, e);
- throw e;
- } catch (Error e) {
- log.log(Level.WARNING, "Error caught during BootstrapDaemon." + name, e);
- throw e;
- } catch (Throwable thrown) {
- log.log(Level.WARNING, "Throwable caught during BootstrapDaemon." + name, thrown);
- } finally {
- complete.countDown();
- thread.join();
- }
- }
-
- @Override
- public void init(DaemonContext context) throws Exception {
- String[] args = context.getArguments();
- if (args == null || args.length != 1 || args[0] == null) {
- throw new IllegalArgumentException("Expected 1 argument, got " + Arrays.toString(args) + ".");
- }
- bundleLocation = args[0];
- if (privileged) {
- log.finer("Initializing application with privileges.");
- startWithWatchDog("init", 60, TimeUnit.SECONDS, () -> loader.init(bundleLocation, true));
- }
- }
-
- @Override
- public void start() throws Exception {
- try {
- if (!privileged) {
- log.finer("Initializing application without privileges.");
- startWithWatchDog("init", 60, TimeUnit.SECONDS, () -> loader.init(bundleLocation, false));
- }
- startWithWatchDog("start", 60, TimeUnit.SECONDS, () -> loader.start());
- } catch (Exception e) {
- try {
- log.log(Level.SEVERE, "Failed starting container", e);
- }
- finally {
- Runtime.getRuntime().halt(1);
- }
- }
- }
-
- @Override
- public void stop() throws Exception {
- startWithWatchDog("stop", 60, TimeUnit.SECONDS, () -> loader.stop());
- }
-
- @Override
- public void destroy() {
- try {
- startWithWatchDog("destroy", 60, TimeUnit.SECONDS, () -> loader.destroy());
- } catch (Exception e) {
- }
- }
-
-}
diff --git a/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java b/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java
deleted file mode 100644
index df8223a6d86..00000000000
--- a/jdisc_core/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonTestCase.java
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.jdisc.core;
-
-import org.apache.commons.daemon.DaemonContext;
-import org.apache.commons.daemon.DaemonController;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-/**
- * @author Simon Thoresen Hult
- */
-public class BootstrapDaemonTestCase {
-
- @Test
- public void requireThatPrivilegedLifecycleWorks() throws Exception {
- MyLoader loader = new MyLoader();
- BootstrapDaemon daemon = new BootstrapDaemon(loader, true);
- daemon.init(new MyContext("foo"));
- assertTrue(loader.hasState(true, false, false, false));
- assertTrue(loader.privileged);
- daemon.start();
- assertTrue(loader.hasState(true, true, false, false));
- daemon.stop();
- assertTrue(loader.hasState(true, true, true, false));
- daemon.destroy();
- assertTrue(loader.hasState(true, true, true, true));
- }
-
- @Test
- public void requireThatNonPrivilegedLifecycleWorks() throws Exception {
- MyLoader loader = new MyLoader();
- BootstrapDaemon daemon = new BootstrapDaemon(loader, false);
- daemon.init(new MyContext("foo"));
- assertTrue(loader.hasState(false, false, false, false));
- daemon.start();
- assertTrue(loader.hasState(true, true, false, false));
- assertFalse(loader.privileged);
- daemon.stop();
- assertTrue(loader.hasState(true, true, true, false));
- daemon.destroy();
- assertTrue(loader.hasState(true, true, true, true));
- }
-
- @Test
- public void requireThatBundleLocationIsRequired() throws Exception {
- MyLoader loader = new MyLoader();
- BootstrapDaemon daemon = new BootstrapDaemon(loader, true);
- try {
- daemon.init(new MyContext((String[])null));
- fail();
- } catch (IllegalArgumentException e) {
- assertNull(loader.bundleLocation);
- }
- try {
- daemon.init(new MyContext());
- fail();
- } catch (IllegalArgumentException e) {
- assertNull(loader.bundleLocation);
- }
- try {
- daemon.init(new MyContext((String)null));
- fail();
- } catch (IllegalArgumentException e) {
- assertNull(loader.bundleLocation);
- }
- try {
- daemon.init(new MyContext("foo", "bar"));
- fail();
- } catch (IllegalArgumentException e) {
- assertNull(loader.bundleLocation);
- }
-
- daemon.init(new MyContext("foo"));
- daemon.start();
-
- assertNotNull(loader.bundleLocation);
- assertEquals("foo", loader.bundleLocation);
-
- daemon.stop();
- daemon.destroy();
- }
-
- @Test
- public void requireThatEnvironmentIsRequired() {
- try {
- new BootstrapDaemon();
- fail();
- } catch (IllegalStateException e) {
-
- }
- }
-
- private static class MyLoader implements BootstrapLoader {
-
- String bundleLocation = null;
- boolean privileged = false;
- boolean initCalled = false;
- boolean startCalled = false;
- boolean stopCalled = false;
- boolean destroyCalled = false;
-
- boolean hasState(boolean initCalled, boolean startCalled, boolean stopCalled, boolean destroyCalled) {
- return this.initCalled == initCalled && this.startCalled == startCalled &&
- this.stopCalled == stopCalled && this.destroyCalled == destroyCalled;
- }
-
- @Override
- public void init(String bundleLocation, boolean privileged) throws Exception {
- this.bundleLocation = bundleLocation;
- this.privileged = privileged;
- initCalled = true;
- }
-
- @Override
- public void start() throws Exception {
- startCalled = true;
- }
-
- @Override
- public void stop() throws Exception {
- stopCalled = true;
- }
-
- @Override
- public void destroy() {
- destroyCalled = true;
- }
- }
-
- private static class MyContext implements DaemonContext {
-
- final String[] args;
-
- MyContext(String... args) {
- this.args = args;
- }
-
- @Override
- public DaemonController getController() {
- return null;
- }
-
- @Override
- public String[] getArguments() {
- return args;
- }
- }
-}
diff --git a/jdisc_core_test/integration_test/pom.xml b/jdisc_core_test/integration_test/pom.xml
index 392d1105716..670d812c9e9 100644
--- a/jdisc_core_test/integration_test/pom.xml
+++ b/jdisc_core_test/integration_test/pom.xml
@@ -301,7 +301,6 @@
</java.util.logging.config.file>
<jdisc.bundle.path>${project.build.directory}/dependency</jdisc.bundle.path>
<jdisc.cache.path>${project.build.directory}/bundlecache</jdisc.cache.path>
- <jdisc.config.file>src/test/resources/config.properties</jdisc.config.file>
<jdisc.logger.level>ALL</jdisc.logger.level>
</systemPropertyVariables>
</configuration>
diff --git a/jdisc_core_test/integration_test/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonIntegrationTest.java b/jdisc_core_test/integration_test/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonIntegrationTest.java
deleted file mode 100644
index d052d2d4715..00000000000
--- a/jdisc_core_test/integration_test/src/test/java/com/yahoo/jdisc/core/BootstrapDaemonIntegrationTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.jdisc.core;
-
-import com.google.inject.Inject;
-import com.google.inject.name.Named;
-import com.yahoo.jdisc.application.Application;
-import org.apache.commons.daemon.DaemonContext;
-import org.junit.Test;
-import org.mockito.Mockito;
-
-import static org.junit.Assert.assertEquals;
-
-
-/**
- * @author Simon Thoresen Hult
- */
-public class BootstrapDaemonIntegrationTest {
-
- @Test
- public void requireThatConfigFileIsInjected() throws Exception {
- BootstrapDaemon daemon = new BootstrapDaemon();
-
- DaemonContext ctx = Mockito.mock(DaemonContext.class);
- Mockito.doReturn(new String[] { MyApplication.class.getName() }).when(ctx).getArguments();
- daemon.init(ctx);
- daemon.start();
-
- assertEquals("bar", ((MyApplication)((ApplicationLoader)daemon.loader()).application()).foo);
-
- daemon.stop();
- daemon.destroy();
- }
-
- public static class MyApplication implements Application {
-
- final String foo;
-
- @Inject
- public MyApplication(@Named("foo") String foo) {
- this.foo = foo;
- }
-
- @Override
- public void start() {
-
- }
-
- @Override
- public void stop() {
-
- }
-
- @Override
- public void destroy() {
-
- }
- }
-}
diff --git a/jdisc_core_test/integration_test/src/test/resources/config.properties b/jdisc_core_test/integration_test/src/test/resources/config.properties
deleted file mode 100644
index 74d0a43fccf..00000000000
--- a/jdisc_core_test/integration_test/src/test/resources/config.properties
+++ /dev/null
@@ -1 +0,0 @@
-foo=bar
diff --git a/jdisc_core_test/test_bundles/cert-k-pkgs/src/main/java/com/yahoo/jdisc/bundle/k/CertificateK.java b/jdisc_core_test/test_bundles/cert-k-pkgs/src/main/java/com/yahoo/jdisc/bundle/k/CertificateK.java
index 459ee60c740..29588c755e4 100644
--- a/jdisc_core_test/test_bundles/cert-k-pkgs/src/main/java/com/yahoo/jdisc/bundle/k/CertificateK.java
+++ b/jdisc_core_test/test_bundles/cert-k-pkgs/src/main/java/com/yahoo/jdisc/bundle/k/CertificateK.java
@@ -133,8 +133,6 @@ public class CertificateK {
private final javax.xml.xpath.XPath xPath = null;
private final org.aopalliance.intercept.Joinpoint jointpoint = null;
private final org.aopalliance.aop.Advice advice = null;
- private final org.apache.commons.daemon.Daemon daemon = null;
- private final org.apache.commons.daemon.support.DaemonLoader daemonLoader = null;
private final org.apache.commons.logging.LogFactory logFactory = null;
private final org.apache.commons.logging.impl.SimpleLog simpleLog = null;
private final org.apache.log4j.Appender appender = null;
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java
index 865bcc61837..058317ffd25 100644
--- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java
+++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java
@@ -27,6 +27,7 @@ import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContext;
import com.yahoo.vespa.hosted.node.admin.task.util.file.FileFinder;
import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath;
+import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -68,6 +69,7 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
private final ServiceIdentityProvider hostIdentityProvider;
private final IdentityDocumentClient identityDocumentClient;
private final CsrGenerator csrGenerator;
+ private final boolean useInternalZts;
// Used as an optimization to ensure ZTS is not DDoS'ed on continuously failing refresh attempts
private final Map<ContainerName, Instant> lastRefreshAttempt = new ConcurrentHashMap<>();
@@ -76,7 +78,8 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
Path trustStorePath,
ConfigServerInfo configServerInfo,
String certificateDnsSuffix,
- ServiceIdentityProvider hostIdentityProvider) {
+ ServiceIdentityProvider hostIdentityProvider,
+ boolean useInternalZts) {
this.ztsEndpoint = ztsEndpoint;
this.trustStorePath = trustStorePath;
this.configserverIdentity = configServerInfo.getConfigServerIdentity();
@@ -87,6 +90,7 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
hostIdentityProvider,
new AthenzIdentityVerifier(singleton(configserverIdentity)));
this.clock = Clock.systemUTC();
+ this.useInternalZts = useInternalZts;
}
public boolean converge(NodeAgentContext context) {
@@ -157,7 +161,12 @@ public class AthenzCredentialsMaintainer implements CredentialsMaintainer {
SignedIdentityDocument signedIdentityDocument = identityDocumentClient.getNodeIdentityDocument(context.hostname().value());
Pkcs10Csr csr = csrGenerator.generateInstanceCsr(
context.identity(), signedIdentityDocument.providerUniqueId(), signedIdentityDocument.ipAddresses(), keyPair);
- try (ZtsClient ztsClient = new DefaultZtsClient(ztsEndpoint, hostIdentityProvider)) {
+
+ // Set up a hostname verified for zts if this is configured to use the config server (internal zts) apis
+ HostnameVerifier ztsHostNameVerifier = useInternalZts
+ ? new AthenzIdentityVerifier(singleton(configserverIdentity))
+ : null;
+ try (ZtsClient ztsClient = new DefaultZtsClient(ztsEndpoint, hostIdentityProvider, ztsHostNameVerifier)) {
InstanceIdentity instanceIdentity =
ztsClient.registerInstance(
configserverIdentity,
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 02161caead6..29c0544420a 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
@@ -168,7 +168,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent {
loadBalancerExpirerInterval = Duration.ofMinutes(10);
reservationExpiry = Duration.ofMinutes(20); // Need to be long enough for deployment to be finished for all config model versions
dynamicProvisionerInterval = Duration.ofMinutes(5);
- osUpgradeActivatorInterval = Duration.ofMinutes(5);
+ osUpgradeActivatorInterval = zone.system().isCd() ? Duration.ofSeconds(30) : Duration.ofMinutes(5);
if (zone.environment().equals(Environment.prod) && ! zone.system().isCd()) {
inactiveExpiry = Duration.ofHours(4); // enough time for the application owner to discover and redeploy
diff --git a/searchcore/src/vespa/searchcore/proton/docsummary/docsumcontext.cpp b/searchcore/src/vespa/searchcore/proton/docsummary/docsumcontext.cpp
index 9e36e19f5be..cc6b0d952a3 100644
--- a/searchcore/src/vespa/searchcore/proton/docsummary/docsumcontext.cpp
+++ b/searchcore/src/vespa/searchcore/proton/docsummary/docsumcontext.cpp
@@ -213,8 +213,11 @@ DocsumContext::ParseLocation(search::docsummary::GetDocsumsState *state)
}
std::unique_ptr<MatchingElements>
-DocsumContext::fill_matching_elements(const StructFieldMapper &)
+DocsumContext::fill_matching_elements(const StructFieldMapper &struct_field_mapper)
{
+ if (_matcher) {
+ return _matcher->get_matching_elements(_request, _searchCtx, _attrCtx, _sessionMgr, struct_field_mapper);
+ }
return std::make_unique<MatchingElements>();
}
diff --git a/searchcore/src/vespa/searchcore/proton/matching/docsum_matcher.cpp b/searchcore/src/vespa/searchcore/proton/matching/docsum_matcher.cpp
index 34310371755..71b1164d127 100644
--- a/searchcore/src/vespa/searchcore/proton/matching/docsum_matcher.cpp
+++ b/searchcore/src/vespa/searchcore/proton/matching/docsum_matcher.cpp
@@ -4,6 +4,7 @@
#include <vespa/eval/eval/tensor.h>
#include <vespa/eval/eval/tensor_engine.h>
#include <vespa/vespalib/objects/nbostream.h>
+#include <vespa/searchcommon/attribute/i_search_context.h>
#include <vespa/searchlib/queryeval/blueprint.h>
#include <vespa/searchlib/queryeval/intermediate_blueprints.h>
#include <vespa/searchlib/queryeval/same_element_blueprint.h>
@@ -23,6 +24,8 @@ using search::queryeval::IntermediateBlueprint;
using search::queryeval::SameElementBlueprint;
using search::queryeval::SearchIterator;
+using AttrSearchCtx = search::attribute::ISearchContext;
+
namespace proton::matching {
namespace {
@@ -98,11 +101,29 @@ void find_matching_elements(const std::vector<uint32_t> &docs, const SameElement
}
}
+void find_matching_elements(const std::vector<uint32_t> &docs, const vespalib::string &struct_field_name, const AttrSearchCtx &attr_ctx, MatchingElements &result) {
+ int32_t weight = 0;
+ std::vector<uint32_t> matches;
+ for (uint32_t i = 0; i < docs.size(); ++i) {
+ for (int32_t id = attr_ctx.find(docs[i], 0, weight); id >= 0; id = attr_ctx.find(docs[i], id+1, weight)) {
+ matches.push_back(id);
+ }
+ if (!matches.empty()) {
+ result.add_matching_elements(docs[i], struct_field_name, matches);
+ matches.clear();
+ }
+ }
+}
+
void find_matching_elements(const StructFieldMapper &mapper, const std::vector<uint32_t> &docs, const Blueprint &bp, MatchingElements &result) {
if (auto same_element = as<SameElementBlueprint>(bp)) {
if (mapper.is_struct_field(same_element->struct_field_name())) {
find_matching_elements(docs, *same_element, result);
}
+ } else if (const AttrSearchCtx *attr_ctx = bp.get_attribute_search_context()) {
+ if (mapper.is_struct_subfield(attr_ctx->attributeName())) {
+ find_matching_elements(docs, mapper.get_struct_field(attr_ctx->attributeName()), *attr_ctx, result);
+ }
} else if (auto and_not = as<AndNotBlueprint>(bp)) {
find_matching_elements(mapper, docs, and_not->getChild(0), result);
} else if (auto intermediate = as<IntermediateBlueprint>(bp)) {
diff --git a/searchlib/src/tests/docstore/logdatastore/logdatastore_test.cpp b/searchlib/src/tests/docstore/logdatastore/logdatastore_test.cpp
index 63c1b320fb8..35e21d133c2 100644
--- a/searchlib/src/tests/docstore/logdatastore/logdatastore_test.cpp
+++ b/searchlib/src/tests/docstore/logdatastore/logdatastore_test.cpp
@@ -289,7 +289,8 @@ TEST("testTruncatedIdxFile"){
}
const char * magic = "mumbo jumbo";
{
- truncate("bug-7257706-truncated/1422358701368384000.idx", 3830);
+ int truncate_result = truncate("bug-7257706-truncated/1422358701368384000.idx", 3830);
+ EXPECT_EQUAL(0, truncate_result);
LogDataStore datastore(executor, "bug-7257706-truncated", config, GrowStrategy(),
TuneFileSummary(), fileHeaderContext, tlSyncer, nullptr);
EXPECT_EQUAL(331ul, datastore.lastSyncToken());
diff --git a/searchlib/src/tests/fef/properties/properties_test.cpp b/searchlib/src/tests/fef/properties/properties_test.cpp
index df868de3a97..b7478da3f71 100644
--- a/searchlib/src/tests/fef/properties/properties_test.cpp
+++ b/searchlib/src/tests/fef/properties/properties_test.cpp
@@ -226,6 +226,14 @@ TEST("test stuff") {
EXPECT_TRUE(!eval::LazyExpressions::check(p, true));
EXPECT_TRUE(!eval::LazyExpressions::check(p, false));
}
+ { // vespa.eval.use_fast_forest
+ EXPECT_EQUAL(eval::UseFastForest::NAME, vespalib::string("vespa.eval.use_fast_forest"));
+ EXPECT_EQUAL(eval::UseFastForest::DEFAULT_VALUE, false);
+ Properties p;
+ EXPECT_EQUAL(eval::UseFastForest::check(p), false);
+ p.add("vespa.eval.use_fast_forest", "true");
+ EXPECT_EQUAL(eval::UseFastForest::check(p), true);
+ }
{ // vespa.rank.firstphase
EXPECT_EQUAL(rank::FirstPhase::NAME, vespalib::string("vespa.rank.firstphase"));
EXPECT_EQUAL(rank::FirstPhase::DEFAULT_VALUE, vespalib::string("nativeRank"));
diff --git a/searchlib/src/tests/fef/rank_program/rank_program_test.cpp b/searchlib/src/tests/fef/rank_program/rank_program_test.cpp
index 7e28178e5f7..d1b0f8112f3 100644
--- a/searchlib/src/tests/fef/rank_program/rank_program_test.cpp
+++ b/searchlib/src/tests/fef/rank_program/rank_program_test.cpp
@@ -90,6 +90,10 @@ struct Fixture {
value ? "true" : "false");
return *this;
}
+ Fixture &use_fast_forest() {
+ indexEnv.getProperties().add(indexproperties::eval::UseFastForest::NAME, "true");
+ return *this;
+ }
Fixture &add_expr(const vespalib::string &name, const vespalib::string &expr) {
vespalib::string feature_name = expr_feature(name);
vespalib::string expr_name = feature_name + ".rankingScript";
@@ -113,6 +117,11 @@ struct Fixture {
program.setup(*match_data, queryEnv, overrides);
return *this;
}
+ vespalib::string final_executor_name() const {
+ size_t n = program.num_executors();
+ ASSERT_TRUE(n > 0);
+ return program.get_executor(n-1).getClassName();
+ }
double get(uint32_t docid = default_docid) {
auto result = program.get_seeds();
EXPECT_EQUAL(1u, result.num_features());
@@ -360,4 +369,26 @@ TEST_F("require that interpreted ranking expressions are pure", Fixture()) {
EXPECT_EQUAL(f1.get(), 7.0);
}
+const vespalib::string tree_expr = "if(value(1)<2,1,2)+if(value(2)<1,10,20)";
+
+TEST_F("require that fast-forest gbdt evaluation can be enabled", Fixture()) {
+ f1.use_fast_forest().add_expr("rank", tree_expr).compile();
+ EXPECT_EQUAL(f1.get(), 21.0);
+ EXPECT_EQUAL(f1.final_executor_name(), "search::features::FastForestExecutor");
+}
+
+TEST_F("require that fast-forest gbdt evaluation is disabled by default", Fixture()) {
+ f1.add_expr("rank", tree_expr).compile();
+ EXPECT_EQUAL(f1.get(), 21.0);
+ EXPECT_EQUAL(f1.final_executor_name(), "search::features::CompiledRankingExpressionExecutor");
+}
+
+TEST_F("require that fast-forest gbdt evaluation is pure", Fixture()) {
+ f1.use_fast_forest().add_expr("rank", tree_expr).compile();
+ EXPECT_EQUAL(3u, count_features(f1.program));
+ EXPECT_EQUAL(3u, count_const_features(f1.program));
+ EXPECT_EQUAL(f1.get(), 21.0);
+ EXPECT_EQUAL(f1.final_executor_name(), "search::features::FastForestExecutor");
+}
+
TEST_MAIN() { TEST_RUN_ALL(); }
diff --git a/searchlib/src/vespa/searchlib/attribute/attribute_blueprint_factory.cpp b/searchlib/src/vespa/searchlib/attribute/attribute_blueprint_factory.cpp
index 192d498125c..5261f568673 100644
--- a/searchlib/src/vespa/searchlib/attribute/attribute_blueprint_factory.cpp
+++ b/searchlib/src/vespa/searchlib/attribute/attribute_blueprint_factory.cpp
@@ -136,6 +136,10 @@ public:
}
void visitMembers(vespalib::ObjectVisitor &visitor) const override;
+
+ const attribute::ISearchContext *get_attribute_search_context() const override {
+ return _search_context.get();
+ }
};
void
diff --git a/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.cpp b/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.cpp
index 2733ec62105..a4b2280fa57 100644
--- a/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.cpp
+++ b/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.cpp
@@ -11,20 +11,21 @@
#include <vespa/log/log.h>
LOG_SETUP(".features.rankingexpression");
-using vespalib::eval::Function;
-using vespalib::eval::PassParams;
+using search::fef::FeatureType;
+using vespalib::ArrayRef;
+using vespalib::ConstArrayRef;
using vespalib::eval::CompileCache;
using vespalib::eval::CompiledFunction;
+using vespalib::eval::DoubleValue;
+using vespalib::eval::Function;
using vespalib::eval::InterpretedFunction;
using vespalib::eval::LazyParams;
-using vespalib::eval::ValueType;
-using vespalib::eval::Value;
-using vespalib::eval::DoubleValue;
using vespalib::eval::NodeTypes;
+using vespalib::eval::PassParams;
+using vespalib::eval::Value;
+using vespalib::eval::ValueType;
+using vespalib::eval::gbdt::FastForest;
using vespalib::tensor::DefaultTensorEngine;
-using search::fef::FeatureType;
-using vespalib::ArrayRef;
-using vespalib::ConstArrayRef;
namespace search::features {
@@ -43,6 +44,23 @@ vespalib::string list_issues(const std::vector<vespalib::string> &issues) {
//-----------------------------------------------------------------------------
/**
+ * Implements the executor for fast forest gbdt evaluation
+ **/
+class FastForestExecutor : public fef::FeatureExecutor
+{
+private:
+ const FastForest &_forest;
+ FastForest::Context _ctx;
+
+public:
+ FastForestExecutor(const FastForest &forest);
+ bool isPure() override { return true; }
+ void execute(uint32_t docId) override;
+};
+
+//-----------------------------------------------------------------------------
+
+/**
* Implements the executor for compiled ranking expressions
**/
class CompiledRankingExpressionExecutor : public fef::FeatureExecutor
@@ -110,6 +128,22 @@ public:
//-----------------------------------------------------------------------------
+FastForestExecutor::FastForestExecutor(const FastForest &forest)
+ : _forest(forest),
+ _ctx(_forest)
+{
+}
+
+void
+FastForestExecutor::execute(uint32_t)
+{
+ const auto &params = inputs();
+ double result = _forest.eval(_ctx, [&params](size_t p){ return params.get_number(p); });
+ outputs().set_number(0, result);
+}
+
+//-----------------------------------------------------------------------------
+
CompiledRankingExpressionExecutor::CompiledRankingExpressionExecutor(const CompiledFunction &compiled_function)
: _ranking_function(compiled_function.get_function()),
_params(compiled_function.num_params(), 0.0)
@@ -178,6 +212,7 @@ RankingExpressionBlueprint::RankingExpressionBlueprint(rankingexpression::Expres
: fef::Blueprint("rankingExpression"),
_expression_replacer(std::move(replacer)),
_intrinsic_expression(),
+ _fast_forest(),
_interpreted_function(),
_compile_token(),
_input_is_object()
@@ -259,11 +294,17 @@ RankingExpressionBlueprint::setup(const fef::IIndexEnvironment &env,
// avoid costly compilation when only verifying setup
if (env.getFeatureMotivation() != env.FeatureMotivation::VERIFY_SETUP) {
if (do_compile) {
- bool suggest_lazy = CompiledFunction::should_use_lazy_params(rank_function);
- if (fef::indexproperties::eval::LazyExpressions::check(env.getProperties(), suggest_lazy)) {
- _compile_token = CompileCache::compile(rank_function, PassParams::LAZY);
- } else {
- _compile_token = CompileCache::compile(rank_function, PassParams::ARRAY);
+ // fast forest evaluation is a possible replacement for compiled tree models
+ if (fef::indexproperties::eval::UseFastForest::check(env.getProperties())) {
+ _fast_forest = FastForest::try_convert(rank_function);
+ }
+ if (!_fast_forest) {
+ bool suggest_lazy = CompiledFunction::should_use_lazy_params(rank_function);
+ if (fef::indexproperties::eval::LazyExpressions::check(env.getProperties(), suggest_lazy)) {
+ _compile_token = CompileCache::compile(rank_function, PassParams::LAZY);
+ } else {
+ _compile_token = CompileCache::compile(rank_function, PassParams::ARRAY);
+ }
}
} else {
_interpreted_function.reset(new InterpretedFunction(DefaultTensorEngine::ref(), rank_function, node_types));
@@ -300,6 +341,9 @@ RankingExpressionBlueprint::createExecutor(const fef::IQueryEnvironment &env, ve
ConstArrayRef<char> input_is_object = stash.copy_array<char>(_input_is_object);
return stash.create<InterpretedRankingExpressionExecutor>(*_interpreted_function, input_is_object);
}
+ if (_fast_forest) {
+ return stash.create<FastForestExecutor>(*_fast_forest);
+ }
assert(_compile_token.get() != nullptr); // will be nullptr for VERIFY_SETUP feature motivation
if (_compile_token->get().pass_params() == PassParams::ARRAY) {
return stash.create<CompiledRankingExpressionExecutor>(_compile_token->get());
diff --git a/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.h b/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.h
index 104e8d63a70..579c8cf91a7 100644
--- a/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.h
+++ b/searchlib/src/vespa/searchlib/features/rankingexpressionfeature.h
@@ -2,6 +2,7 @@
#pragma once
#include <vespa/searchlib/fef/blueprint.h>
+#include <vespa/eval/eval/fast_forest.h>
#include <vespa/eval/eval/interpreted_function.h>
#include <vespa/eval/eval/llvm/compile_cache.h>
#include <vespa/searchlib/features/rankingexpression/expression_replacer.h>
@@ -19,6 +20,7 @@ class RankingExpressionBlueprint : public fef::Blueprint
private:
rankingexpression::ExpressionReplacer::SP _expression_replacer;
rankingexpression::IntrinsicExpression::UP _intrinsic_expression;
+ vespalib::eval::gbdt::FastForest::UP _fast_forest;
vespalib::eval::InterpretedFunction::UP _interpreted_function;
vespalib::eval::CompileCache::Token::UP _compile_token;
std::vector<char> _input_is_object;
diff --git a/searchlib/src/vespa/searchlib/fef/indexproperties.cpp b/searchlib/src/vespa/searchlib/fef/indexproperties.cpp
index a7df39faf2f..ce1bd69cc4c 100644
--- a/searchlib/src/vespa/searchlib/fef/indexproperties.cpp
+++ b/searchlib/src/vespa/searchlib/fef/indexproperties.cpp
@@ -84,6 +84,10 @@ LazyExpressions::check(const Properties &props, bool default_value)
return lookupBool(props, NAME, default_value);
}
+const vespalib::string UseFastForest::NAME("vespa.eval.use_fast_forest");
+const bool UseFastForest::DEFAULT_VALUE(false);
+bool UseFastForest::check(const Properties &props) { return lookupBool(props, NAME, DEFAULT_VALUE); }
+
} // namespace eval
namespace rank {
diff --git a/searchlib/src/vespa/searchlib/fef/indexproperties.h b/searchlib/src/vespa/searchlib/fef/indexproperties.h
index 9adf4487ec5..57aa24222a3 100644
--- a/searchlib/src/vespa/searchlib/fef/indexproperties.h
+++ b/searchlib/src/vespa/searchlib/fef/indexproperties.h
@@ -26,6 +26,13 @@ struct LazyExpressions {
static bool check(const Properties &props, bool default_value);
};
+// use fast-forest evaluation for gbdt expressions. affects rank/summary/dump
+struct UseFastForest {
+ static const vespalib::string NAME;
+ static const bool DEFAULT_VALUE;
+ static bool check(const Properties &props);
+};
+
} // namespace eval
namespace rank {
diff --git a/searchlib/src/vespa/searchlib/fef/rank_program.h b/searchlib/src/vespa/searchlib/fef/rank_program.h
index 3a92fc874a4..e1014df5ee5 100644
--- a/searchlib/src/vespa/searchlib/fef/rank_program.h
+++ b/searchlib/src/vespa/searchlib/fef/rank_program.h
@@ -59,6 +59,7 @@ public:
~RankProgram();
size_t num_executors() const { return _executors.size(); }
+ const FeatureExecutor &get_executor(size_t i) const { return *_executors[i]; }
/**
* Set up this rank program by creating the needed feature
diff --git a/searchlib/src/vespa/searchlib/queryeval/blueprint.h b/searchlib/src/vespa/searchlib/queryeval/blueprint.h
index 2f9dbabe52e..907ea9bb066 100644
--- a/searchlib/src/vespa/searchlib/queryeval/blueprint.h
+++ b/searchlib/src/vespa/searchlib/queryeval/blueprint.h
@@ -14,6 +14,7 @@ namespace vespalib::slime {
struct Cursor;
struct Inserter;
}
+namespace search::attribute { class ISearchContext; }
namespace search::queryeval {
@@ -198,6 +199,7 @@ public:
virtual bool isEquiv() const { return false; }
virtual bool isWhiteList() const { return false; }
virtual bool isIntermediate() const { return false; }
+ virtual const attribute::ISearchContext *get_attribute_search_context() const { return nullptr; }
};
namespace blueprint {
diff --git a/searchsummary/CMakeLists.txt b/searchsummary/CMakeLists.txt
index 4df636e0219..2a23dd4c495 100644
--- a/searchsummary/CMakeLists.txt
+++ b/searchsummary/CMakeLists.txt
@@ -25,6 +25,7 @@ vespa_define_module(
src/tests/docsumformat
src/tests/docsummary
src/tests/docsummary/attribute_combiner
+ src/tests/docsummary/matched_elements_filter
src/tests/docsummary/slime_summary
src/tests/extractkeywords
)
diff --git a/searchsummary/src/tests/docsummary/matched_elements_filter/CMakeLists.txt b/searchsummary/src/tests/docsummary/matched_elements_filter/CMakeLists.txt
new file mode 100644
index 00000000000..a87f5638acc
--- /dev/null
+++ b/searchsummary/src/tests/docsummary/matched_elements_filter/CMakeLists.txt
@@ -0,0 +1,10 @@
+# Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+find_package(GTest REQUIRED)
+vespa_add_executable(searchsummary_matched_elements_filter_test_app TEST
+ SOURCES
+ matched_elements_filter_test.cpp
+ DEPENDS
+ searchsummary
+ GTest::GTest
+)
+vespa_add_test(NAME searchsummary_matched_elements_filter_test_app COMMAND searchsummary_matched_elements_filter_test_app)
diff --git a/searchsummary/src/tests/docsummary/matched_elements_filter/matched_elements_filter_test.cpp b/searchsummary/src/tests/docsummary/matched_elements_filter/matched_elements_filter_test.cpp
new file mode 100644
index 00000000000..40d0285b1ec
--- /dev/null
+++ b/searchsummary/src/tests/docsummary/matched_elements_filter/matched_elements_filter_test.cpp
@@ -0,0 +1,205 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/document/datatype/datatype.h>
+#include <vespa/document/datatype/structdatatype.h>
+#include <vespa/document/document.h>
+#include <vespa/searchlib/common/matching_elements.h>
+#include <vespa/searchlib/util/slime_output_raw_buf_adapter.h>
+#include <vespa/searchsummary/docsummary/docsumstate.h>
+#include <vespa/searchsummary/docsummary/idocsumenvironment.h>
+#include <vespa/searchsummary/docsummary/matched_elements_filter_dfw.h>
+#include <vespa/searchsummary/docsummary/resultconfig.h>
+#include <vespa/searchsummary/docsummary/resultpacker.h>
+#include <vespa/searchsummary/docsummary/summaryfieldconverter.h>
+#include <vespa/vespalib/data/slime/json_format.h>
+#include <vespa/vespalib/data/slime/slime.h>
+#include <vespa/vespalib/gtest/gtest.h>
+#include <iostream>
+
+#include <vespa/log/log.h>
+LOG_SETUP("matched_elements_filter_test");
+
+using search::MatchingElements;
+using search::StructFieldMapper;
+using vespalib::Slime;
+
+using namespace document;
+using namespace search::docsummary;
+using namespace vespalib::slime;
+
+using ElementVector = std::vector<uint32_t>;
+
+struct SlimeValue {
+ Slime slime;
+
+ SlimeValue(const std::string& json_input)
+ : slime()
+ {
+ size_t used = JsonFormat::decode(json_input, slime);
+ EXPECT_GT(used, 0);
+ }
+ SlimeValue(const Slime& slime_with_raw_field)
+ : slime()
+ {
+ size_t used = BinaryFormat::decode(slime_with_raw_field.get().asString(), slime);
+ EXPECT_GT(used, 0);
+ }
+};
+
+StructDataType::UP
+make_struct_elem_type()
+{
+ auto result = std::make_unique<StructDataType>("elem");
+ result->addField(Field("name", *DataType::STRING, true));
+ result->addField(Field("weight", *DataType::INT, true));
+ return result;
+}
+
+constexpr uint32_t class_id = 3;
+constexpr uint32_t doc_id = 2;
+
+class DocsumStore {
+private:
+ ResultConfig _config;
+ ResultPacker _packer;
+ StructDataType::UP _elem_type;
+ ArrayDataType _array_type;
+ MapDataType _map_type;
+
+ StructFieldValue::UP make_elem_value(const std::string& name, int weight) const {
+ auto result = std::make_unique<StructFieldValue>(*_elem_type);
+ result->setValue("name", StringFieldValue(name));
+ result->setValue("weight", IntFieldValue(weight));
+ return result;
+ }
+
+ void write_field_value(const FieldValue& value) {
+ auto converted = SummaryFieldConverter::convertSummaryField(false, value);
+ const auto* raw_field = dynamic_cast<const RawFieldValue*>(converted.get());
+ ASSERT_TRUE(raw_field);
+ auto raw_buf = raw_field->getAsRaw();
+ bool result = _packer.AddLongString(raw_buf.first, raw_buf.second);
+ ASSERT_TRUE(result);
+ }
+
+public:
+ DocsumStore()
+ : _config(),
+ _packer(&_config),
+ _elem_type(make_struct_elem_type()),
+ _array_type(*_elem_type),
+ _map_type(*DataType::STRING, *_elem_type)
+ {
+ auto* result_class = _config.AddResultClass("test", class_id);
+ EXPECT_TRUE(result_class->AddConfigEntry("array", ResType::RES_JSONSTRING));
+ EXPECT_TRUE(result_class->AddConfigEntry("map", ResType::RES_JSONSTRING));
+ _config.CreateEnumMaps();
+ }
+ const ResultConfig& get_config() const { return _config; }
+ const ResultClass* get_class() const { return _config.LookupResultClass(class_id); }
+ search::docsummary::DocsumStoreValue getMappedDocsum() {
+ assert(_packer.Init(class_id));
+ {
+ ArrayFieldValue array_value(_array_type);
+ array_value.append(make_elem_value("a", 3));
+ array_value.append(make_elem_value("b", 5));
+ array_value.append(make_elem_value("c", 7));
+ write_field_value(array_value);
+ }
+ {
+ MapFieldValue map_value(_map_type);
+ map_value.put(StringFieldValue("a"), *make_elem_value("a", 3));
+ map_value.put(StringFieldValue("b"), *make_elem_value("b", 5));
+ map_value.put(StringFieldValue("c"), *make_elem_value("c", 7));
+ write_field_value(map_value);
+ }
+ const char* buf;
+ uint32_t buf_len;
+ assert(_packer.GetDocsumBlob(&buf, &buf_len));
+ return DocsumStoreValue(buf, buf_len);
+ }
+};
+
+class StateCallback : public GetDocsumsStateCallback {
+private:
+ std::string _field_name;
+ ElementVector _matching_elements;
+
+public:
+ StateCallback(const std::string& field_name, const ElementVector& matching_elements)
+ : _field_name(field_name),
+ _matching_elements(matching_elements)
+ {
+ }
+ ~StateCallback() {}
+ void FillSummaryFeatures(GetDocsumsState*, IDocsumEnvironment*) override {}
+ void FillRankFeatures(GetDocsumsState*, IDocsumEnvironment*) override {}
+ void ParseLocation(GetDocsumsState*) override {}
+ std::unique_ptr<MatchingElements> fill_matching_elements(const StructFieldMapper&) override {
+ auto result = std::make_unique<MatchingElements>();
+ result->add_matching_elements(doc_id, _field_name, _matching_elements);
+ return result;
+ }
+};
+
+class MatchedElementsFilterTest : public ::testing::Test {
+private:
+ DocsumStore _store;
+
+ SlimeValue run_filter_field_writer(const std::string& input_field_name, const ElementVector& matching_elements) {
+ int input_field_enum = _store.get_config().GetFieldNameEnum().Lookup(input_field_name.c_str());
+ EXPECT_GE(input_field_enum, 0);
+ MatchedElementsFilterDFW filter(input_field_name, input_field_enum);
+
+ GeneralResult result(_store.get_class());
+ result.inplaceUnpack(_store.getMappedDocsum());
+ StateCallback callback(input_field_name, matching_elements);
+ GetDocsumsState state(callback);
+ Slime slime;
+ SlimeInserter inserter(slime);
+
+ filter.insertField(doc_id, &result, &state, ResType::RES_JSONSTRING, inserter);
+ return SlimeValue(slime);
+ }
+
+public:
+ MatchedElementsFilterTest()
+ : _store()
+ {
+ }
+ void expect_filtered(const std::string& input_field_name, const ElementVector& matching_elements, const std::string& exp_slime_as_json) {
+ SlimeValue act = run_filter_field_writer(input_field_name, matching_elements);
+ SlimeValue exp(exp_slime_as_json);
+ EXPECT_EQ(exp.slime, act.slime);
+ }
+};
+
+TEST_F(MatchedElementsFilterTest, filters_elements_in_array_field_value)
+{
+ expect_filtered("array", {}, "[]");
+ expect_filtered("array", {0}, "[{'name':'a','weight':3}]");
+ expect_filtered("array", {1}, "[{'name':'b','weight':5}]");
+ expect_filtered("array", {2}, "[{'name':'c','weight':7}]");
+ expect_filtered("array", {0, 1, 2}, "[{'name':'a','weight':3},"
+ "{'name':'b','weight':5},"
+ "{'name':'c','weight':7}]");
+}
+
+TEST_F(MatchedElementsFilterTest, filters_elements_in_map_field_value)
+{
+ expect_filtered("map", {}, "[]");
+ expect_filtered("map", {0}, "[{'key':'a','value':{'name':'a','weight':3}}]");
+ expect_filtered("map", {1}, "[{'key':'b','value':{'name':'b','weight':5}}]");
+ expect_filtered("map", {2}, "[{'key':'c','value':{'name':'c','weight':7}}]");
+ expect_filtered("map", {0, 1, 2}, "[{'key':'a','value':{'name':'a','weight':3}},"
+ "{'key':'b','value':{'name':'b','weight':5}},"
+ "{'key':'c','value':{'name':'c','weight':7}}]");
+}
+
+TEST_F(MatchedElementsFilterTest, field_writer_is_not_generated_as_it_depends_on_data_from_document_store)
+{
+ MatchedElementsFilterDFW filter("foo", 0);
+ EXPECT_FALSE(filter.IsGenerated());
+}
+
+GTEST_MAIN_RUN_ALL_TESTS()
diff --git a/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt b/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt
index dccf72b2fe7..fb6a399e71c 100644
--- a/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt
+++ b/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt
@@ -4,29 +4,30 @@ vespa_add_library(searchsummary_docsummary OBJECT
array_attribute_combiner_dfw.cpp
attribute_combiner_dfw.cpp
attribute_field_writer.cpp
- resultclass.cpp
- resultconfig.cpp
- resultpacker.cpp
- urlresult.cpp
- getdocsumargs.cpp
- docsumstate.cpp
+ attributedfw.cpp
+ docsumconfig.cpp
docsumfieldwriter.cpp
+ docsumstate.cpp
docsumwriter.cpp
- keywordextractor.cpp
- attributedfw.cpp
dynamicteaserdfw.cpp
- docsumconfig.cpp
- rankfeaturesdfw.cpp
- summaryfeaturesdfw.cpp
- juniperproperties.cpp
- textextractordfw.cpp
geoposdfw.cpp
- tokenizer.cpp
- positionsdfw.cpp
+ getdocsumargs.cpp
+ juniperproperties.cpp
+ keywordextractor.cpp
linguisticsannotation.cpp
+ matched_elements_filter_dfw.cpp
+ positionsdfw.cpp
+ rankfeaturesdfw.cpp
+ resultclass.cpp
+ resultconfig.cpp
+ resultpacker.cpp
searchdatatype.cpp
struct_map_attribute_combiner_dfw.cpp
+ summaryfeaturesdfw.cpp
summaryfieldconverter.cpp
+ textextractordfw.cpp
+ tokenizer.cpp
+ urlresult.cpp
AFTER
searchsummary_config
)
diff --git a/searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.cpp b/searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.cpp
new file mode 100644
index 00000000000..1b7533b53e3
--- /dev/null
+++ b/searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.cpp
@@ -0,0 +1,87 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "docsumstate.h"
+#include "matched_elements_filter_dfw.h"
+#include <vespa/searchlib/common/matching_elements.h>
+#include <vespa/searchlib/common/struct_field_mapper.h>
+#include <vespa/vespalib/data/slime/binary_format.h>
+#include <vespa/vespalib/data/slime/slime.h>
+#include <vespa/vespalib/data/smart_buffer.h>
+#include <cassert>
+
+using vespalib::Slime;
+using vespalib::slime::ArrayInserter;
+using vespalib::slime::BinaryFormat;
+using vespalib::slime::Inserter;
+using vespalib::slime::Inspector;
+using vespalib::slime::SlimeInserter;
+using vespalib::slime::inject;
+
+namespace search::docsummary {
+
+MatchedElementsFilterDFW::MatchedElementsFilterDFW(const std::string& input_field_name, uint32_t input_field_enum)
+ : _input_field_name(input_field_name),
+ _input_field_enum(input_field_enum),
+ _struct_field_mapper(std::make_shared<StructFieldMapper>())
+{
+ // TODO: Take struct field mapper in constructor and populate based on available attribute vectors.
+}
+
+MatchedElementsFilterDFW::~MatchedElementsFilterDFW() = default;
+
+namespace {
+
+void
+decode_input_field(const ResEntry& entry, search::RawBuf& target_buf, Slime& input_field)
+{
+ const char* buf;
+ uint32_t buf_len;
+ entry._resolve_field(&buf, &buf_len, &target_buf);
+ BinaryFormat::decode(vespalib::Memory(buf, buf_len), input_field);
+}
+
+void
+filter_matching_elements_in_input_field(const Slime& input_field, const std::vector<uint32_t>& matching_elems, Slime& output_field)
+{
+ SlimeInserter output_inserter(output_field);
+ Inspector& input_inspector = input_field.get();
+ ArrayInserter array_inserter(output_inserter.insertArray());
+ auto elems_itr = matching_elems.begin();
+ for (size_t i = 0; (i < input_inspector.entries()) && (elems_itr != matching_elems.end()); ++i) {
+ assert(*elems_itr >= i);
+ if (*elems_itr == i) {
+ inject(input_inspector[i], array_inserter);
+ ++elems_itr;
+ }
+ }
+}
+
+void
+encode_output_field(const Slime& output_field, Inserter& target)
+{
+ vespalib::SmartBuffer buf(4096);
+ BinaryFormat::encode(output_field, buf);
+ target.insertString(buf.obtain());
+}
+
+}
+
+void
+MatchedElementsFilterDFW::insertField(uint32_t docid, GeneralResult* result, GetDocsumsState *state,
+ ResType type, vespalib::slime::Inserter& target)
+{
+ assert(type == ResType::RES_JSONSTRING);
+ int entry_idx = result->GetClass()->GetIndexFromEnumValue(_input_field_enum);
+ ResEntry* entry = result->GetEntry(entry_idx);
+ if (entry != nullptr) {
+ Slime input_field;
+ decode_input_field(*entry, state->_docSumFieldSpace, input_field);
+
+ Slime output_field;
+ filter_matching_elements_in_input_field(input_field, state->get_matching_elements(*_struct_field_mapper).get_matching_elements(docid, _input_field_name), output_field);
+
+ encode_output_field(output_field, target);
+ }
+}
+
+}
diff --git a/searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.h b/searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.h
new file mode 100644
index 00000000000..b96d3595b0a
--- /dev/null
+++ b/searchsummary/src/vespa/searchsummary/docsummary/matched_elements_filter_dfw.h
@@ -0,0 +1,27 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#pragma once
+
+#include "docsumfieldwriter.h"
+
+namespace search::docsummary {
+
+/**
+ * Field writer that filters matched elements (according to the query) from a complex field
+ * (map of primitives, map of struct, array of struct) that is retrieved from the document store.
+ */
+class MatchedElementsFilterDFW : public IDocsumFieldWriter {
+private:
+ std::string _input_field_name;
+ uint32_t _input_field_enum;
+ std::shared_ptr<StructFieldMapper> _struct_field_mapper;
+
+public:
+ MatchedElementsFilterDFW(const std::string& input_field_name, uint32_t input_field_enum);
+ ~MatchedElementsFilterDFW();
+ bool IsGenerated() const override { return false; }
+ void insertField(uint32_t docid, GeneralResult* result, GetDocsumsState *state,
+ ResType type, vespalib::slime::Inserter& target) override;
+};
+
+}
diff --git a/storage/src/tests/distributor/bucketdbupdatertest.cpp b/storage/src/tests/distributor/bucketdbupdatertest.cpp
index 321b0cc3bba..8409bd60986 100644
--- a/storage/src/tests/distributor/bucketdbupdatertest.cpp
+++ b/storage/src/tests/distributor/bucketdbupdatertest.cpp
@@ -2,11 +2,10 @@
#include <vespa/storageapi/message/persistence.h>
#include <vespa/storage/distributor/bucketdbupdater.h>
+#include <vespa/storage/distributor/bucket_space_distribution_context.h>
#include <vespa/storage/distributor/distributormetricsset.h>
#include <vespa/storage/distributor/pending_bucket_space_db_transition.h>
#include <vespa/storage/distributor/outdated_nodes_map.h>
-#include <vespa/vespalib/io/fileutil.h>
-#include <vespa/storageframework/defaultimplementation/clock/realclock.h>
#include <vespa/storage/storageutil/distributorstatecache.h>
#include <tests/distributor/distributortestutil.h>
#include <vespa/document/test/make_document_bucket.h>
@@ -124,7 +123,7 @@ public:
createLinks();
_bucketSpaces = getBucketSpaces();
// Disable deferred activation by default (at least for now) to avoid breaking the entire world.
- getConfig().setAllowStaleReadsDuringClusterStateTransitions(false);
+ getBucketDBUpdater().set_stale_reads_enabled(false);
};
void TearDown() override {
@@ -2415,7 +2414,7 @@ void for_each_bucket(const DistributorBucketSpaceRepo& repo, Func&& f) {
}
TEST_F(BucketDBUpdaterTest, non_owned_buckets_moved_to_read_only_db_on_ownership_change) {
- getConfig().setAllowStaleReadsDuringClusterStateTransitions(true);
+ getBucketDBUpdater().set_stale_reads_enabled(true);
lib::ClusterState initial_state("distributor:1 storage:4"); // All buckets owned by us by definition
set_cluster_state_bundle(lib::ClusterStateBundle(initial_state, {}, false)); // Skip activation step for simplicity
@@ -2468,7 +2467,7 @@ TEST_F(BucketDBUpdaterTest, buckets_no_longer_available_are_not_moved_to_read_on
}
TEST_F(BucketDBUpdaterTest, non_owned_buckets_purged_when_read_only_support_is_config_disabled) {
- getConfig().setAllowStaleReadsDuringClusterStateTransitions(false);
+ getBucketDBUpdater().set_stale_reads_enabled(false);
lib::ClusterState initial_state("distributor:1 storage:4"); // All buckets owned by us by definition
set_cluster_state_bundle(lib::ClusterStateBundle(initial_state, {}, false)); // Skip activation step for simplicity
@@ -2497,7 +2496,6 @@ void BucketDBUpdaterTest::trigger_completed_but_not_yet_activated_transition(
uint32_t pending_buckets,
uint32_t pending_expected_msgs)
{
- getConfig().setAllowStaleReadsDuringClusterStateTransitions(true);
lib::ClusterState initial_state(initial_state_str);
setSystemState(initial_state);
ASSERT_EQ(messageCount(initial_expected_msgs), _sender.commands().size());
@@ -2514,6 +2512,7 @@ void BucketDBUpdaterTest::trigger_completed_but_not_yet_activated_transition(
}
TEST_F(BucketDBUpdaterTest, deferred_activated_state_does_not_enable_state_until_activation_received) {
+ getBucketDBUpdater().set_stale_reads_enabled(true);
constexpr uint32_t n_buckets = 10;
ASSERT_NO_FATAL_FAILURE(
trigger_completed_but_not_yet_activated_transition("version:1 distributor:2 storage:4", 0, 4,
@@ -2533,6 +2532,7 @@ TEST_F(BucketDBUpdaterTest, deferred_activated_state_does_not_enable_state_until
}
TEST_F(BucketDBUpdaterTest, read_only_db_cleared_once_pending_state_is_activated) {
+ getBucketDBUpdater().set_stale_reads_enabled(true);
constexpr uint32_t n_buckets = 10;
ASSERT_NO_FATAL_FAILURE(
trigger_completed_but_not_yet_activated_transition("version:1 distributor:1 storage:4", n_buckets, 4,
@@ -2544,6 +2544,7 @@ TEST_F(BucketDBUpdaterTest, read_only_db_cleared_once_pending_state_is_activated
}
TEST_F(BucketDBUpdaterTest, read_only_db_is_populated_even_when_self_is_marked_down) {
+ getBucketDBUpdater().set_stale_reads_enabled(true);
constexpr uint32_t n_buckets = 10;
ASSERT_NO_FATAL_FAILURE(
trigger_completed_but_not_yet_activated_transition("version:1 distributor:1 storage:4", n_buckets, 4,
@@ -2557,6 +2558,7 @@ TEST_F(BucketDBUpdaterTest, read_only_db_is_populated_even_when_self_is_marked_d
}
TEST_F(BucketDBUpdaterTest, activate_cluster_state_request_with_mismatching_version_returns_actual_version) {
+ getBucketDBUpdater().set_stale_reads_enabled(true);
constexpr uint32_t n_buckets = 10;
ASSERT_NO_FATAL_FAILURE(
trigger_completed_but_not_yet_activated_transition("version:4 distributor:1 storage:4", n_buckets, 4,
@@ -2570,6 +2572,7 @@ TEST_F(BucketDBUpdaterTest, activate_cluster_state_request_with_mismatching_vers
}
TEST_F(BucketDBUpdaterTest, activate_cluster_state_request_without_pending_transition_passes_message_through) {
+ getBucketDBUpdater().set_stale_reads_enabled(true);
constexpr uint32_t n_buckets = 10;
ASSERT_NO_FATAL_FAILURE(
trigger_completed_but_not_yet_activated_transition("version:1 distributor:2 storage:4", 0, 4,
@@ -2727,4 +2730,136 @@ TEST_F(BucketDBUpdaterTest, pending_cluster_state_getter_is_non_null_only_when_s
EXPECT_TRUE(state == nullptr);
}
+struct BucketDBUpdaterSnapshotTest : BucketDBUpdaterTest {
+ lib::ClusterState empty_state;
+ std::shared_ptr<lib::ClusterState> initial_baseline;
+ std::shared_ptr<lib::ClusterState> initial_default;
+ lib::ClusterStateBundle initial_bundle;
+ Bucket default_bucket;
+ Bucket global_bucket;
+
+ BucketDBUpdaterSnapshotTest()
+ : BucketDBUpdaterTest(),
+ empty_state(),
+ initial_baseline(std::make_shared<lib::ClusterState>("distributor:1 storage:2 .0.s:d")),
+ initial_default(std::make_shared<lib::ClusterState>("distributor:1 storage:2 .0.s:m")),
+ initial_bundle(*initial_baseline, {{FixedBucketSpaces::default_space(), initial_default},
+ {FixedBucketSpaces::global_space(), initial_baseline}}),
+ default_bucket(FixedBucketSpaces::default_space(), BucketId(16, 1234)),
+ global_bucket(FixedBucketSpaces::global_space(), BucketId(16, 1234))
+ {
+ }
+ ~BucketDBUpdaterSnapshotTest() override;
+
+ void SetUp() override {
+ BucketDBUpdaterTest::SetUp();
+ getBucketDBUpdater().set_stale_reads_enabled(true);
+ };
+
+ // Assumes that the distributor owns all buckets, so it may choose any arbitrary bucket in the bucket space
+ uint32_t buckets_in_snapshot_matching_current_db(DistributorBucketSpaceRepo& repo, BucketSpace bucket_space) {
+ auto rs = getBucketDBUpdater().read_snapshot_for_bucket(Bucket(bucket_space, BucketId(16, 1234)));
+ if (!rs.is_routable()) {
+ return 0;
+ }
+ auto guard = rs.steal_read_guard();
+ uint32_t found_buckets = 0;
+ for_each_bucket(repo, [&](const auto& space, const auto& entry) {
+ if (space == bucket_space) {
+ std::vector<BucketDatabase::Entry> entries;
+ guard->find_parents_and_self(entry.getBucketId(), entries);
+ if (entries.size() == 1) {
+ ++found_buckets;
+ }
+ }
+ });
+ return found_buckets;
+ }
+};
+
+BucketDBUpdaterSnapshotTest::~BucketDBUpdaterSnapshotTest() = default;
+
+TEST_F(BucketDBUpdaterSnapshotTest, default_space_snapshot_prior_to_activated_state_is_non_routable) {
+ auto rs = getBucketDBUpdater().read_snapshot_for_bucket(default_bucket);
+ EXPECT_FALSE(rs.is_routable());
+}
+
+TEST_F(BucketDBUpdaterSnapshotTest, global_space_snapshot_prior_to_activated_state_is_non_routable) {
+ auto rs = getBucketDBUpdater().read_snapshot_for_bucket(global_bucket);
+ EXPECT_FALSE(rs.is_routable());
+}
+
+TEST_F(BucketDBUpdaterSnapshotTest, read_snapshot_returns_appropriate_cluster_states) {
+ set_cluster_state_bundle(initial_bundle);
+ // State currently pending, empty initial state is active
+
+ auto def_rs = getBucketDBUpdater().read_snapshot_for_bucket(default_bucket);
+ EXPECT_EQ(def_rs.context().active_cluster_state()->toString(), empty_state.toString());
+ EXPECT_EQ(def_rs.context().default_active_cluster_state()->toString(), empty_state.toString());
+ ASSERT_TRUE(def_rs.context().has_pending_state_transition());
+ EXPECT_EQ(def_rs.context().pending_cluster_state()->toString(), initial_default->toString());
+
+ auto global_rs = getBucketDBUpdater().read_snapshot_for_bucket(global_bucket);
+ EXPECT_EQ(global_rs.context().active_cluster_state()->toString(), empty_state.toString());
+ EXPECT_EQ(global_rs.context().default_active_cluster_state()->toString(), empty_state.toString());
+ ASSERT_TRUE(global_rs.context().has_pending_state_transition());
+ EXPECT_EQ(global_rs.context().pending_cluster_state()->toString(), initial_baseline->toString());
+
+ ASSERT_NO_FATAL_FAILURE(completeBucketInfoGathering(*initial_baseline, messageCount(1), 0));
+ // State now activated, no pending
+
+ def_rs = getBucketDBUpdater().read_snapshot_for_bucket(default_bucket);
+ EXPECT_EQ(def_rs.context().active_cluster_state()->toString(), initial_default->toString());
+ EXPECT_EQ(def_rs.context().default_active_cluster_state()->toString(), initial_default->toString());
+ EXPECT_FALSE(def_rs.context().has_pending_state_transition());
+
+ global_rs = getBucketDBUpdater().read_snapshot_for_bucket(global_bucket);
+ EXPECT_EQ(global_rs.context().active_cluster_state()->toString(), initial_baseline->toString());
+ EXPECT_EQ(global_rs.context().default_active_cluster_state()->toString(), initial_default->toString());
+ EXPECT_FALSE(global_rs.context().has_pending_state_transition());
+}
+
+TEST_F(BucketDBUpdaterSnapshotTest, snapshot_with_no_pending_state_transition_returns_mutable_db_guard) {
+ constexpr uint32_t n_buckets = 10;
+ ASSERT_NO_FATAL_FAILURE(
+ trigger_completed_but_not_yet_activated_transition("version:1 distributor:2 storage:4", 0, 4,
+ "version:2 distributor:1 storage:4", n_buckets, 4));
+ EXPECT_FALSE(activate_cluster_state_version(2));
+ EXPECT_EQ(buckets_in_snapshot_matching_current_db(mutable_repo(), FixedBucketSpaces::default_space()),
+ n_buckets);
+ EXPECT_EQ(buckets_in_snapshot_matching_current_db(mutable_repo(), FixedBucketSpaces::global_space()),
+ n_buckets);
+}
+
+TEST_F(BucketDBUpdaterSnapshotTest, snapshot_returns_unroutable_for_non_owned_bucket_in_current_state) {
+ ASSERT_NO_FATAL_FAILURE(
+ trigger_completed_but_not_yet_activated_transition("version:1 distributor:2 storage:4", 0, 4,
+ "version:2 distributor:2 .0.s:d storage:4", 0, 0));
+ EXPECT_FALSE(activate_cluster_state_version(2));
+ // We're down in state 2 and therefore do not own any buckets
+ auto def_rs = getBucketDBUpdater().read_snapshot_for_bucket(default_bucket);
+ EXPECT_FALSE(def_rs.is_routable());
+}
+
+TEST_F(BucketDBUpdaterSnapshotTest, snapshot_with_pending_state_returns_read_only_guard_for_bucket_only_owned_in_current_state) {
+ constexpr uint32_t n_buckets = 10;
+ ASSERT_NO_FATAL_FAILURE(
+ trigger_completed_but_not_yet_activated_transition("version:1 distributor:1 storage:4", n_buckets, 4,
+ "version:2 distributor:2 .0.s:d storage:4", 0, 0));
+ EXPECT_EQ(buckets_in_snapshot_matching_current_db(read_only_repo(), FixedBucketSpaces::default_space()),
+ n_buckets);
+ EXPECT_EQ(buckets_in_snapshot_matching_current_db(read_only_repo(), FixedBucketSpaces::global_space()),
+ n_buckets);
+}
+
+TEST_F(BucketDBUpdaterSnapshotTest, snapshot_is_unroutable_if_stale_reads_disabled_and_bucket_not_owned_in_pending_state) {
+ getBucketDBUpdater().set_stale_reads_enabled(false);
+ constexpr uint32_t n_buckets = 10;
+ ASSERT_NO_FATAL_FAILURE(
+ trigger_completed_but_not_yet_activated_transition("version:1 distributor:1 storage:4", n_buckets, 4,
+ "version:2 distributor:2 .0.s:d storage:4", 0, 0));
+ auto def_rs = getBucketDBUpdater().read_snapshot_for_bucket(default_bucket);
+ EXPECT_FALSE(def_rs.is_routable());
+}
+
}
diff --git a/storage/src/tests/distributor/distributortestutil.cpp b/storage/src/tests/distributor/distributortestutil.cpp
index 4a9ef147741..15820b64ff9 100644
--- a/storage/src/tests/distributor/distributortestutil.cpp
+++ b/storage/src/tests/distributor/distributortestutil.cpp
@@ -417,7 +417,8 @@ DistributorTestUtil::getBucketSpaces() const
void
DistributorTestUtil::enableDistributorClusterState(vespalib::stringref state)
{
- _distributor->enableClusterStateBundle(lib::ClusterStateBundle(lib::ClusterState(state)));
+ getBucketDBUpdater().simulate_cluster_state_bundle_activation(
+ lib::ClusterStateBundle(lib::ClusterState(state)));
}
}
diff --git a/storage/src/tests/distributor/externaloperationhandlertest.cpp b/storage/src/tests/distributor/externaloperationhandlertest.cpp
index 600e56faf31..84f7d34d069 100644
--- a/storage/src/tests/distributor/externaloperationhandlertest.cpp
+++ b/storage/src/tests/distributor/externaloperationhandlertest.cpp
@@ -505,6 +505,7 @@ document::BucketId ExternalOperationHandlerTest::set_up_pending_cluster_state_tr
std::string current = "version:123 distributor:2 storage:2";
std::string pending = "version:321 distributor:3 storage:3";
setupDistributor(1, 3, current);
+ getBucketDBUpdater().set_stale_reads_enabled(read_only_enabled);
getConfig().setAllowStaleReadsDuringClusterStateTransitions(read_only_enabled);
// Trigger pending cluster state
diff --git a/storage/src/tests/distributor/getoperationtest.cpp b/storage/src/tests/distributor/getoperationtest.cpp
index 7c308e152db..99d7c12551d 100644
--- a/storage/src/tests/distributor/getoperationtest.cpp
+++ b/storage/src/tests/distributor/getoperationtest.cpp
@@ -3,6 +3,8 @@
#include <vespa/config/helper/configgetter.h>
#include <vespa/document/config/config-documenttypes.h>
#include <vespa/document/repo/documenttyperepo.h>
+#include <vespa/storage/bucketdb/bucketdatabase.h>
+#include <vespa/storage/distributor/distributor_bucket_space.h>
#include <vespa/storage/distributor/externaloperationhandler.h>
#include <vespa/storage/distributor/distributor.h>
#include <vespa/storage/distributor/distributormetricsset.h>
@@ -31,7 +33,7 @@ struct GetOperationTest : Test, DistributorTestUtil {
std::unique_ptr<Operation> op;
GetOperationTest();
- ~GetOperationTest();
+ ~GetOperationTest() override;
void SetUp() override {
_repo.reset(
@@ -53,6 +55,7 @@ struct GetOperationTest : Test, DistributorTestUtil {
auto msg = std::make_shared<api::GetCommand>(makeDocumentBucket(document::BucketId(0)), docId, "[all]");
op = std::make_unique<GetOperation>(
getExternalOperationHandler(), getDistributorBucketSpace(),
+ getDistributorBucketSpace().getBucketDatabase().acquire_read_guard(),
msg, getDistributor().getMetrics(). gets[msg->getLoadType()]);
op->start(_sender, framework::MilliSecTime(0));
}
diff --git a/storage/src/vespa/storage/bucketdb/btree_bucket_database.cpp b/storage/src/vespa/storage/bucketdb/btree_bucket_database.cpp
index c3ade3c2877..66d44a655e0 100644
--- a/storage/src/vespa/storage/bucketdb/btree_bucket_database.cpp
+++ b/storage/src/vespa/storage/bucketdb/btree_bucket_database.cpp
@@ -148,7 +148,9 @@ Entry BTreeBucketDatabase::entry_from_iterator(const BTree::ConstIterator& iter)
if (!iter.valid()) {
return Entry::createInvalid();
}
- return entry_from_value(iter.getKey(), iter.getData());
+ const auto value = iter.getData();
+ std::atomic_thread_fence(std::memory_order_acquire);
+ return entry_from_value(iter.getKey(), value);
}
ConstEntryRef BTreeBucketDatabase::const_entry_ref_from_iterator(const BTree::ConstIterator& iter) const {
@@ -156,6 +158,7 @@ ConstEntryRef BTreeBucketDatabase::const_entry_ref_from_iterator(const BTree::Co
return ConstEntryRef::createInvalid();
}
const auto value = iter.getData();
+ std::atomic_thread_fence(std::memory_order_acquire);
const auto replicas_ref = _store.get(entry_ref_from_value(value));
const auto bucket = BucketId(BucketId::keyToBucketId(iter.getKey()));
return const_entry_ref_from_replica_array_ref(bucket, gc_timestamp_from_value(value), replicas_ref);
diff --git a/storage/src/vespa/storage/distributor/CMakeLists.txt b/storage/src/vespa/storage/distributor/CMakeLists.txt
index 8c701033e67..944df6e1708 100644
--- a/storage/src/vespa/storage/distributor/CMakeLists.txt
+++ b/storage/src/vespa/storage/distributor/CMakeLists.txt
@@ -7,6 +7,7 @@ vespa_add_library(storage_distributor
bucket_db_prune_elision.cpp
bucketgctimecalculator.cpp
bucketlistmerger.cpp
+ bucket_space_distribution_context.cpp
clusterinformation.cpp
distributor_bucket_space.cpp
distributor_bucket_space_repo.cpp
@@ -20,6 +21,7 @@ vespa_add_library(storage_distributor
idealstatemetricsset.cpp
messagetracker.cpp
nodeinfo.cpp
+ operation_routing_snapshot.cpp
operation_sequencer.cpp
operationowner.cpp
operationtargetresolver.cpp
diff --git a/storage/src/vespa/storage/distributor/bucket_space_distribution_context.cpp b/storage/src/vespa/storage/distributor/bucket_space_distribution_context.cpp
new file mode 100644
index 00000000000..53040bc42b1
--- /dev/null
+++ b/storage/src/vespa/storage/distributor/bucket_space_distribution_context.cpp
@@ -0,0 +1,81 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include "bucket_space_distribution_context.h"
+
+namespace storage::distributor {
+
+BucketSpaceDistributionContext::~BucketSpaceDistributionContext() = default;
+
+BucketSpaceDistributionContext::BucketSpaceDistributionContext(
+ std::shared_ptr<const lib::ClusterState> active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> default_active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> pending_cluster_state,
+ std::shared_ptr<const lib::Distribution> distribution,
+ uint16_t this_node_index)
+ : _active_cluster_state(std::move(active_cluster_state)),
+ _default_active_cluster_state(std::move(default_active_cluster_state)),
+ _pending_cluster_state(std::move(pending_cluster_state)),
+ _distribution(std::move(distribution)),
+ _this_node_index(this_node_index)
+{}
+
+std::shared_ptr<BucketSpaceDistributionContext> BucketSpaceDistributionContext::make_state_transition(
+ std::shared_ptr<const lib::ClusterState> active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> default_active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> pending_cluster_state,
+ std::shared_ptr<const lib::Distribution> distribution,
+ uint16_t this_node_index)
+{
+ return std::make_shared<BucketSpaceDistributionContext>(
+ std::move(active_cluster_state), std::move(default_active_cluster_state),
+ std::move(pending_cluster_state), std::move(distribution),
+ this_node_index);
+}
+
+std::shared_ptr<BucketSpaceDistributionContext> BucketSpaceDistributionContext::make_stable_state(
+ std::shared_ptr<const lib::ClusterState> active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> default_active_cluster_state,
+ std::shared_ptr<const lib::Distribution> distribution,
+ uint16_t this_node_index)
+{
+ return std::make_shared<BucketSpaceDistributionContext>(
+ std::move(active_cluster_state), std::move(default_active_cluster_state),
+ std::shared_ptr<const lib::ClusterState>(),
+ std::move(distribution), this_node_index);
+}
+
+std::shared_ptr<BucketSpaceDistributionContext>
+BucketSpaceDistributionContext::make_not_yet_initialized(uint16_t this_node_index)
+{
+ return std::make_shared<BucketSpaceDistributionContext>(
+ std::make_shared<const lib::ClusterState>(),
+ std::make_shared<const lib::ClusterState>(),
+ std::shared_ptr<const lib::ClusterState>(),
+ std::make_shared<const lib::Distribution>(),
+ this_node_index);
+}
+
+bool BucketSpaceDistributionContext::bucket_owned_in_state(const lib::ClusterState& state,
+ const document::BucketId& id) const
+{
+ try {
+ uint16_t owner_idx = _distribution->getIdealDistributorNode(state, id);
+ return (owner_idx == _this_node_index);
+ } catch (lib::TooFewBucketBitsInUseException&) {
+ return false;
+ } catch (lib::NoDistributorsAvailableException&) {
+ return false;
+ }
+}
+
+bool BucketSpaceDistributionContext::bucket_owned_in_active_state(const document::BucketId& id) const {
+ return bucket_owned_in_state(*_active_cluster_state, id);
+}
+
+bool BucketSpaceDistributionContext::bucket_owned_in_pending_state(const document::BucketId& id) const {
+ if (_pending_cluster_state) {
+ return bucket_owned_in_state(*_pending_cluster_state, id);
+ }
+ return true; // No pending state, owned by default.
+}
+
+}
diff --git a/storage/src/vespa/storage/distributor/bucket_space_distribution_context.h b/storage/src/vespa/storage/distributor/bucket_space_distribution_context.h
new file mode 100644
index 00000000000..7a9c0fcae60
--- /dev/null
+++ b/storage/src/vespa/storage/distributor/bucket_space_distribution_context.h
@@ -0,0 +1,70 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/vdslib/distribution/distribution.h>
+#include <vespa/vdslib/state/clusterstate.h>
+#include <memory>
+#include <cstdint>
+
+namespace storage::distributor {
+
+/**
+ * Represents a consistent snapshot of cluster state and distribution config
+ * information at a particular point in time. This is sufficient to compute
+ * bucket ownership and distributions for the bucket space associated with
+ * the context.
+ *
+ * Since this is a snapshot in time, the context is immutable once created.
+ */
+class BucketSpaceDistributionContext {
+ std::shared_ptr<const lib::ClusterState> _active_cluster_state;
+ std::shared_ptr<const lib::ClusterState> _default_active_cluster_state;
+ std::shared_ptr<const lib::ClusterState> _pending_cluster_state; // May be null if no state is pending
+ std::shared_ptr<const lib::Distribution> _distribution; // TODO ideally should have a pending distribution as well
+ uint16_t _this_node_index;
+public:
+ BucketSpaceDistributionContext() = delete;
+ // Public due to make_shared, prefer factory functions to instantiate instead.
+ BucketSpaceDistributionContext(std::shared_ptr<const lib::ClusterState> active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> default_active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> pending_cluster_state,
+ std::shared_ptr<const lib::Distribution> distribution,
+ uint16_t this_node_index);
+ ~BucketSpaceDistributionContext();
+
+ static std::shared_ptr<BucketSpaceDistributionContext> make_state_transition(
+ std::shared_ptr<const lib::ClusterState> active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> default_active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> pending_cluster_state,
+ std::shared_ptr<const lib::Distribution> distribution,
+ uint16_t this_node_index);
+ static std::shared_ptr<BucketSpaceDistributionContext> make_stable_state(
+ std::shared_ptr<const lib::ClusterState> active_cluster_state,
+ std::shared_ptr<const lib::ClusterState> default_active_cluster_state,
+ std::shared_ptr<const lib::Distribution> distribution,
+ uint16_t this_node_index);
+ static std::shared_ptr<BucketSpaceDistributionContext> make_not_yet_initialized(uint16_t this_node_index);
+
+ const std::shared_ptr<const lib::ClusterState>& active_cluster_state() const noexcept {
+ return _active_cluster_state;
+ }
+
+ const std::shared_ptr<const lib::ClusterState>& default_active_cluster_state() const noexcept {
+ return _default_active_cluster_state;
+ }
+ bool has_pending_state_transition() const noexcept {
+ return (_pending_cluster_state.get() != nullptr);
+ }
+ // Returned shared_ptr is nullptr iff has_pending_state_transition() == false.
+ const std::shared_ptr<const lib::ClusterState>& pending_cluster_state() const noexcept {
+ return _pending_cluster_state;
+ }
+
+ bool bucket_owned_in_state(const lib::ClusterState& state, const document::BucketId& id) const;
+ bool bucket_owned_in_active_state(const document::BucketId& id) const;
+ bool bucket_owned_in_pending_state(const document::BucketId& id) const;
+
+ uint16_t this_node_index() const noexcept { return _this_node_index; }
+};
+
+}
diff --git a/storage/src/vespa/storage/distributor/bucketdbupdater.cpp b/storage/src/vespa/storage/distributor/bucketdbupdater.cpp
index a901ac28a54..227165a0911 100644
--- a/storage/src/vespa/storage/distributor/bucketdbupdater.cpp
+++ b/storage/src/vespa/storage/distributor/bucketdbupdater.cpp
@@ -2,6 +2,7 @@
#include "bucketdbupdater.h"
#include "bucket_db_prune_elision.h"
+#include "bucket_space_distribution_context.h"
#include "distributor.h"
#include "distributor_bucket_space.h"
#include "distributormetricsset.h"
@@ -30,12 +31,44 @@ BucketDBUpdater::BucketDBUpdater(Distributor& owner,
: framework::StatusReporter("bucketdb", "Bucket DB Updater"),
_distributorComponent(owner, bucketSpaceRepo, readOnlyBucketSpaceRepo, compReg, "Bucket DB Updater"),
_sender(sender),
- _transitionTimer(_distributorComponent.getClock())
+ _transitionTimer(_distributorComponent.getClock()),
+ _active_distribution_contexts(),
+ _distribution_context_mutex()
{
+ for (auto& elem : _distributorComponent.getBucketSpaceRepo()) {
+ _active_distribution_contexts.emplace(
+ elem.first,
+ BucketSpaceDistributionContext::make_not_yet_initialized(_distributorComponent.getIndex()));
+ _explicit_transition_read_guard.emplace(elem.first, std::shared_ptr<BucketDatabase::ReadGuard>());
+ }
}
BucketDBUpdater::~BucketDBUpdater() = default;
+OperationRoutingSnapshot BucketDBUpdater::read_snapshot_for_bucket(const document::Bucket& bucket) const {
+ const auto bucket_space = bucket.getBucketSpace();
+ std::lock_guard lock(_distribution_context_mutex);
+ auto active_state_iter = _active_distribution_contexts.find(bucket_space);
+ assert(active_state_iter != _active_distribution_contexts.cend());
+ auto& state = *active_state_iter->second;
+ if (!state.bucket_owned_in_active_state(bucket.getBucketId())) {
+ return OperationRoutingSnapshot::make_not_routable_in_state(active_state_iter->second);
+ }
+ const bool bucket_present_in_mutable_db = state.bucket_owned_in_pending_state(bucket.getBucketId());
+ if (!bucket_present_in_mutable_db && !stale_reads_enabled()) {
+ return OperationRoutingSnapshot::make_not_routable_in_state(active_state_iter->second);
+ }
+ const auto& space_repo = bucket_present_in_mutable_db
+ ? _distributorComponent.getBucketSpaceRepo()
+ : _distributorComponent.getReadOnlyBucketSpaceRepo();
+ auto existing_guard_iter = _explicit_transition_read_guard.find(bucket_space);
+ assert(existing_guard_iter != _explicit_transition_read_guard.cend());
+ auto db_guard = existing_guard_iter->second
+ ? existing_guard_iter-> second
+ : space_repo.get(bucket_space).getBucketDatabase().acquire_read_guard();
+ return OperationRoutingSnapshot::make_routable_with_guard(active_state_iter->second, std::move(db_guard), space_repo);
+}
+
void
BucketDBUpdater::flush()
{
@@ -59,8 +92,7 @@ BucketDBUpdater::print(std::ostream& out, bool verbose, const std::string& inden
bool
BucketDBUpdater::shouldDeferStateEnabling() const noexcept
{
- return _distributorComponent.getDistributor().getConfig()
- .allowStaleReadsDuringClusterStateTransitions();
+ return stale_reads_enabled();
}
bool
@@ -258,6 +290,61 @@ BucketDBUpdater::replyToActivationWithActualVersion(
_distributorComponent.sendUp(reply); // TODO let API accept rvalues
}
+void BucketDBUpdater::update_read_snapshot_before_db_pruning() {
+ std::lock_guard lock(_distribution_context_mutex);
+ for (auto& elem : _distributorComponent.getBucketSpaceRepo()) {
+ // At this point, we're still operating with a distribution context _without_ a
+ // pending state, i.e. anyone using the context will expect to find buckets
+ // in the DB that correspond to how the database looked like prior to pruning
+ // buckets from the DB. To ensure this is not violated, take a snapshot of the
+ // _mutable_ DB and expose this. This snapshot only lives until we atomically
+ // flip to expose a distribution context that includes the new, pending state.
+ // At that point, the read-only DB is known to contain the buckets that have
+ // been pruned away, so we can release the mutable DB snapshot safely.
+ // TODO test for, and handle, state preemption case!
+ _explicit_transition_read_guard[elem.first] = elem.second->getBucketDatabase().acquire_read_guard();
+ }
+}
+
+
+void BucketDBUpdater::update_read_snapshot_after_db_pruning(const lib::ClusterStateBundle& new_state) {
+ std::lock_guard lock(_distribution_context_mutex);
+ const auto old_default_state = _distributorComponent.getBucketSpaceRepo().get(
+ document::FixedBucketSpaces::default_space()).cluster_state_sp();
+ for (auto& elem : _distributorComponent.getBucketSpaceRepo()) {
+ auto new_distribution = elem.second->distribution_sp();
+ auto old_cluster_state = elem.second->cluster_state_sp();
+ auto new_cluster_state = new_state.getDerivedClusterState(elem.first);
+ _active_distribution_contexts.insert_or_assign(
+ elem.first,
+ BucketSpaceDistributionContext::make_state_transition(
+ std::move(old_cluster_state),
+ old_default_state,
+ std::move(new_cluster_state),
+ std::move(new_distribution),
+ _distributorComponent.getIndex()));
+ // We can now remove the explicit mutable DB snapshot, as the buckets that have been
+ // pruned away are visible in the read-only DB.
+ _explicit_transition_read_guard[elem.first] = std::shared_ptr<BucketDatabase::ReadGuard>();
+ }
+}
+
+void BucketDBUpdater::update_read_snapshot_after_activation(const lib::ClusterStateBundle& activated_state) {
+ std::lock_guard lock(_distribution_context_mutex);
+ const auto& default_cluster_state = activated_state.getDerivedClusterState(document::FixedBucketSpaces::default_space());
+ for (auto& elem : _distributorComponent.getBucketSpaceRepo()) {
+ auto new_distribution = elem.second->distribution_sp();
+ auto new_cluster_state = activated_state.getDerivedClusterState(elem.first);
+ _active_distribution_contexts.insert_or_assign(
+ elem.first,
+ BucketSpaceDistributionContext::make_stable_state(
+ std::move(new_cluster_state),
+ default_cluster_state,
+ std::move(new_distribution),
+ _distributorComponent.getIndex()));
+ }
+}
+
bool
BucketDBUpdater::onSetSystemState(
const std::shared_ptr<api::SetSystemStateCommand>& cmd)
@@ -275,8 +362,10 @@ BucketDBUpdater::onSetSystemState(
ensureTransitionTimerStarted();
// Separate timer since _transitionTimer might span multiple pending states.
framework::MilliSecTimer process_timer(_distributorComponent.getClock());
-
- removeSuperfluousBuckets(cmd->getClusterStateBundle(), false);
+ update_read_snapshot_before_db_pruning();
+ const auto& bundle = cmd->getClusterStateBundle();
+ removeSuperfluousBuckets(bundle, false);
+ update_read_snapshot_after_db_pruning(bundle);
replyToPreviousPendingClusterStateIfAny();
ClusterInformation::CSP clusterInfo(
@@ -642,6 +731,7 @@ BucketDBUpdater::activatePendingClusterState()
_distributorComponent.getDistributor().notifyDistributionChangeEnabled();
}
+ update_read_snapshot_after_activation(_pendingClusterState->getNewClusterStateBundle());
_pendingClusterState.reset();
_outdatedNodesMap.clear();
sendAllQueuedBucketRechecks();
@@ -665,6 +755,11 @@ BucketDBUpdater::enableCurrentClusterStateBundleInDistributor()
_distributorComponent.getDistributor().enableClusterStateBundle(state);
}
+void BucketDBUpdater::simulate_cluster_state_bundle_activation(const lib::ClusterStateBundle& activated_state) {
+ update_read_snapshot_after_activation(activated_state);
+ _distributorComponent.getDistributor().enableClusterStateBundle(activated_state);
+}
+
void
BucketDBUpdater::addCurrentStateToClusterStateHistory()
{
diff --git a/storage/src/vespa/storage/distributor/bucketdbupdater.h b/storage/src/vespa/storage/distributor/bucketdbupdater.h
index e69d328d8bc..86ceab14486 100644
--- a/storage/src/vespa/storage/distributor/bucketdbupdater.h
+++ b/storage/src/vespa/storage/distributor/bucketdbupdater.h
@@ -6,6 +6,7 @@
#include "distributorcomponent.h"
#include "distributormessagesender.h"
#include "pendingclusterstate.h"
+#include "operation_routing_snapshot.h"
#include "outdated_nodes_map.h"
#include <vespa/document/bucket/bucket.h>
#include <vespa/storageapi/messageapi/returncode.h>
@@ -15,7 +16,9 @@
#include <vespa/storageframework/generic/clock/timer.h>
#include <vespa/storageframework/generic/status/statusreporter.h>
#include <vespa/storageapi/messageapi/messagehandler.h>
+#include <atomic>
#include <list>
+#include <mutex>
namespace vespalib::xml {
class XmlOutputStream;
@@ -25,6 +28,7 @@ class XmlAttribute;
namespace storage::distributor {
class Distributor;
+class BucketSpaceDistributionContext;
class BucketDBUpdater : public framework::StatusReporter,
public api::MessageHandler
@@ -70,7 +74,14 @@ public:
return ((_pendingClusterState.get() != nullptr)
&& _pendingClusterState->hasBucketOwnershipTransfer());
}
+ void set_stale_reads_enabled(bool enabled) noexcept {
+ _stale_reads_enabled.store(enabled, std::memory_order_relaxed);
+ }
+ bool stale_reads_enabled() const noexcept {
+ return _stale_reads_enabled.load(std::memory_order_relaxed);
+ }
+ OperationRoutingSnapshot read_snapshot_for_bucket(const document::Bucket&) const;
private:
DistributorComponent _distributorComponent;
class MergeReplyGuard {
@@ -129,6 +140,12 @@ private:
}
};
+ friend class DistributorTestUtil;
+ // Only to be used by tests that want to ensure both the BucketDBUpdater _and_ the Distributor
+ // components agree on the currently active cluster state bundle.
+ // Transitively invokes Distributor::enableClusterStateBundle
+ void simulate_cluster_state_bundle_activation(const lib::ClusterStateBundle& activated_state);
+
bool shouldDeferStateEnabling() const noexcept;
bool hasPendingClusterState() const;
bool pendingClusterStateAccepted(const std::shared_ptr<api::RequestBucketInfoReply>& repl);
@@ -166,8 +183,11 @@ private:
void updateState(const lib::ClusterState& oldState, const lib::ClusterState& newState);
+ void update_read_snapshot_before_db_pruning();
void removeSuperfluousBuckets(const lib::ClusterStateBundle& newState,
bool is_distribution_config_change);
+ void update_read_snapshot_after_db_pruning(const lib::ClusterStateBundle& new_state);
+ void update_read_snapshot_after_activation(const lib::ClusterStateBundle& activated_state);
void replyToPreviousPendingClusterStateIfAny();
void replyToActivationWithActualVersion(
@@ -182,9 +202,6 @@ private:
void maybe_inject_simulated_db_pruning_delay();
void maybe_inject_simulated_db_merging_delay();
- friend class BucketDBUpdater_Test;
- friend class MergeOperation_Test;
-
/**
Removes all copies of buckets that are on nodes that are down.
*/
@@ -235,6 +252,16 @@ private:
std::set<EnqueuedBucketRecheck> _enqueuedRechecks;
OutdatedNodesMap _outdatedNodesMap;
framework::MilliSecTimer _transitionTimer;
+ std::atomic<bool> _stale_reads_enabled;
+ using DistributionContexts = std::unordered_map<document::BucketSpace,
+ std::shared_ptr<BucketSpaceDistributionContext>,
+ document::BucketSpace::hash>;
+ DistributionContexts _active_distribution_contexts;
+ using DbGuards = std::unordered_map<document::BucketSpace,
+ std::shared_ptr<BucketDatabase::ReadGuard>,
+ document::BucketSpace::hash>;
+ DbGuards _explicit_transition_read_guard;
+ mutable std::mutex _distribution_context_mutex;
};
}
diff --git a/storage/src/vespa/storage/distributor/distributor.cpp b/storage/src/vespa/storage/distributor/distributor.cpp
index 69b64ac8dc1..4adbdd32669 100644
--- a/storage/src/vespa/storage/distributor/distributor.cpp
+++ b/storage/src/vespa/storage/distributor/distributor.cpp
@@ -77,7 +77,8 @@ Distributor::Distributor(DistributorComponentRegister& compReg,
_distributorStatusDelegate(compReg, *this, *this),
_bucketDBStatusDelegate(compReg, *this, _bucketDBUpdater),
_idealStateManager(*this, *_bucketSpaceRepo, *_readOnlyBucketSpaceRepo, compReg, manageActiveBucketCopies),
- _externalOperationHandler(*this, *_bucketSpaceRepo, *_readOnlyBucketSpaceRepo, _idealStateManager, compReg),
+ _externalOperationHandler(*this, *_bucketSpaceRepo, *_readOnlyBucketSpaceRepo,
+ _idealStateManager, compReg, use_btree_database),
_threadPool(threadPool),
_initializingIsUp(true),
_doneInitializeHandler(doneInitHandler),
@@ -322,9 +323,7 @@ bool
Distributor::handleMessage(const std::shared_ptr<api::StorageMessage>& msg)
{
if (msg->getType().isReply()) {
- std::shared_ptr<api::StorageReply> reply =
- std::dynamic_pointer_cast<api::StorageReply>(msg);
-
+ auto reply = std::dynamic_pointer_cast<api::StorageReply>(msg);
if (handleReply(reply)) {
return true;
}
@@ -400,6 +399,10 @@ Distributor::enableClusterStateBundle(const lib::ClusterStateBundle& state)
}
}
+OperationRoutingSnapshot Distributor::read_snapshot_for_bucket(const document::Bucket& bucket) const {
+ return _bucketDBUpdater.read_snapshot_for_bucket(bucket);
+}
+
void
Distributor::notifyDistributionChangeEnabled()
{
@@ -834,6 +837,7 @@ Distributor::enableNextConfig()
_bucketDBMetricUpdater.setMinimumReplicaCountingMode(getConfig().getMinimumReplicaCountingMode());
_ownershipSafeTimeCalc->setMaxClusterClockSkew(getConfig().getMaxClusterClockSkew());
_pendingMessageTracker.setNodeBusyDuration(getConfig().getInhibitMergesOnBusyNodeDuration());
+ _bucketDBUpdater.set_stale_reads_enabled(getConfig().allowStaleReadsDuringClusterStateTransitions());
}
void
diff --git a/storage/src/vespa/storage/distributor/distributor.h b/storage/src/vespa/storage/distributor/distributor.h
index 638704adf24..48d9145eec7 100644
--- a/storage/src/vespa/storage/distributor/distributor.h
+++ b/storage/src/vespa/storage/distributor/distributor.h
@@ -170,6 +170,8 @@ public:
return *_readOnlyBucketSpaceRepo;
}
+ OperationRoutingSnapshot read_snapshot_for_bucket(const document::Bucket&) const override;
+
class Status;
class MetricUpdateHook : public framework::MetricUpdateHook
{
diff --git a/storage/src/vespa/storage/distributor/distributor_bucket_space.h b/storage/src/vespa/storage/distributor/distributor_bucket_space.h
index 26a0ee9098c..8fbb99dfe89 100644
--- a/storage/src/vespa/storage/distributor/distributor_bucket_space.h
+++ b/storage/src/vespa/storage/distributor/distributor_bucket_space.h
@@ -48,6 +48,9 @@ public:
void setClusterState(std::shared_ptr<const lib::ClusterState> clusterState);
const lib::ClusterState &getClusterState() const noexcept { return *_clusterState; }
+ const std::shared_ptr<const lib::ClusterState>& cluster_state_sp() const noexcept {
+ return _clusterState;
+ }
void setDistribution(std::shared_ptr<const lib::Distribution> distribution);
@@ -55,6 +58,9 @@ public:
const lib::Distribution& getDistribution() const noexcept {
return *_distribution;
}
+ const std::shared_ptr<const lib::Distribution>& distribution_sp() const noexcept {
+ return _distribution;
+ }
};
diff --git a/storage/src/vespa/storage/distributor/distributorinterface.h b/storage/src/vespa/storage/distributor/distributorinterface.h
index d9f037bb8f1..aba58e112dc 100644
--- a/storage/src/vespa/storage/distributor/distributorinterface.h
+++ b/storage/src/vespa/storage/distributor/distributorinterface.h
@@ -4,6 +4,7 @@
#include "bucketgctimecalculator.h"
#include "distributormessagesender.h"
#include "bucketownership.h"
+#include "operation_routing_snapshot.h"
#include <vespa/storage/bucketdb/bucketdatabase.h>
#include <vespa/document/bucket/bucket.h>
@@ -49,6 +50,8 @@ public:
*/
virtual const lib::ClusterStateBundle& getClusterStateBundle() const = 0;
+ virtual OperationRoutingSnapshot read_snapshot_for_bucket(const document::Bucket&) const = 0;
+
/**
* Returns true if the node is currently initializing.
*/
diff --git a/storage/src/vespa/storage/distributor/externaloperationhandler.cpp b/storage/src/vespa/storage/distributor/externaloperationhandler.cpp
index 1b88f02cac6..221c516a56e 100644
--- a/storage/src/vespa/storage/distributor/externaloperationhandler.cpp
+++ b/storage/src/vespa/storage/distributor/externaloperationhandler.cpp
@@ -1,5 +1,6 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include "bucket_space_distribution_context.h"
#include "externaloperationhandler.h"
#include "distributor.h"
#include <vespa/document/base/documentid.h>
@@ -12,7 +13,6 @@
#include <vespa/storage/distributor/operations/external/statbucketlistoperation.h>
#include <vespa/storage/distributor/operations/external/removelocationoperation.h>
#include <vespa/storage/distributor/operations/external/visitoroperation.h>
-#include <vespa/document/util/stringutil.h>
#include <vespa/storageapi/message/persistence.h>
#include <vespa/storageapi/message/removelocation.h>
#include <vespa/storageapi/message/stat.h>
@@ -30,11 +30,16 @@ ExternalOperationHandler::ExternalOperationHandler(Distributor& owner,
DistributorBucketSpaceRepo& bucketSpaceRepo,
DistributorBucketSpaceRepo& readOnlyBucketSpaceRepo,
const MaintenanceOperationGenerator& gen,
- DistributorComponentRegister& compReg)
+ DistributorComponentRegister& compReg,
+ bool enable_concurrent_gets)
: DistributorComponent(owner, bucketSpaceRepo, readOnlyBucketSpaceRepo, compReg, "External operation handler"),
_operationGenerator(gen),
- _rejectFeedBeforeTimeReached() // At epoch
-{ }
+ _rejectFeedBeforeTimeReached(), // At epoch
+ _non_main_thread_ops_mutex(),
+ _non_main_thread_ops_owner(owner, getClock()),
+ _enable_concurrent_gets(enable_concurrent_gets)
+{
+}
ExternalOperationHandler::~ExternalOperationHandler() = default;
@@ -78,24 +83,32 @@ void ExternalOperationHandler::bounce_with_result(api::StorageCommand& cmd, cons
sendUp(std::shared_ptr<api::StorageMessage>(reply.release()));
}
-void ExternalOperationHandler::bounce_with_wrong_distribution(api::StorageCommand& cmd) {
+void ExternalOperationHandler::bounce_with_wrong_distribution(api::StorageCommand& cmd,
+ const lib::ClusterState& cluster_state)
+{
// Distributor ownership is equal across bucket spaces, so always send back default space state.
// This also helps client avoid getting confused by possibly observing different actual
// (derived) state strings for global/non-global document types for the same state version.
// Similarly, if we've yet to activate any version at all we send back BUSY instead
// of a suspiciously empty WrongDistributionReply.
// TOOD consider NOT_READY instead of BUSY once we're sure this won't cause any other issues.
- const auto& cluster_state = _bucketSpaceRepo.get(document::FixedBucketSpaces::default_space()).getClusterState();
if (cluster_state.getVersion() != 0) {
auto cluster_state_str = cluster_state.toString();
- LOG(debug, "Got message with wrong distribution, sending back state '%s'", cluster_state_str.c_str());
+ LOG(debug, "Got %s with wrong distribution, sending back state '%s'",
+ cmd.toString().c_str(), cluster_state_str.c_str());
bounce_with_result(cmd, api::ReturnCode(api::ReturnCode::WRONG_DISTRIBUTION, cluster_state_str));
} else { // Only valid for empty startup state
- LOG(debug, "Got message with wrong distribution, but no cluster state activated yet. Sending back BUSY");
+ LOG(debug, "Got %s with wrong distribution, but no cluster state activated yet. Sending back BUSY",
+ cmd.toString().c_str());
bounce_with_result(cmd, api::ReturnCode(api::ReturnCode::BUSY, "No cluster state activated yet"));
}
}
+void ExternalOperationHandler::bounce_with_wrong_distribution(api::StorageCommand& cmd) {
+ const auto& cluster_state = _bucketSpaceRepo.get(document::FixedBucketSpaces::default_space()).getClusterState();
+ bounce_with_wrong_distribution(cmd, cluster_state);
+}
+
void ExternalOperationHandler::bounce_with_busy_during_state_transition(
api::StorageCommand& cmd,
const lib::ClusterState& current_state,
@@ -283,10 +296,23 @@ IMPL_MSG_COMMAND_H(ExternalOperationHandler, Get)
{
document::Bucket bucket(cmd->getBucket().getBucketSpace(), getBucketId(cmd->getDocumentId()));
auto& metrics = getMetrics().gets[cmd->getLoadType()];
- bounce_or_invoke_read_only_op(*cmd, bucket, metrics, [&](auto& bucket_space_repo) {
- _op = std::make_shared<GetOperation>(*this, bucket_space_repo.get(cmd->getBucket().getBucketSpace()),
- cmd, metrics);
- });
+ auto snapshot = getDistributor().read_snapshot_for_bucket(bucket);
+ if (!snapshot.is_routable()) {
+ const auto& ctx = snapshot.context();
+ if (ctx.has_pending_state_transition()) {
+ bounce_with_busy_during_state_transition(*cmd, *ctx.default_active_cluster_state(),
+ *ctx.pending_cluster_state());
+ } else {
+ bounce_with_wrong_distribution(*cmd, *snapshot.context().default_active_cluster_state());
+ metrics.failures.wrongdistributor.inc(); // TODO thread safety for updates
+ }
+ return true;
+ }
+ // The snapshot is aware of whether stale reads are enabled, so we don't have to check that here.
+ const auto* space_repo = snapshot.bucket_space_repo();
+ assert(space_repo != nullptr);
+ _op = std::make_shared<GetOperation>(*this, space_repo->get(bucket.getBucketSpace()),
+ snapshot.steal_read_guard(), cmd, metrics);
return true;
}
diff --git a/storage/src/vespa/storage/distributor/externaloperationhandler.h b/storage/src/vespa/storage/distributor/externaloperationhandler.h
index 655feb5d00c..9db078af198 100644
--- a/storage/src/vespa/storage/distributor/externaloperationhandler.h
+++ b/storage/src/vespa/storage/distributor/externaloperationhandler.h
@@ -8,6 +8,7 @@
#include <vespa/storage/distributor/distributorcomponent.h>
#include <vespa/storageapi/messageapi/messagehandler.h>
#include <chrono>
+#include <mutex>
namespace storage {
@@ -39,7 +40,8 @@ public:
DistributorBucketSpaceRepo& bucketSpaceRepo,
DistributorBucketSpaceRepo& readOnlyBucketSpaceRepo,
const MaintenanceOperationGenerator&,
- DistributorComponentRegister& compReg);
+ DistributorComponentRegister& compReg,
+ bool enable_concurrent_gets);
~ExternalOperationHandler() override;
@@ -55,6 +57,9 @@ private:
OperationSequencer _mutationSequencer;
Operation::SP _op;
TimePoint _rejectFeedBeforeTimeReached;
+ mutable std::mutex _non_main_thread_ops_mutex;
+ OperationOwner _non_main_thread_ops_owner;
+ bool _enable_concurrent_gets;
template <typename Func>
void bounce_or_invoke_read_only_op(api::StorageCommand& cmd,
@@ -62,6 +67,8 @@ private:
PersistenceOperationMetricSet& metrics,
Func f);
+ void bounce_with_wrong_distribution(api::StorageCommand& cmd, const lib::ClusterState& cluster_state);
+ // Bounce with the current _default_ space cluster state.
void bounce_with_wrong_distribution(api::StorageCommand& cmd);
void bounce_with_busy_during_state_transition(api::StorageCommand& cmd,
const lib::ClusterState& current_state,
diff --git a/storage/src/vespa/storage/distributor/operation_routing_snapshot.cpp b/storage/src/vespa/storage/distributor/operation_routing_snapshot.cpp
new file mode 100644
index 00000000000..ec97e51b66d
--- /dev/null
+++ b/storage/src/vespa/storage/distributor/operation_routing_snapshot.cpp
@@ -0,0 +1,30 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include "operation_routing_snapshot.h"
+
+namespace storage::distributor {
+
+OperationRoutingSnapshot::OperationRoutingSnapshot(std::shared_ptr<const BucketSpaceDistributionContext> context,
+ std::shared_ptr<BucketDatabase::ReadGuard> read_guard,
+ const DistributorBucketSpaceRepo* bucket_space_repo)
+ : _context(std::move(context)),
+ _read_guard(std::move(read_guard)),
+ _bucket_space_repo(bucket_space_repo)
+{}
+
+OperationRoutingSnapshot::~OperationRoutingSnapshot() = default;
+
+OperationRoutingSnapshot OperationRoutingSnapshot::make_not_routable_in_state(
+ std::shared_ptr<const BucketSpaceDistributionContext> context)
+{
+ return OperationRoutingSnapshot(std::move(context), std::shared_ptr<BucketDatabase::ReadGuard>(), nullptr);
+}
+
+OperationRoutingSnapshot OperationRoutingSnapshot::make_routable_with_guard(
+ std::shared_ptr<const BucketSpaceDistributionContext> context,
+ std::shared_ptr<BucketDatabase::ReadGuard> read_guard,
+ const DistributorBucketSpaceRepo& bucket_space_repo)
+{
+ return OperationRoutingSnapshot(std::move(context), std::move(read_guard), &bucket_space_repo);
+}
+
+}
diff --git a/storage/src/vespa/storage/distributor/operation_routing_snapshot.h b/storage/src/vespa/storage/distributor/operation_routing_snapshot.h
new file mode 100644
index 00000000000..16ec8fef1c7
--- /dev/null
+++ b/storage/src/vespa/storage/distributor/operation_routing_snapshot.h
@@ -0,0 +1,60 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/storage/bucketdb/bucketdatabase.h>
+#include <memory>
+
+namespace storage::distributor {
+
+class BucketSpaceDistributionContext;
+class DistributorBucketSpaceRepo;
+
+/**
+ * An "operation routing snapshot" is intended to provide a stable means of computing
+ * bucket routing targets and performing database lookups for a particular bucket space
+ * in a potentially multi-threaded setting. When using multiple threads, both the current
+ * cluster/distribution state as well as the underlying bucket database may change
+ * independent of each other when observed from any other thread than the main distributor
+ * thread. Additionally, the bucket management system may operate with separate read-only
+ * databases during state transitions, complicating things further.
+ *
+ * By using an OperationRoutingSnapshot, a caller gets a consistent view of the world
+ * that stays valid throughout the operation's life time.
+ *
+ * Note that holding the DB read guard should be done for as short a time as possible to
+ * avoid elevated memory usage caused by data stores not being able to free on-hold items.
+ */
+class OperationRoutingSnapshot {
+ std::shared_ptr<const BucketSpaceDistributionContext> _context;
+ std::shared_ptr<BucketDatabase::ReadGuard> _read_guard;
+ const DistributorBucketSpaceRepo* _bucket_space_repo;
+public:
+ OperationRoutingSnapshot(std::shared_ptr<const BucketSpaceDistributionContext> context,
+ std::shared_ptr<BucketDatabase::ReadGuard> read_guard,
+ const DistributorBucketSpaceRepo* bucket_space_repo);
+
+ static OperationRoutingSnapshot make_not_routable_in_state(std::shared_ptr<const BucketSpaceDistributionContext> context);
+ static OperationRoutingSnapshot make_routable_with_guard(std::shared_ptr<const BucketSpaceDistributionContext> context,
+ std::shared_ptr<BucketDatabase::ReadGuard> read_guard,
+ const DistributorBucketSpaceRepo& bucket_space_repo);
+
+ OperationRoutingSnapshot(const OperationRoutingSnapshot&) noexcept = default;
+ OperationRoutingSnapshot& operator=(const OperationRoutingSnapshot&) noexcept = default;
+ OperationRoutingSnapshot(OperationRoutingSnapshot&&) noexcept = default;
+ OperationRoutingSnapshot& operator=(OperationRoutingSnapshot&&) noexcept = default;
+
+ ~OperationRoutingSnapshot();
+
+ const BucketSpaceDistributionContext& context() const noexcept { return *_context; }
+ std::shared_ptr<BucketDatabase::ReadGuard> steal_read_guard() noexcept {
+ return std::move(_read_guard);
+ }
+ bool is_routable() const noexcept {
+ return (_read_guard.get() != nullptr);
+ }
+ const DistributorBucketSpaceRepo* bucket_space_repo() const noexcept {
+ return _bucket_space_repo;
+ }
+};
+
+}
diff --git a/storage/src/vespa/storage/distributor/operations/external/getoperation.cpp b/storage/src/vespa/storage/distributor/operations/external/getoperation.cpp
index 6cfc688db0e..7ff2e298791 100644
--- a/storage/src/vespa/storage/distributor/operations/external/getoperation.cpp
+++ b/storage/src/vespa/storage/distributor/operations/external/getoperation.cpp
@@ -45,7 +45,8 @@ GetOperation::GroupId::operator==(const GroupId& other) const
}
GetOperation::GetOperation(DistributorComponent& manager,
- DistributorBucketSpace &bucketSpace,
+ const DistributorBucketSpace &bucketSpace,
+ std::shared_ptr<BucketDatabase::ReadGuard> read_guard,
std::shared_ptr<api::GetCommand> msg,
PersistenceOperationMetricSet& metric)
: Operation(),
@@ -58,7 +59,7 @@ GetOperation::GetOperation(DistributorComponent& manager,
_metric(metric),
_operationTimer(manager.getClock())
{
- assignTargetNodeGroups();
+ assignTargetNodeGroups(*read_guard);
}
void
@@ -213,13 +214,13 @@ GetOperation::sendReply(DistributorMessageSender& sender)
}
void
-GetOperation::assignTargetNodeGroups()
+GetOperation::assignTargetNodeGroups(const BucketDatabase::ReadGuard& read_guard)
{
document::BucketIdFactory bucketIdFactory;
document::BucketId bid = bucketIdFactory.getBucketId(_msg->getDocumentId());
std::vector<BucketDatabase::Entry> entries;
- _bucketSpace.getBucketDatabase().acquire_read_guard()->find_parents_and_self(bid, entries);
+ read_guard.find_parents_and_self(bid, entries);
for (uint32_t j = 0; j < entries.size(); ++j) {
const BucketDatabase::Entry& e = entries[j];
diff --git a/storage/src/vespa/storage/distributor/operations/external/getoperation.h b/storage/src/vespa/storage/distributor/operations/external/getoperation.h
index 3936f13077e..fe4dab5e9f2 100644
--- a/storage/src/vespa/storage/distributor/operations/external/getoperation.h
+++ b/storage/src/vespa/storage/distributor/operations/external/getoperation.h
@@ -3,7 +3,7 @@
#include <vespa/storageapi/defs.h>
#include <vespa/storage/distributor/operations/operation.h>
-#include <vespa/storage/bucketdb/bucketcopy.h>
+#include <vespa/storage/bucketdb/bucketdatabase.h>
#include <vespa/storageapi/messageapi/storagemessage.h>
#include <vespa/storageframework/generic/clock/timer.h>
@@ -23,8 +23,11 @@ class DistributorBucketSpace;
class GetOperation : public Operation
{
public:
- GetOperation(DistributorComponent& manager, DistributorBucketSpace &bucketSpace,
- std::shared_ptr<api::GetCommand> msg, PersistenceOperationMetricSet& metric);
+ GetOperation(DistributorComponent& manager,
+ const DistributorBucketSpace &bucketSpace,
+ std::shared_ptr<BucketDatabase::ReadGuard> read_guard,
+ std::shared_ptr<api::GetCommand> msg,
+ PersistenceOperationMetricSet& metric);
void onClose(DistributorMessageSender& sender) override;
void onStart(DistributorMessageSender& sender) override;
@@ -74,7 +77,7 @@ private:
std::map<GroupId, GroupVector> _responses;
DistributorComponent& _manager;
- DistributorBucketSpace &_bucketSpace;
+ const DistributorBucketSpace &_bucketSpace;
std::shared_ptr<api::GetCommand> _msg;
@@ -89,7 +92,7 @@ private:
void sendReply(DistributorMessageSender& sender);
bool sendForChecksum(DistributorMessageSender& sender, const document::BucketId& id, GroupVector& res);
- void assignTargetNodeGroups();
+ void assignTargetNodeGroups(const BucketDatabase::ReadGuard& read_guard);
bool copyIsOnLocalNode(const BucketCopy&) const;
/**
* Returns the vector index of the target to send to, or -1 if none
diff --git a/storage/src/vespa/storage/distributor/operations/external/twophaseupdateoperation.cpp b/storage/src/vespa/storage/distributor/operations/external/twophaseupdateoperation.cpp
index b7ebafc114c..b3326a43be2 100644
--- a/storage/src/vespa/storage/distributor/operations/external/twophaseupdateoperation.cpp
+++ b/storage/src/vespa/storage/distributor/operations/external/twophaseupdateoperation.cpp
@@ -178,7 +178,8 @@ TwoPhaseUpdateOperation::startSafePathUpdate(DistributorMessageSender& sender)
document::Bucket bucket(_updateCmd->getBucket().getBucketSpace(), document::BucketId(0));
auto get = std::make_shared<api::GetCommand>(bucket, _updateCmd->getDocumentId(),"[all]");
copyMessageSettings(*_updateCmd, *get);
- auto getOperation = std::make_shared<GetOperation>(_manager, _bucketSpace, get, _getMetric);
+ auto getOperation = std::make_shared<GetOperation>(
+ _manager, _bucketSpace, _bucketSpace.getBucketDatabase().acquire_read_guard(), get, _getMetric);
GetOperation & op = *getOperation;
IntermediateMessageSender intermediate(_sentMessageMap, std::move(getOperation), sender);
op.start(intermediate, _manager.getClock().getTimeInMillis());
diff --git a/tenant-auth/src/main/java/ai/vespa/hosted/auth/ApiAuthenticator.java b/tenant-auth/src/main/java/ai/vespa/hosted/auth/ApiAuthenticator.java
index 2b5bbb188dc..9de06e7f4da 100644
--- a/tenant-auth/src/main/java/ai/vespa/hosted/auth/ApiAuthenticator.java
+++ b/tenant-auth/src/main/java/ai/vespa/hosted/auth/ApiAuthenticator.java
@@ -5,12 +5,17 @@ import ai.vespa.hosted.api.Properties;
public class ApiAuthenticator implements ai.vespa.hosted.api.ApiAuthenticator {
- /** Returns an authenticating controller client, using private key signatures for authentication. */
+ /** Returns a controller client using mTLS if a key and certificate pair is provided, or signed requests otherwise. */
@Override
public ControllerHttpClient controller() {
- return ControllerHttpClient.withSignatureKey(Properties.endpoint(),
- Properties.privateKeyFile(),
- Properties.application());
+ return Properties.certificateFile()
+ .map(certificateFile -> ControllerHttpClient.withKeyAndCertificate(Properties.endpoint(),
+ Properties.privateKeyFile(),
+ certificateFile))
+ .orElseGet(() ->
+ ControllerHttpClient.withSignatureKey(Properties.endpoint(),
+ Properties.privateKeyFile(),
+ Properties.application()));
}
}
diff --git a/tenant-auth/src/main/java/ai/vespa/hosted/auth/EndpointAuthenticator.java b/tenant-auth/src/main/java/ai/vespa/hosted/auth/EndpointAuthenticator.java
index c1cca56f1b9..c9640763ac8 100644
--- a/tenant-auth/src/main/java/ai/vespa/hosted/auth/EndpointAuthenticator.java
+++ b/tenant-auth/src/main/java/ai/vespa/hosted/auth/EndpointAuthenticator.java
@@ -15,6 +15,7 @@ import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Optional;
+import java.util.logging.Logger;
import static ai.vespa.hosted.api.Properties.getNonBlankProperty;
@@ -25,6 +26,8 @@ import static ai.vespa.hosted.api.Properties.getNonBlankProperty;
*/
public class EndpointAuthenticator implements ai.vespa.hosted.api.EndpointAuthenticator {
+ private static final Logger logger = Logger.getLogger(EndpointAuthenticator.class.getName());
+
/** Don't touch. */
public EndpointAuthenticator(@SuppressWarnings("unused") SystemName __) { }
@@ -35,22 +38,39 @@ public class EndpointAuthenticator implements ai.vespa.hosted.api.EndpointAuthen
@Override
public SSLContext sslContext() {
try {
+ Path certificateFile = null;
+ Path privateKeyFile = null;
Optional<String> credentialsRootProperty = getNonBlankProperty("vespa.test.credentials.root");
- if (credentialsRootProperty.isEmpty())
- return SSLContext.getDefault();
-
- Path credentialsRoot = Path.of(credentialsRootProperty.get());
- Path certificateFile = credentialsRoot.resolve("cert");
- Path privateKeyFile = credentialsRoot.resolve("key");
-
- X509Certificate certificate = X509CertificateUtils.fromPem(new String(Files.readAllBytes(certificateFile)));
- if ( Instant.now().isBefore(certificate.getNotBefore().toInstant())
- || Instant.now().isAfter(certificate.getNotAfter().toInstant()))
- throw new IllegalStateException("Certificate at '" + certificateFile + "' is valid between " +
- certificate.getNotBefore() + " and " + certificate.getNotAfter() + " — not now.");
+ if (credentialsRootProperty.isPresent()) {
+ Path credentialsRoot = Path.of(credentialsRootProperty.get());
+ certificateFile = credentialsRoot.resolve("cert");
+ privateKeyFile = credentialsRoot.resolve("key");
+ }
+ else {
+ Optional<String> certificateFileProperty = getNonBlankProperty("dataPlaneCertificateFile");
+ if (certificateFileProperty.isPresent())
+ certificateFile = Path.of(certificateFileProperty.get());
+ Optional<String> privateKeyFileProperty = getNonBlankProperty("dataPlaneKeyFile");
+ if (privateKeyFileProperty.isPresent())
+ privateKeyFile = Path.of(privateKeyFileProperty.get());
+ }
+ if (certificateFile != null && privateKeyFile != null) {
+ X509Certificate certificate = X509CertificateUtils.fromPem(new String(Files.readAllBytes(certificateFile)));
+ if ( Instant.now().isBefore(certificate.getNotBefore().toInstant())
+ || Instant.now().isAfter(certificate.getNotAfter().toInstant()))
+ throw new IllegalStateException("Certificate at '" + certificateFile + "' is valid between " +
+ certificate.getNotBefore() + " and " + certificate.getNotAfter() + " — not now.");
- PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(new String(Files.readAllBytes(privateKeyFile)));
- return new SslContextBuilder().withKeyStore(privateKey, certificate).build();
+ PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(new String(Files.readAllBytes(privateKeyFile)));
+ return new SslContextBuilder().withKeyStore(privateKey, certificate).build();
+ }
+ logger.warning( "##################################################################################\n"
+ + "# Data plane key and/or certificate missing; please specify #\n"
+ + "# '-DdataPlaneCertificateFile=/path/to/certificate' and #\n"
+ + "# '-DdataPlaneKeyFile=/path/to/private_key. #\n"
+ + "# Trying the default SSLContext, but this will most likely cause HTTP error 401. #\n"
+ + "##################################################################################");
+ return SSLContext.getDefault();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java
index bda7e41c19b..4cc92828b0e 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/common/ClientBase.java
@@ -36,9 +36,10 @@ public abstract class ClientBase implements AutoCloseable {
protected ClientBase(String userAgent,
Supplier<SSLContext> sslContextSupplier,
- ClientExceptionFactory exceptionFactory) {
+ ClientExceptionFactory exceptionFactory,
+ HostnameVerifier hostnameVerifier) {
this.exceptionFactory = exceptionFactory;
- this.client = createHttpClient(userAgent, sslContextSupplier);
+ this.client = createHttpClient(userAgent, sslContextSupplier, hostnameVerifier);
}
protected <T> T execute(HttpUriRequest request, ResponseHandler<T> responseHandler) {
@@ -74,11 +75,11 @@ public abstract class ClientBase implements AutoCloseable {
return statusCode>=200 && statusCode<300;
}
- private static CloseableHttpClient createHttpClient(String userAgent, Supplier<SSLContext> sslContextSupplier) {
+ private static CloseableHttpClient createHttpClient(String userAgent, Supplier<SSLContext> sslContextSupplier, HostnameVerifier hostnameVerifier) {
return HttpClientBuilder.create()
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, /*requestSentRetryEnabled*/true))
.setUserAgent(userAgent)
- .setSSLSocketFactory(new SSLConnectionSocketFactory(new ServiceIdentitySslSocketFactory(sslContextSupplier), (HostnameVerifier)null))
+ .setSSLSocketFactory(new SSLConnectionSocketFactory(new ServiceIdentitySslSocketFactory(sslContextSupplier), hostnameVerifier))
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout((int) Duration.ofSeconds(10).toMillis())
.setConnectionRequestTimeout((int)Duration.ofSeconds(10).toMillis())
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java
index da3bd18440b..7b5427216a1 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java
@@ -5,7 +5,6 @@ import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzResourceName;
import com.yahoo.vespa.athenz.api.AthenzRole;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.OktaAccessToken;
import com.yahoo.vespa.athenz.client.common.ClientBase;
import com.yahoo.vespa.athenz.client.zms.bindings.AccessResponseEntity;
@@ -45,7 +44,7 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient {
}
private DefaultZmsClient(URI zmsUrl, AthenzIdentity identity, Supplier<SSLContext> sslContextSupplier) {
- super("vespa-zms-client", sslContextSupplier, ZmsClientException::new);
+ super("vespa-zms-client", sslContextSupplier, ZmsClientException::new, null);
this.zmsUrl = addTrailingSlash(zmsUrl);
this.identity = identity;
}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java
index 8bd0d0b50d4..45597cbad08 100644
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zts/DefaultZtsClient.java
@@ -26,6 +26,7 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
+import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.URI;
@@ -49,15 +50,19 @@ public class DefaultZtsClient extends ClientBase implements ZtsClient {
private final URI ztsUrl;
public DefaultZtsClient(URI ztsUrl, SSLContext sslContext) {
- this(ztsUrl, () -> sslContext);
+ this(ztsUrl, () -> sslContext, null);
}
public DefaultZtsClient(URI ztsUrl, ServiceIdentityProvider identityProvider) {
- this(ztsUrl, identityProvider::getIdentitySslContext);
+ this(ztsUrl, identityProvider::getIdentitySslContext, null);
}
- private DefaultZtsClient(URI ztsUrl, Supplier<SSLContext> sslContextSupplier) {
- super("vespa-zts-client", sslContextSupplier, ZtsClientException::new);
+ public DefaultZtsClient(URI ztsUrl, ServiceIdentityProvider identityProvider, HostnameVerifier hostnameVerifier) {
+ this(ztsUrl, identityProvider::getIdentitySslContext, hostnameVerifier);
+ }
+
+ private DefaultZtsClient(URI ztsUrl, Supplier<SSLContext> sslContextSupplier, HostnameVerifier hostnameVerifier) {
+ super("vespa-zts-client", sslContextSupplier, ZtsClientException::new, hostnameVerifier);
this.ztsUrl = addTrailingSlash(ztsUrl);
}