summaryrefslogtreecommitdiffstats
path: root/hosted-api
diff options
context:
space:
mode:
authorJon Marius Venstad <jvenstad@yahoo-inc.com>2019-05-07 11:38:11 +0200
committerJon Marius Venstad <jvenstad@yahoo-inc.com>2019-05-07 11:38:11 +0200
commit272f8185a355f003ebc9255ee456e3c41523893b (patch)
treee2d5bfb23c249d54f13d49bba16997e10d1176f1 /hosted-api
parent1aad5e00519896846066fe525ab61f77f98f9049 (diff)
Support deployments through the ControllerHttpClient
Diffstat (limited to 'hosted-api')
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/ControllerHttpClient.java83
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/Deployment.java67
-rw-r--r--hosted-api/src/main/java/ai/vespa/hosted/api/DeploymentResult.java21
3 files changed, 171 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
index b4904f7915b..fe8ad66dcb7 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
@@ -3,7 +3,9 @@ package ai.vespa.hosted.api;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.security.SslContextBuilder;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
@@ -26,6 +28,8 @@ import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.function.Supplier;
+import static ai.vespa.hosted.api.Method.DELETE;
+import static ai.vespa.hosted.api.Method.GET;
import static ai.vespa.hosted.api.Method.POST;
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
import static java.net.http.HttpRequest.BodyPublishers.ofInputStream;
@@ -70,6 +74,28 @@ public abstract class ControllerHttpClient {
.addFile("applicationTestZip", submission.applicationTestZip()))));
}
+ /** Sends the given deployment to the given application in the given zone, or throws if this fails. */
+ public DeploymentResult deploy(Deployment deployment, ApplicationId id, ZoneId zone) {
+ return toDeploymentResult(send(request(HttpRequest.newBuilder(deploymentPath(id, zone))
+ .timeout(Duration.ofMinutes(60)),
+ POST,
+ toDataStream(deployment))));
+ }
+
+ public String deactivate(ApplicationId id, ZoneId zone) {
+ return asText(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())
+ .timeout(Duration.ofSeconds(10)),
+ GET)));
+ return ZoneId.from("dev", rootObject.field("name").asString());
+ }
+
protected HttpRequest request(HttpRequest.Builder request, Method method, Supplier<InputStream> data) {
return request.method(method.name(), ofInputStream(data)).build();
}
@@ -102,6 +128,17 @@ public abstract class ControllerHttpClient {
return concatenated(applicationPath(id.tenant(), id.application()), "instance", id.instance().value());
}
+ private URI deploymentPath(ApplicationId id, ZoneId zone) {
+ return concatenated(applicationPath(id.tenant(), id.application()),
+ "environment", zone.environment().value(),
+ "region", zone.region().value(),
+ "instance", id.instance().value());
+ }
+
+ private URI defaultRegionPath() {
+ return concatenated(endpoint, "zone", "v1", "environment", Environment.dev.value(), "default");
+ }
+
private static URI concatenated(URI base, String... parts) {
return base.resolve(String.join("/", parts) + "/");
}
@@ -119,6 +156,27 @@ public abstract class ControllerHttpClient {
}
}
+ /** Returns a JSON representation of the deployment meta data. */
+ private static String metaToJson(Deployment deployment) {
+ Slime slime = new Slime();
+ Cursor rootObject = slime.setObject();
+
+ if (deployment.repository().isPresent()) {
+ Cursor revisionObject = rootObject.setObject("sourceRevision");
+ deployment.repository().ifPresent(repository -> revisionObject.setString("repository", repository));
+ deployment.branch().ifPresent(branch -> revisionObject.setString("branch", branch));
+ deployment.commit().ifPresent(commit -> revisionObject.setString("commit", commit));
+ deployment.build().ifPresent(build -> rootObject.setLong("buildNumber", build));
+ }
+
+ deployment.version().ifPresent(version -> rootObject.setString("vespaVersion", version));
+
+ if (deployment.ignoreValidationErrors()) rootObject.setBool("ignoreValidationErrors", true);
+ rootObject.setBool("deployDirectly", true);
+
+ return toJson(slime);
+ }
+
/** Returns a JSON representation of the submission meta data. */
private static String metaToJson(Submission submission) {
Slime slime = new Slime();
@@ -130,6 +188,19 @@ public abstract class ControllerHttpClient {
return toJson(slime);
}
+ /** Returns a multi part data stream with meta data and, if contained in the deployment, an application package. */
+ private static MultiPartStreamer toDataStream(Deployment deployment) {
+ MultiPartStreamer streamer = new MultiPartStreamer();
+ streamer.addJson("deployOptions", metaToJson(deployment));
+ deployment.applicationZip().ifPresent(zip -> streamer.addFile("applicationZip", zip));
+ return streamer;
+ }
+
+ private static String asText(HttpResponse<byte[]> response) {
+ toInspector(response);
+ return new String(response.body(), UTF_8);
+ }
+
/** Returns an {@link Inspector} for the assumed JSON formatted response, or throws if the status code is non-2XX. */
private static Inspector toInspector(HttpResponse<byte[]> response) {
Inspector rootObject = toSlime(response.body()).get();
@@ -146,6 +217,18 @@ public abstract class ControllerHttpClient {
return toInspector(response).field("message").asString();
}
+ private static DeploymentResult toDeploymentResult(HttpResponse<byte[]> response) {
+ try {
+ Inspector responseObject = toInspector(response);
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ new JsonFormat(false).encode(buffer, responseObject); // Pretty-print until done properly.
+ return new DeploymentResult(buffer.toString(UTF_8));
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
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/Deployment.java b/hosted-api/src/main/java/ai/vespa/hosted/api/Deployment.java
new file mode 100644
index 00000000000..8f981ca5f05
--- /dev/null
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/Deployment.java
@@ -0,0 +1,67 @@
+package ai.vespa.hosted.api;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.OptionalLong;
+
+/**
+ * A deployment intended for hosted Vespa, containing an application package and some meta data.
+ */
+public class Deployment {
+
+ // Deployment options
+ private final Optional<String> version;
+ private final boolean ignoreValidationErrors;
+
+ // Provide an application package ...
+ private final Optional<Path> applicationZip;
+
+ // ... or reference a previously submitted one.
+ private final Optional<String> repository;
+ private final Optional<String> branch;
+ private final Optional<String> commit;
+ private final OptionalLong build;
+
+ private Deployment(Optional<String> version, boolean ignoreValidationErrors, Optional<Path> applicationZip,
+ Optional<String> repository, Optional<String> branch, Optional<String> commit, OptionalLong build) {
+ this.version = version;
+ this.ignoreValidationErrors = ignoreValidationErrors;
+ this.applicationZip = applicationZip;
+ this.repository = repository;
+ this.branch = branch;
+ this.commit = commit;
+ this.build = build;
+ }
+
+
+ /** Returns a deployment which will use the provided application package. */
+ public static Deployment ofPackage(Path applicationZipFile) {
+ return new Deployment(Optional.empty(), false, Optional.of(applicationZipFile),
+ Optional.empty(), Optional.empty(), Optional.empty(), OptionalLong.empty());
+ }
+
+ /** Returns a deployment which will use the previously submitted package with the given reference. */
+ public static Deployment ofReference(String repository, String branch, String commit, long build) {
+ return new Deployment(Optional.empty(), false, Optional.empty(),
+ Optional.of(repository), Optional.of(branch), Optional.of(commit), OptionalLong.of(build));
+ }
+
+ /** Returns a copy of this which will have the specified Vespa version on its nodes. */
+ public Deployment atVersion(String vespaVersion) {
+ return new Deployment(Optional.of(vespaVersion), ignoreValidationErrors, applicationZip, repository, branch, commit, build);
+ }
+
+ /** Returns a copy of this which will additionally ignore validation errors upon deployment. */
+ public Deployment ignoringValidationErrors() {
+ return new Deployment(version, true, applicationZip, repository, branch, commit, build);
+ }
+
+ public Optional<String> version() { return version; }
+ public boolean ignoreValidationErrors() { return ignoreValidationErrors; }
+ public Optional<Path> applicationZip() { return applicationZip; }
+ public Optional<String> repository() { return repository; }
+ public Optional<String> branch() { return branch; }
+ public Optional<String> commit() { return commit; }
+ public OptionalLong build() { return build; }
+
+}
diff --git a/hosted-api/src/main/java/ai/vespa/hosted/api/DeploymentResult.java b/hosted-api/src/main/java/ai/vespa/hosted/api/DeploymentResult.java
new file mode 100644
index 00000000000..63142ffbbf1
--- /dev/null
+++ b/hosted-api/src/main/java/ai/vespa/hosted/api/DeploymentResult.java
@@ -0,0 +1,21 @@
+package ai.vespa.hosted.api;
+
+
+/**
+ * Contains information about the result of a {@link Deployment} against a {@link ControllerHttpClient}.
+ *
+ * @author jonmv
+ */
+public class DeploymentResult {
+
+ private final String json; // TODO probably do this properly.
+
+ public DeploymentResult(String json) {
+ this.json = json;
+ }
+
+ public String json() {
+ return json;
+ }
+
+}