1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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);
}
}
|