|
2 | 2 |
|
3 | 3 | import io.kubernetes.client.ApiException;
|
4 | 4 | import io.kubernetes.client.Configuration;
|
| 5 | +import io.kubernetes.client.models.V1ObjectMeta; |
| 6 | +import io.kubernetes.client.proto.Meta.Status; |
| 7 | +import io.kubernetes.client.proto.Runtime.TypeMeta; |
5 | 8 | import io.kubernetes.client.proto.Runtime.Unknown;
|
6 | 9 |
|
7 | 10 | import com.google.common.io.ByteStreams;
|
| 11 | +import com.google.common.primitives.Bytes; |
| 12 | +import com.google.protobuf.Descriptors; |
8 | 13 | import com.google.protobuf.Message;
|
| 14 | +import com.squareup.okhttp.MediaType; |
9 | 15 | import com.squareup.okhttp.Request;
|
| 16 | +import com.squareup.okhttp.RequestBody; |
10 | 17 | import com.squareup.okhttp.Response;
|
11 | 18 | import com.squareup.okhttp.ResponseBody;
|
| 19 | +import okio.ByteString; |
12 | 20 |
|
13 | 21 | import java.io.IOException;
|
14 | 22 | import java.io.InputStream;
|
| 23 | +import java.util.Arrays; |
15 | 24 | import java.util.ArrayList;
|
16 | 25 | import java.util.HashMap;
|
17 | 26 |
|
18 | 27 | public class ProtoClient {
|
| 28 | + /** |
| 29 | + * ObjectOrStatus is an object that is the return from a method call |
| 30 | + * it holds either an API Object or an API Status object, but not both. |
| 31 | + * Only one field may be non-null at a time. |
| 32 | + * |
| 33 | + * Oh, how I long for multi-return... |
| 34 | + */ |
| 35 | + public static class ObjectOrStatus<T extends Message> { |
| 36 | + public ObjectOrStatus(T obj, Status status) { |
| 37 | + this.object = obj; |
| 38 | + this.status = status; |
| 39 | + } |
| 40 | + |
| 41 | + public T object; |
| 42 | + public Status status; |
| 43 | + |
| 44 | + public String toString() { |
| 45 | + if (object != null) { |
| 46 | + return object.toString(); |
| 47 | + } |
| 48 | + return status.toString(); |
| 49 | + } |
| 50 | + } |
| 51 | + |
19 | 52 | private ApiClient apiClient;
|
20 | 53 | // Magic number for the beginning of proto encoded.
|
21 | 54 | // https://github.com/kubernetes/apimachinery/blob/master/pkg/runtime/serializer/protobuf/protobuf.go#L42
|
22 | 55 | private static final byte[] MAGIC = new byte[] { 0x6b, 0x38, 0x73, 0x00 };
|
| 56 | + private static final String MEDIA_TYPE = "application/vnd.kubernetes.protobuf"; |
23 | 57 |
|
24 | 58 | /**
|
25 | 59 | * Simple Protocol Budder API client constructor, uses default configuration
|
@@ -56,56 +90,114 @@ public void setApiClient(ApiClient apiClient) {
|
56 | 90 | * Get a Kubernetes API object using protocol buffer encoding.
|
57 | 91 | * @param builder The appropriate Builder for the object receveived from the request.
|
58 | 92 | * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name)
|
59 |
| - * @return A Message of type T |
| 93 | + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. |
60 | 94 | */
|
61 |
| - public <T extends Message> T get(T.Builder builder, String path) throws ApiException, IOException { |
62 |
| - return (T) request(builder, path, "GET"); |
| 95 | + public <T extends Message> ObjectOrStatus<T> get(T.Builder builder, String path) throws ApiException, IOException { |
| 96 | + return request(builder, path, "GET", null, null, null); |
63 | 97 | }
|
64 | 98 |
|
65 | 99 | /**
|
66 | 100 | * List is fluent, semantic sugar method on top of get, which is intended
|
67 | 101 | * to convey that the object is a List of objects rather than a single object
|
68 | 102 | * @param builder The appropriate Builder for the object receveived from the request.
|
69 | 103 | * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name)
|
70 |
| - * @return A Message of type T |
| 104 | + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. |
71 | 105 | */
|
72 |
| - public <T extends Message> T list(T.Builder listObj, String path) throws ApiException, IOException { |
| 106 | + public <T extends Message> ObjectOrStatus<T> list(T.Builder listObj, String path) throws ApiException, IOException { |
73 | 107 | return get(listObj, path);
|
74 | 108 | }
|
75 | 109 |
|
| 110 | + /** |
| 111 | + * Create a Kubernetes API object using protocol buffer encoding. Performs a POST |
| 112 | + * @param obj The object to create |
| 113 | + * @param path The URL path to call |
| 114 | + * @param apiVersion The api version to use |
| 115 | + * @param kind The kind of the object |
| 116 | + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. |
| 117 | + */ |
| 118 | + public <T extends Message> ObjectOrStatus<T> create(T obj, String path, String apiVersion, String kind) |
| 119 | + throws ApiException, IOException { |
| 120 | + return request(obj.newBuilderForType(), path, "POST", obj, apiVersion, kind); |
| 121 | + } |
| 122 | + |
| 123 | + /** |
| 124 | + * Update a Kubernetes API object using protocol buffer encoding. Performs a PUT |
| 125 | + * @param obj The object to create |
| 126 | + * @param path The URL path to call |
| 127 | + * @param apiVersion The api version to use |
| 128 | + * @param kind The kind of the object |
| 129 | + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. |
| 130 | + */ |
| 131 | + public <T extends Message> ObjectOrStatus<T> update(T obj, String path, String apiVersion, String kind) |
| 132 | + throws ApiException, IOException { |
| 133 | + return request(obj.newBuilderForType(), path, "PUT", obj, apiVersion, kind); |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * Delete a kubernetes API object using protocol buffer encoding. |
| 138 | + * @param builder The builder for the response |
| 139 | + * @param path The path to call in the API server |
| 140 | + * @return The response status |
| 141 | + */ |
| 142 | + public <T extends Message> Status delete(T.Builder builder, String path) throws ApiException, IOException { |
| 143 | + return request(builder, path, "DELETE", null, null, null).status; |
| 144 | + } |
| 145 | + |
76 | 146 | /**
|
77 | 147 | * Generic protocol buffer based HTTP request.
|
78 | 148 | * Not intended for general consumption, but public for advance use cases.
|
79 | 149 | * @param builder The appropriate Builder for the object receveived from the request.
|
80 | 150 | * @param method The HTTP method (e.g. GET) for this request.
|
81 | 151 | * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name)
|
82 |
| - * @return A Message of type T |
| 152 | + * @param body The body to send with the request (optional) |
| 153 | + * @param apiVersion The 'apiVersion' to use when encoding, required if body is non-null, ignored otherwise. |
| 154 | + * @param kind The 'kind' field to use when encoding, required if body is non-null, ignored otherwise. |
| 155 | + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. |
83 | 156 | */
|
84 |
| - public <T extends Message> T request(T.Builder builder, String path, String method) throws ApiException, IOException { |
| 157 | + public <T extends Message> ObjectOrStatus<T> request(T.Builder builder, String path, String method, T body, String apiVersion, |
| 158 | + String kind) throws ApiException, IOException { |
85 | 159 | HashMap<String, String> headers = new HashMap<String, String>();
|
86 |
| - headers.put("Content-type", "application/vnd.kubernetes.protobuf"); |
87 |
| - headers.put("Accept", "application/vnd.kubernetes.protobuf"); |
| 160 | + headers.put("Content-type", MEDIA_TYPE); |
| 161 | + headers.put("Accept", MEDIA_TYPE); |
88 | 162 | Request request = apiClient.buildRequest(path, method, new ArrayList<Pair>(), new ArrayList<Pair>(), null,
|
89 | 163 | headers, new HashMap<String, Object>(), new String[0], null);
|
| 164 | + if (body != null) { |
| 165 | + byte[] bytes = encode(body, apiVersion, kind); |
| 166 | + request = request.newBuilder().post(RequestBody.create(MediaType.parse(MEDIA_TYPE), bytes)).build(); |
| 167 | + } |
90 | 168 | Response resp = apiClient.getHttpClient().newCall(request).execute();
|
91 | 169 | Unknown u = parse(resp.body().byteStream());
|
92 |
| - return (T) builder.mergeFrom(u.getRaw()).build(); |
| 170 | + if (u.getTypeMeta().getApiVersion().equals("v1") && |
| 171 | + u.getTypeMeta().getKind().equals("Status")) { |
| 172 | + Status status = Status.newBuilder().mergeFrom(u.getRaw()).build(); |
| 173 | + return new ObjectOrStatus(null, status); |
| 174 | + } |
| 175 | + |
| 176 | + return new ObjectOrStatus((T) builder.mergeFrom(u.getRaw()).build(), null); |
| 177 | + } |
| 178 | + |
| 179 | + // This isn't really documented anywhere except the code, but |
| 180 | + // the proto-buf format is: |
| 181 | + // * 4 byte magic number |
| 182 | + // * Protocol Buffer encoded object of type runtime.Unknown |
| 183 | + // * the 'raw' field in that object contains a Protocol Buffer |
| 184 | + // encoding of the actual object. |
| 185 | + // TODO: Document this somewhere proper. |
| 186 | + |
| 187 | + private byte[] encode(Message msg, String apiVersion, String kind) { |
| 188 | + // It is unfortunate that we have to include apiVersion and kind, |
| 189 | + // since we should be able to extract it from the Message, but |
| 190 | + // for now at least, those fields are missing from the proto-buffer. |
| 191 | + Unknown u = Unknown.newBuilder().setTypeMeta(TypeMeta.newBuilder().setApiVersion(apiVersion).setKind(kind)) |
| 192 | + .setRaw(msg.toByteString()).build(); |
| 193 | + return Bytes.concat(MAGIC, u.toByteArray()); |
93 | 194 | }
|
94 | 195 |
|
95 | 196 | private Unknown parse(InputStream stream) throws ApiException, IOException {
|
96 |
| - // This isn't really documented anywhere except the code, but |
97 |
| - // the proto-buf format is: |
98 |
| - // * 4 byte magic number |
99 |
| - // * Protocol Buffer encoded object of type runtime.Unknown |
100 |
| - // * the 'raw' field in that object contains a Protocol Buffer |
101 |
| - // encoding of the actual object. |
102 |
| - // TODO: Document this somewhere proper. |
103 | 197 | byte[] magic = new byte[4];
|
104 |
| - stream.read(magic); |
105 |
| - for (int i = 0; i < MAGIC.length; i++) { |
106 |
| - if (magic[i] != MAGIC[i]) { |
107 |
| - throw new ApiException("Unexpected magic number: " + magic); |
108 |
| - } |
| 198 | + ByteStreams.readFully(stream, magic); |
| 199 | + if (!Arrays.equals(magic, MAGIC)) { |
| 200 | + throw new ApiException("Unexpected magic number: " + magic); |
109 | 201 | }
|
110 | 202 | return Unknown.parseFrom(stream);
|
111 | 203 | }
|
|
0 commit comments