diff options
author | Jon Marius Venstad <jvenstad@yahoo-inc.com> | 2019-06-13 13:52:18 +0200 |
---|---|---|
committer | Jon Marius Venstad <jvenstad@yahoo-inc.com> | 2019-06-13 13:52:18 +0200 |
commit | 2bb71582b979a83a39260f3f11466737b94ee47f (patch) | |
tree | ac4d17548ae440348a7ecd62bbd87cfa998c6496 /hosted-api | |
parent | 7d2366a939f64f964a208e01c4455dd530f833c6 (diff) |
Wrap TestConfig in TestRuntime, which includes and Authenticator, and obtain the former from controller
Diffstat (limited to 'hosted-api')
4 files changed, 179 insertions, 12 deletions
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/Authenticator.java b/hosted-api/src/main/java/ai/vespa/hosted/api/Authenticator.java new file mode 100644 index 00000000000..acd8a215e7f --- /dev/null +++ b/hosted-api/src/main/java/ai/vespa/hosted/api/Authenticator.java @@ -0,0 +1,23 @@ +package ai.vespa.hosted.api; + +import javax.net.ssl.SSLContext; +import java.net.http.HttpRequest; +import java.util.Optional; + +/** + * Adds environment dependent authentication to HTTP request against hosted Vespa API and deployments. + * + * @author jonmv + */ +public interface Authenticator { + + /** Returns an SSLContext which provides authentication against a Vespa endpoint. */ + SSLContext sslContext(); + + /** Adds necessary authentication to the given HTTP request builder, to pass the data plane of a Vespa endpoint. */ + HttpRequest.Builder authenticated(HttpRequest.Builder request); + + /** Returns a client authenticated to talk to the hosted Vespa API. */ + ControllerHttpClient controller(); + +} diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java b/hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java index d59eb166e2b..153a03e56d2 100644 --- a/hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java +++ b/hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java @@ -104,14 +104,14 @@ public abstract class ControllerHttpClient { /** Deactivates the deployment of the given application in the given zone. */ public String deactivate(ApplicationId id, ZoneId zone) { - return asText(send(request(HttpRequest.newBuilder(deploymentPath(id, zone)) - .timeout(Duration.ofSeconds(10)), - DELETE))); + return asString(send(request(HttpRequest.newBuilder(deploymentPath(id, zone)) + .timeout(Duration.ofSeconds(10)), + DELETE))); } - /** Returns the default {@link Environment#dev} {@link ZoneId}, to use for development deployments. */ - public ZoneId devZone() { - Inspector rootObject = toInspector(send(request(HttpRequest.newBuilder(defaultRegionPath()) + /** Returns the default {@link ZoneId} for the given environment, if any. */ + public ZoneId defaultZone(Environment environment) { + Inspector rootObject = toInspector(send(request(HttpRequest.newBuilder(defaultRegionPath(environment)) .timeout(Duration.ofSeconds(10)), GET))); return ZoneId.from("dev", rootObject.field("name").asString()); @@ -125,6 +125,11 @@ public abstract class ControllerHttpClient { .field("compileVersion").asString(); } + /** Returns the test config for functional and verification tests of the indicated Vespa deployment. */ + public TestConfig testConfig(ApplicationId id, ZoneId zone) { + return TestConfig.fromJson(asBytes(send(request(HttpRequest.newBuilder(testConfigPath(id, zone)), GET)))); + } + /** Returns the sorted list of log entries after the given after from the deployment job of the given ids. */ public DeploymentLog deploymentLog(ApplicationId id, ZoneId zone, long run, long after) { return toDeploymentLog(send(request(HttpRequest.newBuilder(runPath(id, zone, run, after)) @@ -137,6 +142,7 @@ public abstract class ControllerHttpClient { return deploymentLog(id, zone, run, -1); } + /** Returns an authenticated request from the given input. Override this for, e.g., request signing. */ protected HttpRequest request(HttpRequest.Builder request, Method method, Supplier<InputStream> data) { return request.method(method.name(), ofInputStream(data)).build(); } @@ -181,15 +187,22 @@ public abstract class ControllerHttpClient { "deploy", jobNameOf(zone)); } + private URI jobPath(ApplicationId id, ZoneId zone) { + return concatenated(instancePath(id), "job", jobNameOf(zone)); + } + + private URI testConfigPath(ApplicationId id, ZoneId zone) { + return concatenated(instancePath(id), "test-config"); + } + private URI runPath(ApplicationId id, ZoneId zone, long run, long after) { - return withQuery(concatenated(instancePath(id), - "job", jobNameOf(zone), + return withQuery(concatenated(jobPath(id, zone), "run", Long.toString(run)), "after", Long.toString(after)); } - private URI defaultRegionPath() { - return concatenated(endpoint, "zone", "v1", "environment", Environment.dev.value(), "default"); + private URI defaultRegionPath(Environment environment) { + return concatenated(endpoint, "zone", "v1", "environment", environment.value(), "default"); } private static URI concatenated(URI base, String... parts) { @@ -247,9 +260,15 @@ public abstract class ControllerHttpClient { return streamer; } - private static String asText(HttpResponse<byte[]> response) { + /** Returns the response body as a String, or throws if the status code is non-2XX. */ + private static String asString(HttpResponse<byte[]> response) { + return new String(asBytes(response), UTF_8); + } + + /** Returns the response body as a byte array, or throws if the status code is non-2XX. */ + private static byte[] asBytes(HttpResponse<byte[]> response) { toInspector(response); - return new String(response.body(), UTF_8); + return response.body(); } /** Returns an {@link Inspector} for the assumed JSON formatted response, or throws if the status code is non-2XX. */ diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java b/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java new file mode 100644 index 00000000000..61893a30e7e --- /dev/null +++ b/hosted-api/src/main/java/ai/vespa/hosted/api/Properties.java @@ -0,0 +1,55 @@ +package ai.vespa.hosted.api; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.RegionName; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +/** + * Utilities and common definitions of system properties defining a Vespa application project. + * + * @author jonmv + */ +public class Properties { + + public static ApplicationId application() { + return ApplicationId.from(requireNonBlankProperty("tenant"), + requireNonBlankProperty("application"), + getNonBlankProperty("instance").orElse("default")); + } + + public static Optional<Environment> environment() { + return getNonBlankProperty("environment").map(Environment::from); + } + + public static Optional<RegionName> region() { + return getNonBlankProperty("region").map(RegionName::from); + } + + public static URI endpoint() { + return URI.create(requireNonBlankProperty("endpoint")); + } + + public static Path privateKeyFile() { + return Paths.get(requireNonBlankProperty("privateKeyFile")); + } + + public static Path certificateFile() { + return Paths.get(requireNonBlankProperty("certificateFile")); + } + + /** Returns the system property with the given name if it is set, or empty. */ + public static Optional<String> getNonBlankProperty(String name) { + return Optional.ofNullable(System.getProperty(name)).filter(value -> ! value.isBlank()); + } + + /** Returns the system property with the given name if it is set, or throws. */ + public static String requireNonBlankProperty(String name) { + return getNonBlankProperty(name).orElseThrow(() -> new IllegalStateException("Missing required property '" + name + "'")); + } + +} diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java b/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java new file mode 100644 index 00000000000..a0c679f312e --- /dev/null +++ b/hosted-api/src/main/java/ai/vespa/hosted/api/TestConfig.java @@ -0,0 +1,70 @@ +package ai.vespa.hosted.api; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.zone.ZoneId; +import com.yahoo.slime.Inspector; +import com.yahoo.slime.JsonDecoder; +import com.yahoo.slime.ObjectTraverser; +import com.yahoo.slime.Slime; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; + +/** + * Config required to run a functional or verification test of a Vespa deployment. + * + * @author jvenstad + */ +public class TestConfig { + + private final ApplicationId application; + private final ZoneId zone; + private final SystemName system; + private final Map<ZoneId, Map<String, URI>> deployments; + + public TestConfig(ApplicationId application, ZoneId zone, SystemName system, Map<ZoneId, Map<String, URI>> deployments) { + if ( ! deployments.containsKey(zone)) + throw new IllegalArgumentException("Config must contain a deployment for its zone, but only does for " + deployments.keySet()); + this.application = requireNonNull(application); + this.zone = requireNonNull(zone); + this.system = requireNonNull(system); + this.deployments = deployments.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(), + entry -> Map.copyOf(entry.getValue()))); + } + + public static TestConfig fromJson(byte[] jsonBytes) { + Inspector config = new JsonDecoder().decode(new Slime(), jsonBytes).get(); + ApplicationId application = ApplicationId.fromSerializedForm(config.field("application").asString()); + ZoneId zone = ZoneId.from(config.field("zone").asString()); + SystemName system = SystemName.from(config.field("system").asString()); + Map<ZoneId, Map<String, URI>> deployments = new HashMap<>(); + config.field("clusterEndpoints").traverse((ObjectTraverser) (zoneId, endpointsObject) -> { + Map<String, URI> endpoints = new HashMap<>(); + endpointsObject.traverse((ObjectTraverser) (cluster, uri) -> endpoints.put(cluster, URI.create(uri.asString()))); + deployments.put(ZoneId.from(zoneId), endpoints); + }); + return new TestConfig(application, zone, system, deployments); + } + + /** Returns the full id of the application to test. */ + public ApplicationId application() { return application; } + + /** Returns the zone of the deployment to test. */ + public ZoneId zone() { return zone; } + + /** Returns an immutable view of deployments, per zone, of the application to test. */ + public Map<ZoneId, Map<String, URI>> allDeployments() { return deployments; } + + /** Returns the deployment to test in this test runtime. */ + public Map<String, URI> deploymentToTest() { return deployments.get(zone); } + + /** Returns the hosted Vespa system this is run against. */ + public SystemName system() { return system; } + +} |