summaryrefslogtreecommitdiffstats
path: root/hosted-api
diff options
context:
space:
mode:
authorJon Marius Venstad <jvenstad@yahoo-inc.com>2019-05-03 15:30:55 +0200
committerJon Marius Venstad <jvenstad@yahoo-inc.com>2019-05-03 15:38:09 +0200
commitfd0d8e218b4f9a7afb4bb983f7ee94e8e7e94b99 (patch)
tree99393753d103dc5044eea3d01b3371e43c2ce946 /hosted-api
parent93f18a00c63c6cfc8d2e1aebf2b5f7b67b91224b (diff)
Add ControllerHttpClient and Submission
Diffstat (limited to 'hosted-api')
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java121
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/Submission.java38
2 files changed, 159 insertions, 0 deletions
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
new file mode 100644
index 00000000000..94b7d8851a5
--- /dev/null
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java
@@ -0,0 +1,121 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.hosted.api;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.JsonDecoder;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+
+import static ai.vespa.hosted.api.Method.POST;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Talks to a remote controller over HTTP.
+ *
+ * Uses request signing with a public/private key pair to authenticate with the controller.
+ *
+ * @author jonmv
+ */
+public class ControllerHttpClient {
+
+ private final ApplicationId id;
+ private final RequestSigner signer;
+ private final URI endpoint;
+ private final HttpClient client;
+
+ /** Creates a HTTP client against the given endpoint, which uses the given key to authenticate as the given application. */
+ public ControllerHttpClient(URI endpoint, String privateKey, ApplicationId id) {
+ this.id = id;
+ this.signer = new RequestSigner(privateKey, id.serializedForm());
+ this.endpoint = endpoint.resolve("/");
+ this.client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(5))
+ .build();
+ }
+
+ /** Sends submission to the remote controller and returns the version of the accepted package, or throws if this fails. */
+ public String submit(Submission submission) {
+ HttpRequest request = signer.signed(HttpRequest.newBuilder(instancePath(id).resolve("submit"))
+ .timeout(Duration.ofMinutes(30)),
+ POST,
+ new MultiPartStreamer().addJson("submitOptions", metaToSlime(submission))
+ .addFile("applicationZip", submission.applicationZip())
+ .addFile("applicationTestZip", submission.applicationTestZip()));
+ try {
+ return toMessage(client.send(request, HttpResponse.BodyHandlers.ofByteArray()));
+ }
+ catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private URI apiPath() {
+ return concatenated(endpoint, "application", "v4");
+ }
+
+ private URI tenantPath(TenantName tenant) {
+ return concatenated(apiPath(), "tenant", tenant.value());
+ }
+
+ private URI applicationPath(TenantName tenant, ApplicationName application) {
+ return concatenated(tenantPath(tenant), "application", application.value());
+ }
+
+ private URI instancePath(ApplicationId id) {
+ return concatenated(applicationPath(id.tenant(), id.application()), "instance", id.instance().value());
+ }
+
+ private static URI concatenated(URI base, String... parts) {
+ return base.resolve(String.join("/", parts) + "/");
+ }
+
+ /** Returns a JSON representation of the submission meta data. */
+ private static String metaToSlime(Submission submission) {
+ try {
+ Slime slime = new Slime();
+ Cursor rootObject = slime.setObject();
+ rootObject.setString("repository", submission.repository());
+ rootObject.setString("branch", submission.branch());
+ rootObject.setString("commit", submission.commit());
+ rootObject.setString("authorEmail", submission.authorEmail());
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ new JsonFormat(true).encode(buffer, slime);
+ return buffer.toString(UTF_8);
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ /** Returns the "message" element contained in the JSON formatted response, if 2XX status code, or throws otherwise. */
+ private static String toMessage(HttpResponse<byte[]> response) {
+ Inspector rootObject = toSlime(response.body()).get();
+ if (response.statusCode() / 100 == 2)
+ return rootObject.field("message").asString();
+
+ else {
+ throw new RuntimeException(response.request() + " returned code " + response.statusCode() +
+ " (" + rootObject.field("error-code").asString() + "): " +
+ rootObject.field("message").asString());
+ }
+ }
+
+ private static Slime toSlime(byte[] data) {
+ return new JsonDecoder().decode(new Slime(), data);
+ }
+
+}
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/Submission.java b/hosted-api/src/main/java/ai/vespa/hosted/api/Submission.java
new file mode 100644
index 00000000000..f4cb90176da
--- /dev/null
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/Submission.java
@@ -0,0 +1,38 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.hosted.api;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Path;
+
+/**
+ * A submission intended for hosted Vespa containing an application package with tests and meta data.
+ *
+ * @author jonmv
+ */
+public class Submission {
+
+ private final String repository;
+ private final String branch;
+ private final String commit;
+ private final String authorEmail;
+ private final Path applicationZip;
+ private final Path applicationTestZip;
+
+ public Submission(String repository, String branch, String commit, String authorEmail, Path applicationZip, Path applicationTestZip) {
+ this.repository = repository;
+ this.branch = branch;
+ this.commit = commit;
+ this.authorEmail = authorEmail;
+ this.applicationZip = applicationZip;
+ this.applicationTestZip = applicationTestZip;
+ }
+
+ public String repository() { return repository; }
+ public String branch() { return branch; }
+ public String commit() { return commit; }
+ public String authorEmail() { return authorEmail; }
+ public Path applicationZip() { return applicationZip; }
+ public Path applicationTestZip() { return applicationTestZip; }
+
+}