diff options
37 files changed, 891 insertions, 272 deletions
diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java index a678b8607fd..cd2e0cc5064 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java @@ -7,6 +7,8 @@ import com.yahoo.system.ProcessExecuter; import com.yahoo.vespa.hosted.dockerapi.Docker; import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; import com.yahoo.vespa.hosted.node.admin.config.ConfigServerConfig; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerClients; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerClientsImpl; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperationsImpl; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; @@ -16,12 +18,7 @@ import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdaterImpl; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepositoryImpl; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; -import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorImpl; import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; -import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor; import java.time.Clock; import java.time.Duration; @@ -41,7 +38,7 @@ public class DockerAdminComponent implements AdminComponent { private final Optional<ClassLocking> classLocking; private Optional<Environment> environment = Optional.empty(); - private Optional<ConfigServerHttpRequestExecutor> requestExecutor = Optional.empty(); + private Optional<ConfigServerClients> configServerClients = Optional.empty(); private Optional<NodeAdminStateUpdaterImpl> nodeAdminStateUpdater = Optional.empty(); public DockerAdminComponent(ConfigServerConfig configServerConfig, @@ -85,17 +82,10 @@ public class DockerAdminComponent implements AdminComponent { environment = Optional.of(new Environment(configServerConfig)); } - if (!requestExecutor.isPresent()) { - requestExecutor = Optional.of(ConfigServerHttpRequestExecutor.create( - environment.get().getConfigServerUris(), - environment.get().getKeyStoreOptions(), - environment.get().getTrustStoreOptions(), - environment.get().getAthenzIdentity())); + if (!configServerClients.isPresent()) { + configServerClients = Optional.of(new ConfigServerClientsImpl(environment.get())); } - NodeRepository nodeRepository = new NodeRepositoryImpl(requestExecutor.get()); - Orchestrator orchestrator = new OrchestratorImpl(requestExecutor.get()); - Clock clock = Clock.systemUTC(); String dockerHostHostName = HostName.getLocalhost(); ProcessExecuter processExecuter = new ProcessExecuter(); @@ -115,13 +105,13 @@ public class DockerAdminComponent implements AdminComponent { AclMaintainer aclMaintainer = new AclMaintainer( dockerOperations, - nodeRepository, + configServerClients.get().nodeRepository(), dockerHostHostName); Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl( hostName, - nodeRepository, - orchestrator, + configServerClients.get().nodeRepository(), + configServerClients.get().orchestrator(), dockerOperations, storageMaintainer, aclMaintainer, @@ -138,8 +128,8 @@ public class DockerAdminComponent implements AdminComponent { clock); return new NodeAdminStateUpdaterImpl( - nodeRepository, - orchestrator, + configServerClients.get().nodeRepository(), + configServerClients.get().orchestrator(), storageMaintainer, nodeAdmin, dockerHostHostName, @@ -155,7 +145,7 @@ public class DockerAdminComponent implements AdminComponent { } nodeAdminStateUpdater.ifPresent(NodeAdminStateUpdaterImpl::stop); - requestExecutor.ifPresent(ConfigServerHttpRequestExecutor::close); + configServerClients.ifPresent(ConfigServerClients::stop); nodeAdminStateUpdater = Optional.empty(); } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/Environment.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/Environment.java index 7376a59bd5c..5581415fec2 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/Environment.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/Environment.java @@ -86,11 +86,13 @@ public class Environment { createKeyStoreOptions( configServerConfig.keyStoreConfig().path(), configServerConfig.keyStoreConfig().password().toCharArray(), - configServerConfig.keyStoreConfig().type().name()), + configServerConfig.keyStoreConfig().type().name(), + "BC"), createKeyStoreOptions( configServerConfig.trustStoreConfig().path(), configServerConfig.trustStoreConfig().password().toCharArray(), - configServerConfig.trustStoreConfig().type().name()), + configServerConfig.trustStoreConfig().type().name(), + null), createAthenzIdentity( configServerConfig.athenzDomain(), configServerConfig.serviceName()) @@ -161,10 +163,10 @@ public class Environment { return Arrays.asList(logstashNodes.split("[,\\s]+")); } - private static Optional<KeyStoreOptions> createKeyStoreOptions(String pathToKeyStore, char[] password, String type) { + private static Optional<KeyStoreOptions> createKeyStoreOptions(String pathToKeyStore, char[] password, String type, String provider) { return Optional.ofNullable(pathToKeyStore) .filter(path -> !Strings.isNullOrEmpty(path)) - .map(path -> new KeyStoreOptions(Paths.get(path), password, type)); + .map(path -> new KeyStoreOptions(Paths.get(path), password, type, provider)); } private static Optional<AthenzIdentity> createAthenzIdentity(String athenzDomain, String serviceName) { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApi.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApi.java new file mode 100644 index 00000000000..4d4a6c0328d --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApi.java @@ -0,0 +1,28 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver; + +import java.util.Optional; + +/** + * Interface to execute basic HTTP request against config server(s) + * + * @author freva + */ +public interface ConfigServerApi extends AutoCloseable { + + <T> T get(String path, Class<T> wantedReturnType); + + <T> T post(String path, Object bodyJsonPojo, Class<T> wantedReturnType); + + <T> T put(String path, Optional<Object> bodyJsonPojo, Class<T> wantedReturnType); + + <T> T patch(String path, Object bodyJsonPojo, Class<T> wantedReturnType); + + <T> T delete(String path, Class<T> wantedReturnType); + + /** + * Close the underlying HTTP client and any threads this class might have started. + */ + @Override + void close(); +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/ConfigServerHttpRequestExecutor.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApiImpl.java index 13bfc949533..c5592e91973 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/ConfigServerHttpRequestExecutor.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApiImpl.java @@ -1,12 +1,9 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.util; +package com.yahoo.vespa.hosted.node.admin.configserver; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.concurrent.ThreadFactoryFactory; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; -import com.yahoo.vespa.athenz.tls.AthenzSslContextBuilder; +import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import org.apache.http.HttpHeaders; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; @@ -18,28 +15,15 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.entity.StringEntity; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; -import java.nio.file.Path; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.Security; -import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; /** * Retries request on config server a few times before giving up. Assumes that all requests should be sent with @@ -47,13 +31,11 @@ import java.util.function.Supplier; * * @author dybdahl */ -public class ConfigServerHttpRequestExecutor implements AutoCloseable { - private static final PrefixLogger NODE_ADMIN_LOGGER = PrefixLogger.getNodeAdminLogger(ConfigServerHttpRequestExecutor.class); - private static final Duration CLIENT_REFRESH_INTERVAL = Duration.ofHours(1); +public class ConfigServerApiImpl implements ConfigServerApi { + private static final PrefixLogger NODE_ADMIN_LOGGER = PrefixLogger.getNodeAdminLogger(ConfigServerApiImpl.class); private final ObjectMapper mapper = new ObjectMapper(); - private final ScheduledExecutorService clientRefresherScheduler = - Executors.newScheduledThreadPool(1, ThreadFactoryFactory.getDaemonThreadFactory("http-client-refresher")); + private final List<URI> configServerHosts; /** @@ -66,30 +48,20 @@ public class ConfigServerHttpRequestExecutor implements AutoCloseable { */ private volatile SelfCloseableHttpClient client; - public static ConfigServerHttpRequestExecutor create( - Collection<URI> configServerUris, - Optional<KeyStoreOptions> keyStoreOptions, - Optional<KeyStoreOptions> trustStoreOptions, - Optional<AthenzIdentity> athenzIdentity) { - Security.addProvider(new BouncyCastleProvider()); - - Supplier<SelfCloseableHttpClient> clientSupplier = () -> createHttpClient(keyStoreOptions, trustStoreOptions, athenzIdentity); - ConfigServerHttpRequestExecutor requestExecutor = new ConfigServerHttpRequestExecutor( - randomizeConfigServerUris(configServerUris), clientSupplier.get()); + public ConfigServerApiImpl(Collection<URI> configServerUris) { + this(configServerUris, SSLConnectionSocketFactory.getSocketFactory()); + } - if (keyStoreOptions.isPresent() || trustStoreOptions.isPresent()) { - requestExecutor.clientRefresherScheduler.scheduleAtFixedRate(() -> requestExecutor.client = clientSupplier.get(), - CLIENT_REFRESH_INTERVAL.toMillis(), CLIENT_REFRESH_INTERVAL.toMillis(), TimeUnit.MILLISECONDS); - } - return requestExecutor; + ConfigServerApiImpl(Collection<URI> configServerUris, SSLConnectionSocketFactory sslConnectionSocketFactory) { + this(randomizeConfigServerUris(configServerUris), new SelfCloseableHttpClient(sslConnectionSocketFactory)); } - ConfigServerHttpRequestExecutor(List<URI> configServerHosts, SelfCloseableHttpClient client) { + ConfigServerApiImpl(List<URI> configServerHosts, SelfCloseableHttpClient client) { this.configServerHosts = configServerHosts; this.client = client; } - public interface CreateRequest { + interface CreateRequest { HttpUriRequest createRequest(URI configServerUri) throws JsonProcessingException, UnsupportedEncodingException; } @@ -181,6 +153,10 @@ public class ConfigServerHttpRequestExecutor implements AutoCloseable { request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json"); } + public void setSSLConnectionSocketFactory(SSLConnectionSocketFactory sslSocketFactory) { + this.client = new SelfCloseableHttpClient(sslSocketFactory); + } + // Shuffle config server URIs to balance load private static List<URI> randomizeConfigServerUris(Collection<URI> configServerUris) { List<URI> shuffledConfigServerHosts = new ArrayList<>(configServerUris); @@ -188,62 +164,8 @@ public class ConfigServerHttpRequestExecutor implements AutoCloseable { return shuffledConfigServerHosts; } - private static SelfCloseableHttpClient createHttpClient(Optional<KeyStoreOptions> keyStoreOptions, - Optional<KeyStoreOptions> trustStoreOptions, - Optional<AthenzIdentity> athenzIdentity) { - NODE_ADMIN_LOGGER.info("Creating new HTTP client"); - try { - SSLContext sslContext = makeSslContext(keyStoreOptions, trustStoreOptions); - HostnameVerifier hostnameVerifier = makeHostnameVerifier(athenzIdentity); - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); - return new SelfCloseableHttpClient(sslSocketFactory); - } catch (Exception e) { - NODE_ADMIN_LOGGER.error("Failed to create HTTP client with custom SSL Context, proceeding with default", e); - return new SelfCloseableHttpClient(); - } - } - - private static SSLContext makeSslContext(Optional<KeyStoreOptions> keyStoreOptions, Optional<KeyStoreOptions> trustStoreOptions) { - AthenzSslContextBuilder sslContextBuilder = new AthenzSslContextBuilder(); - trustStoreOptions.ifPresent(options -> sslContextBuilder.withTrustStore(options.path.toFile(), options.type)); - keyStoreOptions.ifPresent(options -> { - try { - KeyStore keyStore = loadKeyStoreFromFileWithProvider(options.path, options.password, options.type, "BC"); - sslContextBuilder.withKeyStore(keyStore, options.password); - } catch (Exception e) { - throw new RuntimeException("Failed to read key store", e); - } - }); - - return sslContextBuilder.build(); - } - - private static HostnameVerifier makeHostnameVerifier(Optional<AthenzIdentity> athenzIdentity) { - return athenzIdentity - .map(identity -> (HostnameVerifier) new AthenzIdentityVerifier(Collections.singleton(identity))) - .orElse(SSLConnectionSocketFactory.getDefaultHostnameVerifier()); - } - @Override public void close() { - clientRefresherScheduler.shutdown(); - do { - try { - clientRefresherScheduler.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - } catch (InterruptedException e1) { - NODE_ADMIN_LOGGER.info("Interrupted while waiting for clientRefresherScheduler to shutdown"); - } - } while (!clientRefresherScheduler.isTerminated()); - client.close(); } - - private static KeyStore loadKeyStoreFromFileWithProvider(Path path, char[] password, String keyStoreType, String provider) - throws IOException, GeneralSecurityException { - KeyStore keyStore = KeyStore.getInstance(keyStoreType, provider); - try (FileInputStream in = new FileInputStream(path.toFile())) { - keyStore.load(in, password); - } - return keyStore; - } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerClients.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerClients.java new file mode 100644 index 00000000000..f52487c306f --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerClients.java @@ -0,0 +1,15 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver; + +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; + +/** + * @author freva + */ +public interface ConfigServerClients { + NodeRepository nodeRepository(); + Orchestrator orchestrator(); + + void stop(); +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerClientsImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerClientsImpl.java new file mode 100644 index 00000000000..43a2c66a9e5 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerClientsImpl.java @@ -0,0 +1,54 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver; + +import com.yahoo.vespa.hosted.node.admin.component.Environment; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepositoryImpl; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.OrchestratorImpl; + +import java.util.Optional; + +/** + * @author freva + */ +public class ConfigServerClientsImpl implements ConfigServerClients { + + private final Optional<ConfigServerApi> configServerApi; + private final NodeRepository nodeRepository; + private final Orchestrator orchestrator; + + public ConfigServerClientsImpl(Environment environment) { + this(new SslConfigServerApiImpl(environment)); + } + + public ConfigServerClientsImpl(NodeRepository nodeRepository, Orchestrator orchestrator) { + this(nodeRepository, orchestrator, Optional.empty()); + } + + private ConfigServerClientsImpl(ConfigServerApi configServerApi) { + this(new NodeRepositoryImpl(configServerApi), new OrchestratorImpl(configServerApi), Optional.of(configServerApi)); + } + + private ConfigServerClientsImpl(NodeRepository nodeRepository, Orchestrator orchestrator, + Optional<ConfigServerApi> configServerApi) { + this.nodeRepository = nodeRepository; + this.orchestrator = orchestrator; + this.configServerApi = configServerApi; + } + + @Override + public NodeRepository nodeRepository() { + return nodeRepository; + } + + @Override + public Orchestrator orchestrator() { + return orchestrator; + } + + @Override + public void stop() { + configServerApi.ifPresent(ConfigServerApi::close); + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/HttpException.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/HttpException.java index 55d3ecc4e60..0a2ae1bd426 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/HttpException.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/HttpException.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.util; +package com.yahoo.vespa.hosted.node.admin.configserver; import javax.ws.rs.core.Response; import java.util.Optional; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/SelfCloseableHttpClient.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SelfCloseableHttpClient.java index 8e516729aff..cead7816387 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/SelfCloseableHttpClient.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SelfCloseableHttpClient.java @@ -1,5 +1,5 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.util; +package com.yahoo.vespa.hosted.node.admin.configserver; import com.yahoo.log.LogLevel; import org.apache.http.client.config.RequestConfig; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java new file mode 100644 index 00000000000..8c2b87f4068 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/SslConfigServerApiImpl.java @@ -0,0 +1,120 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver; + +import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; +import com.yahoo.vespa.athenz.tls.AthenzSslContextBuilder; +import com.yahoo.vespa.hosted.node.admin.component.Environment; +import com.yahoo.vespa.hosted.node.admin.configserver.certificate.ConfigServerKeyStoreRefresher; +import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import java.security.Security; +import java.util.Collections; +import java.util.Optional; + +/** + * ConfigServerApi with proper keystore, truststore and hostname verifier to communicate with the + * configserver(s). The keystore is refreshed automatically. + * + * @author freva + */ +public class SslConfigServerApiImpl implements ConfigServerApi { + + private final ConfigServerApiImpl configServerApi; + private final Environment environment; + private final Optional<ConfigServerKeyStoreRefresher> keyStoreRefresher; + + public SslConfigServerApiImpl(Environment environment) { + Security.addProvider(new BouncyCastleProvider()); + + this.environment = environment; + + // At this point we don't know the state of the keystore, it may not exist at all, or the keystore + // maybe exists, but the certificate in it is expired. Create the ConfigServerApi without a keystore + // (but with truststore and hostname verifier). + this.configServerApi = new ConfigServerApiImpl( + environment.getConfigServerUris(), makeSslConnectionSocketFactory(Optional.empty())); + + // If we have keystore options, we should make sure we use the keystore with the latest certificate, + // start the keystore refresher. + this.keyStoreRefresher = environment.getKeyStoreOptions().map(keyStoreOptions -> { + // Any callback from KeyStoreRefresher should result in using the latest keystore on disk + Runnable connectionFactoryRefresher = () -> configServerApi.setSSLConnectionSocketFactory( + makeSslConnectionSocketFactory(Optional.of(keyStoreOptions))); + + ConfigServerKeyStoreRefresher keyStoreRefresher = new ConfigServerKeyStoreRefresher( + keyStoreOptions, connectionFactoryRefresher, configServerApi); + + // Run the refresh once manually to make sure that we have a valid certificate, otherwise fail. + try { + keyStoreRefresher.refreshKeyStoreIfNeeded(); + connectionFactoryRefresher.run(); // Update connectionFactory with the keystore on disk + } catch (Exception e) { + throw new RuntimeException("Failed to acquire certificate to config server", e); + } + + keyStoreRefresher.start(); + return keyStoreRefresher; + }); + } + + @Override + public <T> T get(String path, Class<T> wantedReturnType) { + return configServerApi.get(path, wantedReturnType); + } + + @Override + public <T> T post(String path, Object bodyJsonPojo, Class<T> wantedReturnType) { + return configServerApi.post(path, bodyJsonPojo, wantedReturnType); + } + + @Override + public <T> T put(String path, Optional<Object> bodyJsonPojo, Class<T> wantedReturnType) { + return configServerApi.put(path, bodyJsonPojo, wantedReturnType); + } + + @Override + public <T> T patch(String path, Object bodyJsonPojo, Class<T> wantedReturnType) { + return configServerApi.patch(path, bodyJsonPojo, wantedReturnType); + } + + @Override + public <T> T delete(String path, Class<T> wantedReturnType) { + return configServerApi.delete(path, wantedReturnType); + } + + @Override + public void close() { + keyStoreRefresher.ifPresent(ConfigServerKeyStoreRefresher::stop); + configServerApi.close(); + } + + private SSLConnectionSocketFactory makeSslConnectionSocketFactory(Optional<KeyStoreOptions> keyStoreOptions) { + return new SSLConnectionSocketFactory(makeSslContext(keyStoreOptions), makeHostnameVerifier()); + } + + private SSLContext makeSslContext(Optional<KeyStoreOptions> keyStoreOptions) { + AthenzSslContextBuilder sslContextBuilder = new AthenzSslContextBuilder(); + environment.getTrustStoreOptions().ifPresent(options -> + sslContextBuilder.withTrustStore(options.path.toFile(), options.type)); + + keyStoreOptions.ifPresent(options -> { + try { + sslContextBuilder.withKeyStore(options.loadKeyStore(), options.password); + } catch (Exception e) { + throw new RuntimeException("Failed to read key store", e); + } + }); + + return sslContextBuilder.build(); + } + + private HostnameVerifier makeHostnameVerifier() { + return environment.getAthenzIdentity() + .map(identity -> (HostnameVerifier) new AthenzIdentityVerifier(Collections.singleton(identity))) + .orElseGet(SSLConnectionSocketFactory::getDefaultHostnameVerifier); + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java new file mode 100644 index 00000000000..e7148754fde --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CertificateSerializedPayload.java @@ -0,0 +1,69 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver.certificate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.openssl.PEMParser; + +import java.io.IOException; +import java.io.StringReader; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Contains PEM formatted signed certificate + * TODO: Combine with its counterpart in athenz-identity-provider-service? + * + * @author freva + */ +public class CertificateSerializedPayload { + + @JsonProperty("certificate") public final X509Certificate certificate; + + @JsonCreator + public CertificateSerializedPayload(@JsonProperty("certificate") @JsonDeserialize(using = CertificateDeserializer.class) + X509Certificate certificate) { + this.certificate = certificate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CertificateSerializedPayload that = (CertificateSerializedPayload) o; + + return certificate.equals(that.certificate); + } + + @Override + public int hashCode() { + return certificate.hashCode(); + } + + @Override + public String toString() { + return "CertificateSerializedPayload{" + + "certificate='" + certificate + '\'' + + '}'; + } + + public static class CertificateDeserializer extends JsonDeserializer<X509Certificate> { + @Override + public X509Certificate deserialize( + JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + try (PEMParser pemParser = new PEMParser(new StringReader(jsonParser.getValueAsString()))) { + X509CertificateHolder x509CertificateHolder = (X509CertificateHolder) pemParser.readObject(); + return new JcaX509CertificateConverter().getCertificate(x509CertificateHolder); + } catch (CertificateException e) { + throw new RuntimeException("Failed to deserialize X509Certificate", e); + } + } + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java new file mode 100644 index 00000000000..1595db1047a --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresher.java @@ -0,0 +1,188 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver.certificate; + +import com.yahoo.net.HostName; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; +import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Clock; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Automatically refreshes the KeyStore used to authenticate this node to the configserver. + * The keystore contains a single certificate signed by one of the configservers. + * + * @author freva + */ +public class ConfigServerKeyStoreRefresher { + + private static final Logger logger = Logger.getLogger(ConfigServerKeyStoreRefresher.class.getName()); + private static final String KEY_STORE_ALIAS = "alias"; + static final long MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY = 3600; + static final String SIGNER_ALGORITHM = "SHA256withRSA"; + static final String CONFIG_SERVER_CERTIFICATE_SIGNING_PATH = "/athenz/v1/provider/sign"; + + private final ScheduledExecutorService executor; + private final KeyStoreOptions keyStoreOptions; + private final Runnable keyStoreUpdatedCallback; + private final ConfigServerApi configServerApi; + private final Clock clock; + private final String hostname; + + public ConfigServerKeyStoreRefresher( + KeyStoreOptions keyStoreOptions, Runnable keyStoreUpdatedCallback, ConfigServerApi configServerApi) { + this(keyStoreOptions, keyStoreUpdatedCallback, configServerApi, Executors.newScheduledThreadPool(0), + Clock.systemUTC(), HostName.getLocalhost()); + } + + ConfigServerKeyStoreRefresher(KeyStoreOptions keyStoreOptions, + Runnable keyStoreUpdatedCallback, + ConfigServerApi configServerApi, + ScheduledExecutorService executor, + Clock clock, + String hostname) { + this.keyStoreOptions = keyStoreOptions; + this.keyStoreUpdatedCallback = keyStoreUpdatedCallback; + this.configServerApi = configServerApi; + this.executor = executor; + this.clock = clock; + this.hostname = hostname; + } + + public void start() { + executor.schedule(this::refresh, getSecondsUntilNextRefresh(), TimeUnit.SECONDS); + } + + void refresh() { + try { + if (refreshKeyStoreIfNeeded()) { + keyStoreUpdatedCallback.run(); + } + final long secondsUntilNextRefresh = getSecondsUntilNextRefresh(); + executor.schedule(this::refresh, secondsUntilNextRefresh, TimeUnit.SECONDS); + logger.log(Level.INFO, "Successfully updated keystore, scheduled next refresh in " + + secondsUntilNextRefresh + "sec"); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to update keystore on schedule, will try again in " + + MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY + "sec", e); + executor.schedule(this::refresh, MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY, TimeUnit.SECONDS); + } + } + + public void stop() { + do { + try { + executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (InterruptedException e1) { + logger.info("Interrupted while waiting for ConfigServerKeyStoreRefresher thread to shutdown"); + } + } while (!executor.isTerminated()); + } + + public boolean refreshKeyStoreIfNeeded() throws + IOException, NoSuchAlgorithmException, OperatorCreationException, CertificateException, KeyStoreException, NoSuchProviderException { + if (!shouldRefreshCertificate()) return false; + + KeyPair keyPair = generateKeyPair(); + PKCS10CertificationRequest csr = generateCsr(keyPair, hostname); + X509Certificate certificate = sendCsr(csr); + + storeCertificate(keyPair, certificate); + return true; + } + + private long getSecondsUntilNextRefresh() { + long secondsUntilNextCheck = 0; + try { + secondsUntilNextCheck = getSecondsUntilCertificateShouldBeRefreshed(); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to get remaining certificate lifetime", e); + } + + return Math.max(MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY, secondsUntilNextCheck); + } + + private boolean shouldRefreshCertificate() { + try { + return getSecondsUntilCertificateShouldBeRefreshed() <= 0; + } catch (Exception e) { // We can't read the key store for whatever reason, let's just try to refresh it + return true; + } + } + + /** + * Returns number of seconds until we should start trying to refresh the certificate, this should be + * well before the certificate actually expires so that we have enough time to retry without + * overloading config server. + */ + private long getSecondsUntilCertificateShouldBeRefreshed() + throws NoSuchAlgorithmException, CertificateException, NoSuchProviderException, KeyStoreException, IOException { + X509Certificate cert = getConfigServerCertificate(); + long notBefore = cert.getNotBefore().getTime() / 1000; + long notAfter = cert.getNotAfter().getTime() / 1000; + long now = clock.millis() / 1000; + long thirdOfLifetime = (notAfter - notBefore) / 3; + + return Math.max(0, notBefore + thirdOfLifetime - now); + } + + X509Certificate getConfigServerCertificate() throws NoSuchAlgorithmException, CertificateException, NoSuchProviderException, KeyStoreException, IOException { + return (X509Certificate) keyStoreOptions.loadKeyStore().getCertificate(KEY_STORE_ALIAS); + } + + private void storeCertificate(KeyPair keyPair, X509Certificate certificate) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, NoSuchProviderException { + keyStoreOptions.path.getParent().toFile().mkdirs(); + X509Certificate[] certificateChain = {certificate}; + + try (FileOutputStream fos = new FileOutputStream(keyStoreOptions.path.toFile())) { + KeyStore keyStore = keyStoreOptions.getKeyStoreInstance(); + keyStore.load(null, null); + keyStore.setKeyEntry(KEY_STORE_ALIAS, keyPair.getPrivate(), keyStoreOptions.password, certificateChain); + keyStore.store(fos, keyStoreOptions.password); + } + } + + private X509Certificate sendCsr(PKCS10CertificationRequest csr) { + CertificateSerializedPayload certificateSerializedPayload = configServerApi.post( + CONFIG_SERVER_CERTIFICATE_SIGNING_PATH, + new CsrSerializedPayload(csr), + CertificateSerializedPayload.class); + + return certificateSerializedPayload.certificate; + } + + static KeyPair generateKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); + rsa.initialize(2048); + return rsa.genKeyPair(); + } + + private static PKCS10CertificationRequest generateCsr(KeyPair keyPair, String commonName) + throws NoSuchAlgorithmException, OperatorCreationException { + ContentSigner signer = new JcaContentSignerBuilder(SIGNER_ALGORITHM).build(keyPair.getPrivate()); + + return new JcaPKCS10CertificationRequestBuilder(new X500Name("CN=" + commonName), keyPair.getPublic()) + .build(signer); + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java new file mode 100644 index 00000000000..aa83fdc9e22 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/CsrSerializedPayload.java @@ -0,0 +1,66 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver.certificate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.util.io.pem.PemObject; + +import java.io.IOException; +import java.io.StringWriter; + +/** + * Contains PEM formatted Certificate Signing Request (CSR) + * TODO: Combine with its counterpart in athenz-identity-provider-service? + * + * @author freva + */ +public class CsrSerializedPayload { + + @JsonProperty("csr") @JsonSerialize(using = CertificateRequestSerializer.class) + public final PKCS10CertificationRequest csr; + + @JsonCreator + public CsrSerializedPayload(@JsonProperty("csr") PKCS10CertificationRequest csr) { + this.csr = csr; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CsrSerializedPayload that = (CsrSerializedPayload) o; + + return csr.equals(that.csr); + } + + @Override + public int hashCode() { + return csr.hashCode(); + } + + @Override + public String toString() { + return "CsrSerializedPayload{" + + "csr='" + csr + '\'' + + '}'; + } + + public static class CertificateRequestSerializer extends JsonSerializer<PKCS10CertificationRequest> { + @Override + public void serialize( + PKCS10CertificationRequest csr, JsonGenerator gen, SerializerProvider serializers) throws IOException { + try (StringWriter stringWriter = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(new PemObject("CERTIFICATE REQUEST", csr.getEncoded())); + pemWriter.flush(); + gen.writeString(stringWriter.toString()); + } + } + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java index 9f4c6916b48..8012805f4d1 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepository.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java @@ -1,11 +1,10 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository; import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; -import java.io.IOException; import java.util.List; import java.util.Optional; @@ -13,7 +12,7 @@ import java.util.Optional; * @author stiankri */ public interface NodeRepository { - List<ContainerNodeSpec> getContainersToRun(String baseHostName) throws IOException; + List<ContainerNodeSpec> getContainersToRun(String baseHostName); Optional<ContainerNodeSpec> getContainerNodeSpec(String hostName); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepositoryImpl.java index ab7332ca556..f2152dffc0c 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepositoryImpl.java @@ -1,22 +1,21 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository; import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; -import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetAclResponse; -import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.GetNodesResponse; -import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.NodeMessageResponse; -import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.UpdateNodeAttributesRequestBody; -import com.yahoo.vespa.hosted.node.admin.noderepository.bindings.UpdateNodeAttributesResponse; -import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor; -import com.yahoo.vespa.hosted.node.admin.util.HttpException; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.GetAclResponse; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.GetNodesResponse; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.NodeMessageResponse; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.UpdateNodeAttributesRequestBody; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.UpdateNodeAttributesResponse; +import com.yahoo.vespa.hosted.node.admin.configserver.HttpException; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import com.yahoo.vespa.hosted.provision.Node; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -30,44 +29,37 @@ import java.util.stream.Collectors; public class NodeRepositoryImpl implements NodeRepository { private static final PrefixLogger NODE_ADMIN_LOGGER = PrefixLogger.getNodeAdminLogger(NodeRepositoryImpl.class); - private final ConfigServerHttpRequestExecutor requestExecutor; + private final ConfigServerApi configServerApi; - public NodeRepositoryImpl(ConfigServerHttpRequestExecutor requestExecutor) { - this.requestExecutor = requestExecutor; + public NodeRepositoryImpl(ConfigServerApi configServerApi) { + this.configServerApi = configServerApi; } @Override - public List<ContainerNodeSpec> getContainersToRun(String baseHostName) throws IOException { - try { - final GetNodesResponse nodesForHost = requestExecutor.get( - "/nodes/v2/node/?parentHost=" + baseHostName + "&recursive=true", - GetNodesResponse.class); - - if (nodesForHost.nodes == null) { - throw new IOException("Response didn't contain nodes element"); - } - List<ContainerNodeSpec> nodes = new ArrayList<>(nodesForHost.nodes.size()); - for (GetNodesResponse.Node node : nodesForHost.nodes) { - ContainerNodeSpec nodeSpec; - try { - nodeSpec = createContainerNodeSpec(node); - } catch (IllegalArgumentException | NullPointerException e) { - NODE_ADMIN_LOGGER.warning("Bad node received from node repo when requesting children of the " - + baseHostName + " host: " + node, e); - continue; - } - nodes.add(nodeSpec); + public List<ContainerNodeSpec> getContainersToRun(String baseHostName) { + final GetNodesResponse nodesForHost = configServerApi.get( + "/nodes/v2/node/?parentHost=" + baseHostName + "&recursive=true", + GetNodesResponse.class); + + List<ContainerNodeSpec> nodes = new ArrayList<>(nodesForHost.nodes.size()); + for (GetNodesResponse.Node node : nodesForHost.nodes) { + ContainerNodeSpec nodeSpec; + try { + nodeSpec = createContainerNodeSpec(node); + } catch (IllegalArgumentException | NullPointerException e) { + NODE_ADMIN_LOGGER.warning("Bad node received from node repo when requesting children of the " + + baseHostName + " host: " + node, e); + continue; } - return nodes; - } catch (Exception e) { - throw new IOException(e); + nodes.add(nodeSpec); } + return nodes; } @Override public Optional<ContainerNodeSpec> getContainerNodeSpec(String hostName) { try { - GetNodesResponse.Node nodeResponse = requestExecutor.get("/nodes/v2/node/" + hostName, + GetNodesResponse.Node nodeResponse = configServerApi.get("/nodes/v2/node/" + hostName, GetNodesResponse.Node.class); if (nodeResponse == null) { return Optional.empty(); @@ -82,7 +74,7 @@ public class NodeRepositoryImpl implements NodeRepository { public List<ContainerAclSpec> getContainerAclSpecs(String hostName) { try { final String path = String.format("/nodes/v2/acl/%s?children=true", hostName); - final GetAclResponse response = requestExecutor.get(path, GetAclResponse.class); + final GetAclResponse response = configServerApi.get(path, GetAclResponse.class); return response.trustedNodes.stream() .map(node -> new ContainerAclSpec( node.hostname, node.ipAddress, ContainerName.fromHostname(node.trustedBy))) @@ -142,7 +134,7 @@ public class NodeRepositoryImpl implements NodeRepository { @Override public void updateNodeAttributes(final String hostName, final NodeAttributes nodeAttributes) { - UpdateNodeAttributesResponse response = requestExecutor.patch( + UpdateNodeAttributesResponse response = configServerApi.patch( "/nodes/v2/node/" + hostName, new UpdateNodeAttributesRequestBody(nodeAttributes), UpdateNodeAttributesResponse.class); @@ -166,7 +158,7 @@ public class NodeRepositoryImpl implements NodeRepository { } private void markNodeToState(String hostName, String state) { - NodeMessageResponse response = requestExecutor.put( + NodeMessageResponse response = configServerApi.put( "/nodes/v2/state/" + state + "/" + hostName, Optional.empty(), /* body */ NodeMessageResponse.class); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java index 254ab5fa3ba..b7762cf6aa9 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetAclResponse.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository.bindings; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetNodesResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetNodesResponse.java index ce31c3c1b4c..c94b3836100 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/GetNodesResponse.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetNodesResponse.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository.bindings; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/NodeMessageResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/NodeMessageResponse.java index b8c903f863d..0f5e896c290 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/NodeMessageResponse.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/NodeMessageResponse.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository.bindings; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/UpdateNodeAttributesRequestBody.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/UpdateNodeAttributesRequestBody.java index 7acd94a2947..28605bc3a8d 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/UpdateNodeAttributesRequestBody.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/UpdateNodeAttributesRequestBody.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository.bindings; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings; import com.fasterxml.jackson.annotation.JsonInclude; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/UpdateNodeAttributesResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/UpdateNodeAttributesResponse.java index a11f3bf46a9..80c90e8311f 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/noderepository/bindings/UpdateNodeAttributesResponse.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/UpdateNodeAttributesResponse.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository.bindings; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/Orchestrator.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/Orchestrator.java index d98378e194c..b5d41b7fbb4 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/Orchestrator.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/Orchestrator.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.orchestrator; +package com.yahoo.vespa.hosted.node.admin.configserver.orchestrator; import java.util.List; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorException.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorException.java index 51d542622da..fe19da0c41c 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorException.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorException.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.orchestrator; +package com.yahoo.vespa.hosted.node.admin.configserver.orchestrator; @SuppressWarnings("serial") public class OrchestratorException extends RuntimeException { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorImpl.java index 7093f3f12e7..0409004c6e6 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorImpl.java @@ -1,9 +1,9 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.orchestrator; +package com.yahoo.vespa.hosted.node.admin.configserver.orchestrator; -import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; -import com.yahoo.vespa.hosted.node.admin.util.HttpException; +import com.yahoo.vespa.hosted.node.admin.configserver.HttpException; import com.yahoo.vespa.orchestrator.restapi.HostApi; import com.yahoo.vespa.orchestrator.restapi.HostSuspensionApi; import com.yahoo.vespa.orchestrator.restapi.wire.BatchHostSuspendRequest; @@ -25,17 +25,17 @@ public class OrchestratorImpl implements Orchestrator { static final String ORCHESTRATOR_PATH_PREFIX_HOST_SUSPENSION_API = ORCHESTRATOR_PATH_PREFIX + HostSuspensionApi.PATH_PREFIX; - private final ConfigServerHttpRequestExecutor requestExecutor; + private final ConfigServerApi configServerApi; - public OrchestratorImpl(ConfigServerHttpRequestExecutor requestExecutor) { - this.requestExecutor = requestExecutor; + public OrchestratorImpl(ConfigServerApi configServerApi) { + this.configServerApi = configServerApi; } @Override public void suspend(final String hostName) { UpdateHostResponse response; try { - response = requestExecutor.put(getSuspendPath(hostName), + response = configServerApi.put(getSuspendPath(hostName), Optional.empty(), /* body */ UpdateHostResponse.class); } catch (HttpException.NotFoundException n) { @@ -56,7 +56,7 @@ public class OrchestratorImpl implements Orchestrator { public void suspend(String parentHostName, List<String> hostNames) { final BatchOperationResult batchOperationResult; try { - batchOperationResult = requestExecutor.put( + batchOperationResult = configServerApi.put( ORCHESTRATOR_PATH_PREFIX_HOST_SUSPENSION_API, Optional.of(new BatchHostSuspendRequest(parentHostName, hostNames)), BatchOperationResult.class); @@ -77,7 +77,7 @@ public class OrchestratorImpl implements Orchestrator { UpdateHostResponse response; try { String path = getSuspendPath(hostName); - response = requestExecutor.delete(path, UpdateHostResponse.class); + response = configServerApi.delete(path, UpdateHostResponse.class); } catch (HttpException.NotFoundException n) { throw new OrchestratorNotFoundException("Failed to resume " + hostName + ", host not found"); } catch (HttpException e) { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorNotFoundException.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorNotFoundException.java index 088152bd7a3..ac39f7c3280 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorNotFoundException.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorNotFoundException.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.orchestrator; +package com.yahoo.vespa.hosted.node.admin.configserver.orchestrator; @SuppressWarnings("serial") public class OrchestratorNotFoundException extends OrchestratorException { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java index 2947ef68ba4..a453ea46ffd 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java @@ -10,7 +10,7 @@ import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Chain; import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Command; import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.FlushCommand; import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.PolicyCommand; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import java.util.HashMap; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImpl.java index cdc7c9badaf..8f6e5bf9748 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImpl.java @@ -7,16 +7,15 @@ import com.yahoo.concurrent.classlock.ClassLocking; import com.yahoo.concurrent.classlock.LockInterruptException; import com.yahoo.log.LogLevel; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; -import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorException; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.OrchestratorException; import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; -import com.yahoo.vespa.hosted.node.admin.util.HttpException; +import com.yahoo.vespa.hosted.node.admin.configserver.HttpException; import com.yahoo.vespa.hosted.provision.Node; -import java.io.IOException; import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -269,35 +268,22 @@ public class NodeAdminStateUpdaterImpl implements NodeAdminStateUpdater { log.info("Frozen, skipping fetching info from node repository"); return; } - final List<ContainerNodeSpec> containersToRun; - try { - containersToRun = nodeRepository.getContainersToRun(dockerHostHostName); - } catch (Exception e) { - log.log(LogLevel.WARNING, "Failed fetching container info from node repository", e); - return; - } - if (containersToRun == null) { - log.warning("Got null from node repository"); - return; - } + try { + final List<ContainerNodeSpec> containersToRun = nodeRepository.getContainersToRun(dockerHostHostName); nodeAdmin.refreshContainersToRun(containersToRun); } catch (Exception e) { - log.log(LogLevel.WARNING, "Failed updating node admin: ", e); + log.log(LogLevel.WARNING, "Failed to update which containers should be running", e); } } } private List<String> getNodesInActiveState() { - try { - return nodeRepository.getContainersToRun(dockerHostHostName) - .stream() - .filter(nodespec -> nodespec.nodeState == Node.State.active) - .map(nodespec -> nodespec.hostname) - .collect(Collectors.toList()); - } catch (IOException e) { - throw new RuntimeException("Failed to get nodes from node repo", e); - } + return nodeRepository.getContainersToRun(dockerHostHostName) + .stream() + .filter(nodespec -> nodespec.nodeState == Node.State.active) + .map(nodespec -> nodespec.hostname) + .collect(Collectors.toList()); } public void start() { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java index f05a4054924..893054e5ac0 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java @@ -17,9 +17,9 @@ import com.yahoo.vespa.hosted.dockerapi.metrics.MetricReceiverWrapper; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; -import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorException; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.OrchestratorException; import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import com.yahoo.vespa.hosted.provision.Node; diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/KeyStoreOptions.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/KeyStoreOptions.java index 643abde101b..1115f6dca91 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/KeyStoreOptions.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/util/KeyStoreOptions.java @@ -1,16 +1,45 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.util; +import java.io.FileInputStream; +import java.io.IOException; import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.util.Optional; public class KeyStoreOptions { public final Path path; public final char[] password; public final String type; + private final Optional<String> provider; public KeyStoreOptions(Path path, char[] password, String type) { + this(path, password, type, null); + } + + public KeyStoreOptions(Path path, char[] password, String type, String provider) { this.path = path; this.password = password; this.type = type; + this.provider = Optional.ofNullable(provider); + } + + public KeyStore loadKeyStore() + throws IOException, NoSuchProviderException, KeyStoreException, CertificateException, NoSuchAlgorithmException { + try (FileInputStream in = new FileInputStream(path.toFile())) { + KeyStore keyStore = getKeyStoreInstance(); + keyStore.load(in, password); + return keyStore; + } + } + + public KeyStore getKeyStoreInstance() throws NoSuchProviderException, KeyStoreException { + return provider.isPresent() ? + KeyStore.getInstance(type, provider.get()) : + KeyStore.getInstance(type); } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/util/ConfigServerHttpRequestExecutorTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApiImplTest.java index 175d3a9a051..f39a64d2dee 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/util/ConfigServerHttpRequestExecutorTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/ConfigServerApiImplTest.java @@ -1,5 +1,5 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.util; +package com.yahoo.vespa.hosted.node.admin.configserver; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -34,7 +34,7 @@ import static org.mockito.Mockito.when; * * @author dybis */ -public class ConfigServerHttpRequestExecutorTest { +public class ConfigServerApiImplTest { @JsonIgnoreProperties(ignoreUnknown = true) public static class TestPojo { @@ -49,7 +49,7 @@ public class ConfigServerHttpRequestExecutorTest { private final List<URI> configServers = Arrays.asList(URI.create(uri1), URI.create(uri2)); private final StringBuilder mockLog = new StringBuilder(); - private ConfigServerHttpRequestExecutor executor; + private ConfigServerApiImpl executor; private int mockReturnCode = 200; @Before @@ -72,7 +72,7 @@ public class ConfigServerHttpRequestExecutorTest { return response; }); - executor = new ConfigServerHttpRequestExecutor(configServers, httpMock); + executor = new ConfigServerApiImpl(configServers, httpMock); } @Test diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java new file mode 100644 index 00000000000..f9f8b230154 --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/certificate/ConfigServerKeyStoreRefresherTest.java @@ -0,0 +1,162 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.node.admin.configserver.certificate; + +import com.yahoo.test.ManualClock; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; +import com.yahoo.vespa.hosted.node.admin.util.KeyStoreOptions; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.util.Date; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +/** + * @author freva + */ +public class ConfigServerKeyStoreRefresherTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private final ManualClock clock = new ManualClock(); + private final String commonName = "CertificateRefresherTest"; + private final Duration certificateExpiration = Duration.ofDays(6); + private final ConfigServerApi configServerApi = mock(ConfigServerApi.class); + private final Runnable keyStoreUpdatedCallback = mock(Runnable.class); + private final ScheduledExecutorService executor = mock(ScheduledExecutorService.class); + private KeyStoreOptions keyStoreOptions; + + @Before + public void setup() { + keyStoreOptions = new KeyStoreOptions( + tempFolder.getRoot().toPath().resolve("some/path/keystore.p12"), new char[0], "PKCS12", null); + } + + @Test + public void manually_trigger_certificate_refresh() throws Exception { + X509Certificate firstCertificate = mockConfigServerCertificateSigning(1); + + ConfigServerKeyStoreRefresher keyStoreRefresher = new ConfigServerKeyStoreRefresher( + keyStoreOptions, keyStoreUpdatedCallback, configServerApi, executor, clock, commonName); + + // No keystore previously existed, so a new one should be written + assertTrue(keyStoreRefresher.refreshKeyStoreIfNeeded()); + assertEquals(firstCertificate, keyStoreRefresher.getConfigServerCertificate()); + + // Calling it again before a third of certificate lifetime has passed has no effect + assertFalse(keyStoreRefresher.refreshKeyStoreIfNeeded()); + assertEquals(firstCertificate, keyStoreRefresher.getConfigServerCertificate()); + + // After a third of the expiration time passes, we should refresh the certificate + clock.advance(certificateExpiration.dividedBy(3).plusSeconds(1)); + X509Certificate secondCertificate = mockConfigServerCertificateSigning(2); + assertTrue(keyStoreRefresher.refreshKeyStoreIfNeeded()); + assertEquals(secondCertificate, keyStoreRefresher.getConfigServerCertificate()); + + verify(configServerApi, times(2)) + .post(eq(ConfigServerKeyStoreRefresher.CONFIG_SERVER_CERTIFICATE_SIGNING_PATH), any(), any()); + + // We're just triggering refresh manually, so callback and executor should not have been touched + verifyZeroInteractions(keyStoreUpdatedCallback); + verifyZeroInteractions(executor); + } + + @Test + public void certificate_refresh_schedule_test() throws Exception { + ConfigServerKeyStoreRefresher keyStoreRefresher = new ConfigServerKeyStoreRefresher( + keyStoreOptions, keyStoreUpdatedCallback, configServerApi, executor, clock, commonName); + + // No keystore exist, so refresh once + mockConfigServerCertificateSigning(1); + assertTrue(keyStoreRefresher.refreshKeyStoreIfNeeded()); + + // Start automatic refreshment, since keystore was just written, next check should be in 1/3rd of + // certificate lifetime, which is in 2 days. + keyStoreRefresher.start(); + Duration nextExpectedExecution = Duration.ofDays(2); + verify(executor, times(1)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS)); + + // First automatic refreshment goes without any problems + clock.advance(nextExpectedExecution); + mockConfigServerCertificateSigning(2); + keyStoreRefresher.refresh(); + verify(executor, times(2)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS)); + verify(keyStoreUpdatedCallback).run(); + + // We fail to refresh the certificate, wait minimum amount of time and try again + clock.advance(nextExpectedExecution); + mockConfigServerCertificateSigningFailure(new RuntimeException()); + keyStoreRefresher.refresh(); + nextExpectedExecution = Duration.ofSeconds(ConfigServerKeyStoreRefresher.MINIMUM_SECONDS_BETWEEN_REFRESH_RETRY); + verify(executor, times(1)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS)); + + clock.advance(nextExpectedExecution); + keyStoreRefresher.refresh(); + verify(executor, times(2)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS)); + verifyNoMoreInteractions(keyStoreUpdatedCallback); // Callback not called after the last 2 failures + + clock.advance(nextExpectedExecution); + mockConfigServerCertificateSigning(3); + keyStoreRefresher.refresh(); + nextExpectedExecution = Duration.ofDays(2); + verify(executor, times(3)).schedule(any(Runnable.class), eq(nextExpectedExecution.getSeconds()), eq(TimeUnit.SECONDS)); + verify(keyStoreUpdatedCallback, times(2)).run(); + } + + private X509Certificate mockConfigServerCertificateSigning(int serial) throws Exception { + X509Certificate certificate = makeCertificate(serial); + + when(configServerApi.post(eq(ConfigServerKeyStoreRefresher.CONFIG_SERVER_CERTIFICATE_SIGNING_PATH), any(), any())) + .thenReturn(new CertificateSerializedPayload(certificate)); + return certificate; + } + + private void mockConfigServerCertificateSigningFailure(Exception exception) throws Exception { + when(configServerApi.post(eq(ConfigServerKeyStoreRefresher.CONFIG_SERVER_CERTIFICATE_SIGNING_PATH), any(), any())) + .thenThrow(exception); + } + + private X509Certificate makeCertificate(int serial) throws Exception { + try { + KeyPair keyPair = ConfigServerKeyStoreRefresher.generateKeyPair(); + X500Name subject = new X500Name("CN=" + commonName); + Date notBefore = Date.from(clock.instant()); + Date notAfter = Date.from(clock.instant().plus(certificateExpiration)); + + JcaX509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(subject, + BigInteger.valueOf(serial), notBefore, notAfter, subject, keyPair.getPublic()); + ContentSigner sigGen = new JcaContentSignerBuilder(ConfigServerKeyStoreRefresher.SIGNER_ALGORITHM) + .build(keyPair.getPrivate()); + return new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()) + .getCertificate(certGen.build(sigGen)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +}
\ No newline at end of file diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepositoryImplTest.java index 949b4ccdf78..85e101714e8 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/noderepository/NodeRepositoryImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepositoryImplTest.java @@ -1,13 +1,13 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.noderepository; +package com.yahoo.vespa.hosted.node.admin.configserver.noderepository; import com.yahoo.application.Networking; import com.yahoo.application.container.JDisc; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.dockerapi.DockerImage; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApiImpl; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; -import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.testutils.ContainerConfig; @@ -36,7 +36,7 @@ import static org.junit.Assert.fail; */ public class NodeRepositoryImplTest { private JDisc container; - private ConfigServerHttpRequestExecutor requestExecutor; + private ConfigServerApiImpl configServerApi; private int findRandomOpenPort() throws IOException { @@ -63,8 +63,7 @@ public class NodeRepositoryImplTest { try { final int port = findRandomOpenPort(); container = JDisc.fromServicesXml(ContainerConfig.servicesXmlV2(port), Networking.enable); - requestExecutor = ConfigServerHttpRequestExecutor.create( - Collections.singleton(URI.create("http://127.0.0.1:" + port)), Optional.empty(), Optional.empty(), Optional.empty()); + configServerApi = new ConfigServerApiImpl(Collections.singleton(URI.create("http://127.0.0.1:" + port))); return; } catch (RuntimeException e) { lastException = e; @@ -75,7 +74,7 @@ public class NodeRepositoryImplTest { private void waitForJdiscContainerToServe() throws InterruptedException { Instant start = Instant.now(); - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); while (Instant.now().minusSeconds(120).isBefore(start)) { try { nodeRepositoryApi.getContainersToRun("foobar"); @@ -95,9 +94,9 @@ public class NodeRepositoryImplTest { } @Test - public void testGetContainersToRunApi() throws IOException, InterruptedException { + public void testGetContainersToRunApi() throws InterruptedException { waitForJdiscContainerToServe(); - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); String dockerHostHostname = "dockerhost1.yahoo.com"; final List<ContainerNodeSpec> containersToRun = nodeRepositoryApi.getContainersToRun(dockerHostHostname); @@ -116,7 +115,7 @@ public class NodeRepositoryImplTest { @Test public void testGetContainer() throws InterruptedException, IOException { waitForJdiscContainerToServe(); - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); String hostname = "host4.yahoo.com"; Optional<ContainerNodeSpec> nodeSpec = nodeRepositoryApi.getContainerNodeSpec(hostname); assertThat(nodeSpec.isPresent(), is(true)); @@ -126,7 +125,7 @@ public class NodeRepositoryImplTest { @Test public void testGetContainerForNonExistingNode() throws InterruptedException, IOException { waitForJdiscContainerToServe(); - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); String hostname = "host-that-does-not-exist"; Optional<ContainerNodeSpec> nodeSpec = nodeRepositoryApi.getContainerNodeSpec(hostname); assertFalse(nodeSpec.isPresent()); @@ -135,7 +134,7 @@ public class NodeRepositoryImplTest { @Test public void testUpdateNodeAttributes() throws InterruptedException, IOException { waitForJdiscContainerToServe(); - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); String hostname = "host4.yahoo.com"; nodeRepositoryApi.updateNodeAttributes( hostname, @@ -148,7 +147,7 @@ public class NodeRepositoryImplTest { @Test(expected = RuntimeException.class) public void testUpdateNodeAttributesWithBadValue() throws InterruptedException, IOException { waitForJdiscContainerToServe(); - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); String hostname = "host4.yahoo.com"; nodeRepositoryApi.updateNodeAttributes( hostname, @@ -160,7 +159,7 @@ public class NodeRepositoryImplTest { @Test public void testMarkAsReady() throws InterruptedException, IOException { - NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(requestExecutor); + NodeRepository nodeRepositoryApi = new NodeRepositoryImpl(configServerApi); waitForJdiscContainerToServe(); nodeRepositoryApi.markAsDirty("host5.yahoo.com"); diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorImplTest.java index 779a0a6a376..2d355c93c09 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/orchestrator/OrchestratorImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/configserver/orchestrator/OrchestratorImplTest.java @@ -1,8 +1,8 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.orchestrator; +package com.yahoo.vespa.hosted.node.admin.configserver.orchestrator; -import com.yahoo.vespa.hosted.node.admin.util.ConfigServerHttpRequestExecutor; -import com.yahoo.vespa.hosted.node.admin.util.HttpException; +import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApiImpl; +import com.yahoo.vespa.hosted.node.admin.configserver.HttpException; import com.yahoo.vespa.orchestrator.restapi.wire.BatchHostSuspendRequest; import com.yahoo.vespa.orchestrator.restapi.wire.BatchOperationResult; import com.yahoo.vespa.orchestrator.restapi.wire.HostStateChangeDenialReason; @@ -23,12 +23,12 @@ import static org.mockito.Mockito.when; public class OrchestratorImplTest { private static final String hostName = "host123.yahoo.com"; - private final ConfigServerHttpRequestExecutor requestExecutor = mock(ConfigServerHttpRequestExecutor.class); - private final OrchestratorImpl orchestrator = new OrchestratorImpl(requestExecutor); + private final ConfigServerApiImpl configServerApi = mock(ConfigServerApiImpl.class); + private final OrchestratorImpl orchestrator = new OrchestratorImpl(configServerApi); @Test public void testSuspendCall() { - when(requestExecutor.put( + when(configServerApi.put( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_API + "/" + hostName+ "/suspended", Optional.empty(), UpdateHostResponse.class @@ -39,7 +39,7 @@ public class OrchestratorImplTest { @Test(expected=OrchestratorException.class) public void testSuspendCallWithFailureReason() { - when(requestExecutor.put( + when(configServerApi.put( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_API + "/" + hostName+ "/suspended", Optional.empty(), UpdateHostResponse.class @@ -50,7 +50,7 @@ public class OrchestratorImplTest { @Test(expected=OrchestratorNotFoundException.class) public void testSuspendCallWithNotFound() { - when(requestExecutor.put( + when(configServerApi.put( any(String.class), any(), any() @@ -61,7 +61,7 @@ public class OrchestratorImplTest { @Test(expected=RuntimeException.class) public void testSuspendCallWithSomeOtherException() { - when(requestExecutor.put( + when(configServerApi.put( any(String.class), any(), any() @@ -73,7 +73,7 @@ public class OrchestratorImplTest { @Test public void testResumeCall() { - when(requestExecutor.delete( + when(configServerApi.delete( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_API + "/" + hostName+ "/suspended", UpdateHostResponse.class )).thenReturn(new UpdateHostResponse(hostName, null)); @@ -83,7 +83,7 @@ public class OrchestratorImplTest { @Test(expected=OrchestratorException.class) public void testResumeCallWithFailureReason() { - when(requestExecutor.delete( + when(configServerApi.delete( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_API + "/" + hostName+ "/suspended", UpdateHostResponse.class )).thenReturn(new UpdateHostResponse(hostName, new HostStateChangeDenialReason("hostname", "fail"))); @@ -93,7 +93,7 @@ public class OrchestratorImplTest { @Test(expected=OrchestratorNotFoundException.class) public void testResumeCallWithNotFound() { - when(requestExecutor.delete( + when(configServerApi.delete( any(String.class), any() )).thenThrow(new HttpException.NotFoundException("Not Found")); @@ -103,7 +103,7 @@ public class OrchestratorImplTest { @Test(expected=RuntimeException.class) public void testResumeCallWithSomeOtherException() { - when(requestExecutor.put( + when(configServerApi.put( any(String.class), any(), any() @@ -118,7 +118,7 @@ public class OrchestratorImplTest { String parentHostName = "host1.test.yahoo.com"; List<String> hostNames = Arrays.asList("a1.host1.test.yahoo.com", "a2.host1.test.yahoo.com"); - when(requestExecutor.put( + when(configServerApi.put( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_SUSPENSION_API, Optional.of(new BatchHostSuspendRequest(parentHostName, hostNames)), BatchOperationResult.class @@ -133,7 +133,7 @@ public class OrchestratorImplTest { List<String> hostNames = Arrays.asList("a1.host1.test.yahoo.com", "a2.host1.test.yahoo.com"); String failureReason = "Failed to suspend"; - when(requestExecutor.put( + when(configServerApi.put( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_SUSPENSION_API, Optional.of(new BatchHostSuspendRequest(parentHostName, hostNames)), BatchOperationResult.class @@ -148,7 +148,7 @@ public class OrchestratorImplTest { List<String> hostNames = Arrays.asList("a1.host1.test.yahoo.com", "a2.host1.test.yahoo.com"); String exceptionMessage = "Exception: Something crashed!"; - when(requestExecutor.put( + when(configServerApi.put( OrchestratorImpl.ORCHESTRATOR_PATH_PREFIX_HOST_SUSPENSION_API, Optional.of(new BatchHostSuspendRequest(parentHostName, hostNames)), BatchOperationResult.class diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java index 2d2a622f8f0..8557da75ee9 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java @@ -4,10 +4,9 @@ package com.yahoo.vespa.hosted.node.admin.integrationTests; import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; import com.yahoo.vespa.hosted.provision.Node; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -32,7 +31,7 @@ public class NodeRepoMock implements NodeRepository { } @Override - public List<ContainerNodeSpec> getContainersToRun(String dockerHostHostname) throws IOException { + public List<ContainerNodeSpec> getContainersToRun(String dockerHostHostname) { synchronized (monitor) { return new ArrayList<>(containerNodeSpecsByHostname.values()); } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/OrchestratorMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/OrchestratorMock.java index dc285bb27ce..469022cec56 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/OrchestratorMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/OrchestratorMock.java @@ -1,7 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.integrationTests; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; import java.util.List; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java index e4b6558e8e1..3a163c94caa 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/RunInContainerTest.java @@ -18,9 +18,9 @@ import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminImpl; import com.yahoo.vespa.hosted.node.admin.nodeadmin.NodeAdminStateUpdaterImpl; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgent; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentImpl; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; -import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorException; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.OrchestratorException; import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.provision.Node; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java index a699377b4c3..d50f869617a 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java @@ -6,7 +6,7 @@ import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.DockerImage; import com.yahoo.vespa.hosted.node.admin.ContainerAclSpec; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; import org.junit.Before; import org.junit.Test; import org.mockito.verification.VerificationMode; diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImplTest.java index 1ab24fe8f9a..c9985247018 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeadmin/NodeAdminStateUpdaterImplTest.java @@ -4,14 +4,13 @@ package com.yahoo.vespa.hosted.node.admin.nodeadmin; import com.yahoo.test.ManualClock; import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; -import com.yahoo.vespa.hosted.node.admin.orchestrator.OrchestratorException; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.OrchestratorException; import com.yahoo.vespa.hosted.node.admin.provider.NodeAdminStateUpdater; import com.yahoo.vespa.hosted.provision.Node; import org.junit.Test; -import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -53,7 +52,7 @@ public class NodeAdminStateUpdaterImplTest { @Test - public void testStateConvergence() throws IOException { + public void testStateConvergence() { mockNodeRepo(4); List<String> activeHostnames = nodeRepository.getContainersToRun(parentHostname).stream() .map(node -> node.hostname) @@ -155,7 +154,7 @@ public class NodeAdminStateUpdaterImplTest { } @Test - public void half_transition_revert() throws IOException { + public void half_transition_revert() { mockNodeRepo(3); // Initially everything is frozen to force convergence @@ -182,7 +181,7 @@ public class NodeAdminStateUpdaterImplTest { verify(nodeAdmin, times(2)).setFrozen(eq(false)); // Make sure that we unfreeze! } - private void mockNodeRepo(int numberOfNodes) throws IOException { + private void mockNodeRepo(int numberOfNodes) { List<ContainerNodeSpec> containersToRun = IntStream.range(0, numberOfNodes) .mapToObj(i -> new ContainerNodeSpec.Builder() .hostname("host" + i + ".test.yahoo.com") diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java index f1852fda8a3..fb9303ea382 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java @@ -15,8 +15,8 @@ import com.yahoo.vespa.hosted.node.admin.ContainerNodeSpec; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer; -import com.yahoo.vespa.hosted.node.admin.noderepository.NodeRepository; -import com.yahoo.vespa.hosted.node.admin.orchestrator.Orchestrator; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.configserver.orchestrator.Orchestrator; import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.node.admin.util.InetAddressResolver; import com.yahoo.vespa.hosted.node.admin.component.PathResolver; @@ -95,7 +95,7 @@ public class NodeAgentImplTest { @Test - public void upToDateContainerIsUntouched() throws Exception { + public void upToDateContainerIsUntouched() { final long restartGeneration = 1; final long rebootGeneration = 0; final ContainerNodeSpec nodeSpec = nodeSpecBuilder @@ -135,7 +135,7 @@ public class NodeAgentImplTest { } @Test - public void verifyRemoveOldFilesIfDiskFull() throws Exception { + public void verifyRemoveOldFilesIfDiskFull() { final long restartGeneration = 1; final long rebootGeneration = 0; final ContainerNodeSpec nodeSpec = nodeSpecBuilder @@ -199,7 +199,7 @@ public class NodeAgentImplTest { } @Test - public void containerIsNotStoppedIfNewImageMustBePulled() throws Exception { + public void containerIsNotStoppedIfNewImageMustBePulled() { final DockerImage newDockerImage = new DockerImage("new-image"); final long wantedRestartGeneration = 2; final long currentRestartGeneration = 1; @@ -230,7 +230,7 @@ public class NodeAgentImplTest { } @Test - public void containerIsRestartedIfFlavorChanged() throws Exception { + public void containerIsRestartedIfFlavorChanged() { final long wantedRestartGeneration = 1; final long currentRestartGeneration = 1; ContainerNodeSpec.Builder specBuilder = nodeSpecBuilder @@ -268,7 +268,7 @@ public class NodeAgentImplTest { } @Test - public void noRestartIfOrchestratorSuspendFails() throws Exception { + public void noRestartIfOrchestratorSuspendFails() { final long wantedRestartGeneration = 2; final long currentRestartGeneration = 1; final ContainerNodeSpec nodeSpec = nodeSpecBuilder @@ -294,7 +294,7 @@ public class NodeAgentImplTest { } @Test - public void failedNodeRunningContainerShouldStillBeRunning() throws Exception { + public void failedNodeRunningContainerShouldStillBeRunning() { final long restartGeneration = 1; final long rebootGeneration = 0; final ContainerNodeSpec nodeSpec = nodeSpecBuilder @@ -324,7 +324,7 @@ public class NodeAgentImplTest { } @Test - public void readyNodeLeadsToNoAction() throws Exception { + public void readyNodeLeadsToNoAction() { final long restartGeneration = 1; final long rebootGeneration = 0; final ContainerNodeSpec nodeSpec = nodeSpecBuilder @@ -356,7 +356,7 @@ public class NodeAgentImplTest { } @Test - public void inactiveNodeRunningContainerShouldStillBeRunning() throws Exception { + public void inactiveNodeRunningContainerShouldStillBeRunning() { final long restartGeneration = 1; final long rebootGeneration = 0; @@ -390,7 +390,7 @@ public class NodeAgentImplTest { } @Test - public void reservedNodeDoesNotUpdateNodeRepoWithVersion() throws Exception { + public void reservedNodeDoesNotUpdateNodeRepoWithVersion() { final long restartGeneration = 1; final long rebootGeneration = 0; @@ -462,7 +462,7 @@ public class NodeAgentImplTest { } @Test - public void provisionedNodeIsMarkedAsDirty() throws Exception { + public void provisionedNodeIsMarkedAsDirty() { final ContainerNodeSpec nodeSpec = nodeSpecBuilder .wantedDockerImage(dockerImage) .nodeState(Node.State.provisioned) @@ -497,7 +497,7 @@ public class NodeAgentImplTest { } @Test - public void resumeProgramRunsUntilSuccess() throws Exception { + public void resumeProgramRunsUntilSuccess() { final long restartGeneration = 1; final ContainerNodeSpec nodeSpec = nodeSpecBuilder .wantedDockerImage(dockerImage) @@ -626,7 +626,7 @@ public class NodeAgentImplTest { } @Test - public void testGetRelevantMetricsForReadyNode() throws Exception { + public void testGetRelevantMetricsForReadyNode() { final ContainerNodeSpec nodeSpec = nodeSpecBuilder .nodeState(Node.State.ready) .build(); |