From 17680e5bd51252b282e011e4f9929653f78be016 Mon Sep 17 00:00:00 2001 From: Morten Tokle Date: Thu, 25 May 2023 14:24:52 +0200 Subject: Configure data plane proxy --- configdefinitions/src/vespa/dataplane-proxy.def | 8 +- .../jdisc/DataplaneProxyConfigurator.java | 23 +++ .../container/jdisc/DataplaneProxyService.java | 173 +++++++++++++++++++++ 3 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyConfigurator.java create mode 100644 container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java diff --git a/configdefinitions/src/vespa/dataplane-proxy.def b/configdefinitions/src/vespa/dataplane-proxy.def index bef2c8457f7..dab54a8b978 100644 --- a/configdefinitions/src/vespa/dataplane-proxy.def +++ b/configdefinitions/src/vespa/dataplane-proxy.def @@ -1,4 +1,10 @@ # Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. namespace=cloud.config -port int \ No newline at end of file +port int +clientCertificate string +clientKey string +serverCertificate string +serverKey string +mTlsEndpoint string +tokenEndpoint string diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyConfigurator.java b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyConfigurator.java new file mode 100644 index 00000000000..4c637b5798d --- /dev/null +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyConfigurator.java @@ -0,0 +1,23 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.jdisc; + +import com.yahoo.cloud.config.DataplaneProxyConfig; +import com.yahoo.component.AbstractComponent; +import com.yahoo.jdisc.http.ConnectorConfig; + +/** + * Reconfigurable component for supporting data plane proxy. Configures the {@code DataplaneProxyService} by calling {@code DataplaneProxyService#init} + * + * @author mortent + */ +public class DataplaneProxyConfigurator extends AbstractComponent { + + public DataplaneProxyConfigurator(DataplaneProxyConfig config, DataplaneProxyService dataplaneProxyService) { + dataplaneProxyService.reconfigure(config); + } + + @Override + public void deconstruct() { + super.deconstruct(); + } +} diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java new file mode 100644 index 00000000000..5b0cdfaf630 --- /dev/null +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java @@ -0,0 +1,173 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.jdisc; + +import com.yahoo.cloud.config.DataplaneProxyConfig; +import com.yahoo.component.AbstractComponent; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +/** + * Configures a data plane proxy. Currently using Nginx. + * + * @author mortent + */ +public class DataplaneProxyService extends AbstractComponent { + + private static final String PREFIX = "/opt/vespa"; + private static final Path CONFIG_TEMPLATE = Paths.get(PREFIX, "conf/nginx/nginx.conf.template"); + + private static final Path clientCertificateFile = Paths.get(PREFIX, "conf/nginx/client_cert.pem"); + private static final Path clientKeyFile = Paths.get(PREFIX, "conf/nginx/client_key.pem"); + private static final Path serverCertificateFile = Paths.get(PREFIX, "conf/nginx/server_cert.pem"); + private static final Path serverKeyFile = Paths.get(PREFIX, "conf/nginx/server_key.pem"); + + private static final Path nginxConf = Paths.get(PREFIX, "conf/nginx/nginx.conf"); + + private boolean started; + + @Inject + public DataplaneProxyService() { + this.started = false; + } + + public void reconfigure(DataplaneProxyConfig config) { + try { + String serverCert = config.serverCertificate(); + String serverKey = config.serverKey(); + String clientCert = config.clientCertificate(); + String clientKey = config.clientKey(); + + boolean configChanged = false; + configChanged |= writeFile(clientCertificateFile, clientCert); + configChanged |= writeFile(clientKeyFile, clientKey); + configChanged |= writeFile(serverCertificateFile, serverCert); + configChanged |= writeFile(serverKeyFile, serverKey); + configChanged |= writeFile(nginxConf, + nginxConfig( + clientCertificateFile, + clientKeyFile, + serverCertificateFile, + serverKeyFile, + URI.create(config.mTlsEndpoint()), + URI.create(config.tokenEndpoint()), + config.port(), + PREFIX + )); + if (!started) { + startNginx(); + started = true; + } else if (configChanged){ + reloadNginx(); + } + } catch (IOException e) { + throw new RuntimeException("Error reconfiguring data plane proxy", e); + } + } + + private void startNginx() { + try { + Process startCommand = new ProcessBuilder().command( + "nginx", + "-c", nginxConf.toString() + ).start(); + int exitCode = startCommand.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("Non-zero exitcode from nginx: %d".formatted(exitCode)); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Could not start nginx", e); + } + } + + private void reloadNginx() { + try { + Process reloadCommand = new ProcessBuilder().command( + "nginx", + "-s", "reload" + ).start(); + int exitCode = reloadCommand.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("Non-zero exitcode from nginx: %d".formatted(exitCode)); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Could not start nginx", e); + } + } + + private void stopNginx() { + try { + Process stopCommand = new ProcessBuilder().command( + "nginx", + "-s", "reload" + ).start(); + int exitCode = stopCommand.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("Non-zero exitcode from nginx: %d".formatted(exitCode)); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Could not start nginx", e); + } + } + + @Override + public void deconstruct() { + super.deconstruct(); + stopNginx(); + } + + /* + * Writes a file to disk + * return true if file was changed, false if no changes + */ + private boolean writeFile(Path file, String contents) throws IOException { + Path tempPath = Paths.get(file.toFile().getAbsolutePath() + ".new"); + Files.createDirectories(tempPath.getParent()); + Files.writeString(tempPath, contents); + + if (!Files.exists(file) || Files.mismatch(tempPath, file) > 0) { + Files.move(tempPath, file, StandardCopyOption.REPLACE_EXISTING); + return true; + } else { + Files.delete(tempPath); + return false; + } + } + + static String nginxConfig( + Path clientCert, + Path clientKey, + Path serverCert, + Path serverKey, + URI mTlsEndpoint, + URI tokenEndpoint, + int vespaPort, + String prefix) { + + try { + String nginxTemplate = Files.readString(CONFIG_TEMPLATE); + nginxTemplate = replace(nginxTemplate, "client_cert", clientCert.toString()); + nginxTemplate = replace(nginxTemplate, "client_key", clientKey.toString()); + nginxTemplate = replace(nginxTemplate, "server_cert", serverCert.toString()); + nginxTemplate = replace(nginxTemplate, "server_key", serverKey.toString()); + nginxTemplate = replace(nginxTemplate, "mtls_endpoint", mTlsEndpoint.getHost()); + nginxTemplate = replace(nginxTemplate, "token_endpoint", tokenEndpoint.getHost()); + nginxTemplate = replace(nginxTemplate, "vespa_port", Integer.toString(vespaPort)); + nginxTemplate = replace(nginxTemplate, "prefix", prefix); + + // TODO: verify that all template vars have been expanded + return nginxTemplate; + } catch (IOException e) { + throw new IllegalArgumentException("Could not create data plane proxy configuration", e); + } + } + + private static String replace(String template, String key, String value) { + return template.replaceAll("\\$\\{%s\\}".formatted(key), value); + } +} -- cgit v1.2.3