From ea2729bfbc76b84c995ea3c543d534d970261e15 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Sun, 10 Sep 2017 21:41:01 -0700 Subject: [PATCH 1/2] Add create, update and delete protocol buffer methods. --- .../client/examples/ProtoExample.java | 38 ++++- .../io/kubernetes/client/ProtoClient.java | 130 +++++++++++++++--- 2 files changed, 145 insertions(+), 23 deletions(-) diff --git a/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java b/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java index b78dd5d14b..85f8dff85a 100644 --- a/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java +++ b/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java @@ -16,6 +16,11 @@ import io.kubernetes.client.ApiException; import io.kubernetes.client.Configuration; import io.kubernetes.client.ProtoClient; +import io.kubernetes.client.ProtoClient.ObjectOrStatus; +import io.kubernetes.client.proto.Meta.ObjectMeta; +import io.kubernetes.client.proto.Meta.Status; +import io.kubernetes.client.proto.V1.Namespace; +import io.kubernetes.client.proto.V1.NamespaceSpec; import io.kubernetes.client.proto.V1.Pod; import io.kubernetes.client.proto.V1.PodList; import io.kubernetes.client.util.Config; @@ -37,11 +42,36 @@ public static void main(String[] args) throws IOException, ApiException, Interru Configuration.setDefaultApiClient(client); ProtoClient pc = new ProtoClient(client); - PodList list = pc.list(PodList.newBuilder(), "/api/v1/namespaces/default/pods"); + ObjectOrStatus list = pc.list(PodList.newBuilder(), "/api/v1/namespaces/default/pods"); - if (list.getItemsCount() > 0) { - Pod p = list.getItems(0); - System.out.println(p.toString()); + if (list.object.getItemsCount() > 0) { + Pod p = list.object.getItems(0); + System.out.println(p); } + + Namespace namespace = Namespace.newBuilder() + .setMetadata(ObjectMeta.newBuilder() + .setName("test").build()) + .build(); + + ObjectOrStatus ns = pc.create(namespace, "/api/v1/namespaces", "v1", "Namespace"); + System.out.println(ns); + if (ns.object != null) { + namespace = ns.object.toBuilder() + .setSpec( + NamespaceSpec.newBuilder() + .addFinalizers("test") + .build() + ) + .build(); + // This is how you would update an object, but you can't actually + // update namespaces, so this returns a 405 + ns = pc.update(namespace, "/api/v1/namespaces/test", "v1", "Namespace"); + System.out.println(ns.status); + } + + Status stat = pc.delete(Namespace.newBuilder(), "/api/v1/namespaces/test"); + System.out.println(stat); + } } diff --git a/util/src/main/java/io/kubernetes/client/ProtoClient.java b/util/src/main/java/io/kubernetes/client/ProtoClient.java index 5aefc0b17e..270e0322c6 100644 --- a/util/src/main/java/io/kubernetes/client/ProtoClient.java +++ b/util/src/main/java/io/kubernetes/client/ProtoClient.java @@ -2,24 +2,58 @@ import io.kubernetes.client.ApiException; import io.kubernetes.client.Configuration; +import io.kubernetes.client.models.V1ObjectMeta; +import io.kubernetes.client.proto.Meta.Status; +import io.kubernetes.client.proto.Runtime.TypeMeta; import io.kubernetes.client.proto.Runtime.Unknown; import com.google.common.io.ByteStreams; +import com.google.common.primitives.Bytes; +import com.google.protobuf.Descriptors; import com.google.protobuf.Message; +import com.squareup.okhttp.MediaType; import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; +import okio.ByteString; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; public class ProtoClient { + /** + * ObjectOrStatus is an object that is the return from a method call + * it holds either an API Object or an API Status object, but not both. + * Only one field may be non-null at a time. + * + * Oh, how I long for multi-return... + */ + public static class ObjectOrStatus { + public ObjectOrStatus(T obj, Status status) { + this.object = obj; + this.status = status; + } + + public T object; + public Status status; + + public String toString() { + if (object != null) { + return object.toString(); + } + return status.toString(); + } + } + private ApiClient apiClient; // Magic number for the beginning of proto encoded. // https://github.com/kubernetes/apimachinery/blob/master/pkg/runtime/serializer/protobuf/protobuf.go#L42 private static final byte[] MAGIC = new byte[] { 0x6b, 0x38, 0x73, 0x00 }; + private static final String MEDIA_TYPE = "application/vnd.kubernetes.protobuf"; /** * Simple Protocol Budder API client constructor, uses default configuration @@ -58,8 +92,8 @@ public void setApiClient(ApiClient apiClient) { * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name) * @return A Message of type T */ - public T get(T.Builder builder, String path) throws ApiException, IOException { - return (T) request(builder, path, "GET"); + public ObjectOrStatus get(T.Builder builder, String path) throws ApiException, IOException { + return request(builder, path, "GET", null, null, null); } /** @@ -69,43 +103,101 @@ public T get(T.Builder builder, String path) throws ApiExcep * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name) * @return A Message of type T */ - public T list(T.Builder listObj, String path) throws ApiException, IOException { + public ObjectOrStatus list(T.Builder listObj, String path) throws ApiException, IOException { return get(listObj, path); } + /** + * Create a Kubernetes API object using protocol buffer encoding. Performs a POST + * @param obj The object to create + * @param path The URL path to call + * @param apiVersion The api version to use + * @param kind The kind of the object + * @return The response received. + */ + public ObjectOrStatus create(T obj, String path, String apiVersion, String kind) + throws ApiException, IOException { + return request(obj.newBuilderForType(), path, "POST", obj, apiVersion, kind); + } + + /** + * Update a Kubernetes API object using protocol buffer encoding. Performs a PUT + * @param obj The object to create + * @param path The URL path to call + * @param apiVersion The api version to use + * @param kind The kind of the object + * @return The response received. + */ + public ObjectOrStatus update(T obj, String path, String apiVersion, String kind) + throws ApiException, IOException { + return request(obj.newBuilderForType(), path, "PUT", obj, apiVersion, kind); + } + + /** + * Delete a kubernetes API object using protocol buffer encoding. + * @param builder The builder for the response + * @param path The path to call in the API server + * @return The response received + */ + public Status delete(T.Builder builder, String path) throws ApiException, IOException { + return request(builder, path, "DELETE", null, null, null).status; + } + /** * Generic protocol buffer based HTTP request. * Not intended for general consumption, but public for advance use cases. * @param builder The appropriate Builder for the object receveived from the request. * @param method The HTTP method (e.g. GET) for this request. * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name) + * @param body The body to send with the request (optional) + * @param apiVersion The 'apiVersion' to use when encoding, required if body is non-null, ignored otherwise. + * @param kind The 'kind' field to use when encoding, required if body is non-null, ignored otherwise. * @return A Message of type T */ - public T request(T.Builder builder, String path, String method) throws ApiException, IOException { + public ObjectOrStatus request(T.Builder builder, String path, String method, T body, String apiVersion, + String kind) throws ApiException, IOException { HashMap headers = new HashMap(); - headers.put("Content-type", "application/vnd.kubernetes.protobuf"); - headers.put("Accept", "application/vnd.kubernetes.protobuf"); + headers.put("Content-type", MEDIA_TYPE); + headers.put("Accept", MEDIA_TYPE); Request request = apiClient.buildRequest(path, method, new ArrayList(), new ArrayList(), null, headers, new HashMap(), new String[0], null); + if (body != null) { + byte[] bytes = encode(body, apiVersion, kind); + request = request.newBuilder().post(RequestBody.create(MediaType.parse(MEDIA_TYPE), bytes)).build(); + } Response resp = apiClient.getHttpClient().newCall(request).execute(); Unknown u = parse(resp.body().byteStream()); - return (T) builder.mergeFrom(u.getRaw()).build(); + if (u.getTypeMeta().getApiVersion().equals("v1") && + u.getTypeMeta().getKind().equals("Status")) { + Status status = Status.newBuilder().mergeFrom(u.getRaw()).build(); + return new ObjectOrStatus(null, status); + } + + return new ObjectOrStatus((T) builder.mergeFrom(u.getRaw()).build(), null); + } + + // This isn't really documented anywhere except the code, but + // the proto-buf format is: + // * 4 byte magic number + // * Protocol Buffer encoded object of type runtime.Unknown + // * the 'raw' field in that object contains a Protocol Buffer + // encoding of the actual object. + // TODO: Document this somewhere proper. + + private byte[] encode(Message msg, String apiVersion, String kind) { + // It is unfortunate that we have to include apiVersion and kind, + // since we should be able to extract it from the Message, but + // for now at least, those fields are missing from the proto-buffer. + Unknown u = Unknown.newBuilder().setTypeMeta(TypeMeta.newBuilder().setApiVersion(apiVersion).setKind(kind)) + .setRaw(msg.toByteString()).build(); + return Bytes.concat(MAGIC, u.toByteArray()); } private Unknown parse(InputStream stream) throws ApiException, IOException { - // This isn't really documented anywhere except the code, but - // the proto-buf format is: - // * 4 byte magic number - // * Protocol Buffer encoded object of type runtime.Unknown - // * the 'raw' field in that object contains a Protocol Buffer - // encoding of the actual object. - // TODO: Document this somewhere proper. byte[] magic = new byte[4]; - stream.read(magic); - for (int i = 0; i < MAGIC.length; i++) { - if (magic[i] != MAGIC[i]) { - throw new ApiException("Unexpected magic number: " + magic); - } + ByteStreams.readFully(stream, magic); + if (!Arrays.equals(magic, MAGIC)) { + throw new ApiException("Unexpected magic number: " + magic); } return Unknown.parseFrom(stream); } From 3f41d805f71ddd67a7b0fd7ae2b06b1219491314 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Sun, 10 Sep 2017 21:41:01 -0700 Subject: [PATCH 2/2] Add create, update and delete protocol buffer methods. --- .../client/examples/ProtoExample.java | 38 ++++- .../io/kubernetes/client/ProtoClient.java | 136 +++++++++++++++--- 2 files changed, 148 insertions(+), 26 deletions(-) diff --git a/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java b/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java index b78dd5d14b..85f8dff85a 100644 --- a/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java +++ b/examples/src/main/java/io/kubernetes/client/examples/ProtoExample.java @@ -16,6 +16,11 @@ import io.kubernetes.client.ApiException; import io.kubernetes.client.Configuration; import io.kubernetes.client.ProtoClient; +import io.kubernetes.client.ProtoClient.ObjectOrStatus; +import io.kubernetes.client.proto.Meta.ObjectMeta; +import io.kubernetes.client.proto.Meta.Status; +import io.kubernetes.client.proto.V1.Namespace; +import io.kubernetes.client.proto.V1.NamespaceSpec; import io.kubernetes.client.proto.V1.Pod; import io.kubernetes.client.proto.V1.PodList; import io.kubernetes.client.util.Config; @@ -37,11 +42,36 @@ public static void main(String[] args) throws IOException, ApiException, Interru Configuration.setDefaultApiClient(client); ProtoClient pc = new ProtoClient(client); - PodList list = pc.list(PodList.newBuilder(), "/api/v1/namespaces/default/pods"); + ObjectOrStatus list = pc.list(PodList.newBuilder(), "/api/v1/namespaces/default/pods"); - if (list.getItemsCount() > 0) { - Pod p = list.getItems(0); - System.out.println(p.toString()); + if (list.object.getItemsCount() > 0) { + Pod p = list.object.getItems(0); + System.out.println(p); } + + Namespace namespace = Namespace.newBuilder() + .setMetadata(ObjectMeta.newBuilder() + .setName("test").build()) + .build(); + + ObjectOrStatus ns = pc.create(namespace, "/api/v1/namespaces", "v1", "Namespace"); + System.out.println(ns); + if (ns.object != null) { + namespace = ns.object.toBuilder() + .setSpec( + NamespaceSpec.newBuilder() + .addFinalizers("test") + .build() + ) + .build(); + // This is how you would update an object, but you can't actually + // update namespaces, so this returns a 405 + ns = pc.update(namespace, "/api/v1/namespaces/test", "v1", "Namespace"); + System.out.println(ns.status); + } + + Status stat = pc.delete(Namespace.newBuilder(), "/api/v1/namespaces/test"); + System.out.println(stat); + } } diff --git a/util/src/main/java/io/kubernetes/client/ProtoClient.java b/util/src/main/java/io/kubernetes/client/ProtoClient.java index 5aefc0b17e..61d9288217 100644 --- a/util/src/main/java/io/kubernetes/client/ProtoClient.java +++ b/util/src/main/java/io/kubernetes/client/ProtoClient.java @@ -2,24 +2,58 @@ import io.kubernetes.client.ApiException; import io.kubernetes.client.Configuration; +import io.kubernetes.client.models.V1ObjectMeta; +import io.kubernetes.client.proto.Meta.Status; +import io.kubernetes.client.proto.Runtime.TypeMeta; import io.kubernetes.client.proto.Runtime.Unknown; import com.google.common.io.ByteStreams; +import com.google.common.primitives.Bytes; +import com.google.protobuf.Descriptors; import com.google.protobuf.Message; +import com.squareup.okhttp.MediaType; import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; +import okio.ByteString; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; public class ProtoClient { + /** + * ObjectOrStatus is an object that is the return from a method call + * it holds either an API Object or an API Status object, but not both. + * Only one field may be non-null at a time. + * + * Oh, how I long for multi-return... + */ + public static class ObjectOrStatus { + public ObjectOrStatus(T obj, Status status) { + this.object = obj; + this.status = status; + } + + public T object; + public Status status; + + public String toString() { + if (object != null) { + return object.toString(); + } + return status.toString(); + } + } + private ApiClient apiClient; // Magic number for the beginning of proto encoded. // https://github.com/kubernetes/apimachinery/blob/master/pkg/runtime/serializer/protobuf/protobuf.go#L42 private static final byte[] MAGIC = new byte[] { 0x6b, 0x38, 0x73, 0x00 }; + private static final String MEDIA_TYPE = "application/vnd.kubernetes.protobuf"; /** * Simple Protocol Budder API client constructor, uses default configuration @@ -56,10 +90,10 @@ public void setApiClient(ApiClient apiClient) { * Get a Kubernetes API object using protocol buffer encoding. * @param builder The appropriate Builder for the object receveived from the request. * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name) - * @return A Message of type T + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. */ - public T get(T.Builder builder, String path) throws ApiException, IOException { - return (T) request(builder, path, "GET"); + public ObjectOrStatus get(T.Builder builder, String path) throws ApiException, IOException { + return request(builder, path, "GET", null, null, null); } /** @@ -67,45 +101,103 @@ public T get(T.Builder builder, String path) throws ApiExcep * to convey that the object is a List of objects rather than a single object * @param builder The appropriate Builder for the object receveived from the request. * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name) - * @return A Message of type T + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. */ - public T list(T.Builder listObj, String path) throws ApiException, IOException { + public ObjectOrStatus list(T.Builder listObj, String path) throws ApiException, IOException { return get(listObj, path); } + /** + * Create a Kubernetes API object using protocol buffer encoding. Performs a POST + * @param obj The object to create + * @param path The URL path to call + * @param apiVersion The api version to use + * @param kind The kind of the object + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. + */ + public ObjectOrStatus create(T obj, String path, String apiVersion, String kind) + throws ApiException, IOException { + return request(obj.newBuilderForType(), path, "POST", obj, apiVersion, kind); + } + + /** + * Update a Kubernetes API object using protocol buffer encoding. Performs a PUT + * @param obj The object to create + * @param path The URL path to call + * @param apiVersion The api version to use + * @param kind The kind of the object + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. + */ + public ObjectOrStatus update(T obj, String path, String apiVersion, String kind) + throws ApiException, IOException { + return request(obj.newBuilderForType(), path, "PUT", obj, apiVersion, kind); + } + + /** + * Delete a kubernetes API object using protocol buffer encoding. + * @param builder The builder for the response + * @param path The path to call in the API server + * @return The response status + */ + public Status delete(T.Builder builder, String path) throws ApiException, IOException { + return request(builder, path, "DELETE", null, null, null).status; + } + /** * Generic protocol buffer based HTTP request. * Not intended for general consumption, but public for advance use cases. * @param builder The appropriate Builder for the object receveived from the request. * @param method The HTTP method (e.g. GET) for this request. * @param path The URL path to call (e.g. /api/v1/namespaces/default/pods/pod-name) - * @return A Message of type T + * @param body The body to send with the request (optional) + * @param apiVersion The 'apiVersion' to use when encoding, required if body is non-null, ignored otherwise. + * @param kind The 'kind' field to use when encoding, required if body is non-null, ignored otherwise. + * @return An ObjectOrStatus which contains the Object requested, or a Status about the request. */ - public T request(T.Builder builder, String path, String method) throws ApiException, IOException { + public ObjectOrStatus request(T.Builder builder, String path, String method, T body, String apiVersion, + String kind) throws ApiException, IOException { HashMap headers = new HashMap(); - headers.put("Content-type", "application/vnd.kubernetes.protobuf"); - headers.put("Accept", "application/vnd.kubernetes.protobuf"); + headers.put("Content-type", MEDIA_TYPE); + headers.put("Accept", MEDIA_TYPE); Request request = apiClient.buildRequest(path, method, new ArrayList(), new ArrayList(), null, headers, new HashMap(), new String[0], null); + if (body != null) { + byte[] bytes = encode(body, apiVersion, kind); + request = request.newBuilder().post(RequestBody.create(MediaType.parse(MEDIA_TYPE), bytes)).build(); + } Response resp = apiClient.getHttpClient().newCall(request).execute(); Unknown u = parse(resp.body().byteStream()); - return (T) builder.mergeFrom(u.getRaw()).build(); + if (u.getTypeMeta().getApiVersion().equals("v1") && + u.getTypeMeta().getKind().equals("Status")) { + Status status = Status.newBuilder().mergeFrom(u.getRaw()).build(); + return new ObjectOrStatus(null, status); + } + + return new ObjectOrStatus((T) builder.mergeFrom(u.getRaw()).build(), null); + } + + // This isn't really documented anywhere except the code, but + // the proto-buf format is: + // * 4 byte magic number + // * Protocol Buffer encoded object of type runtime.Unknown + // * the 'raw' field in that object contains a Protocol Buffer + // encoding of the actual object. + // TODO: Document this somewhere proper. + + private byte[] encode(Message msg, String apiVersion, String kind) { + // It is unfortunate that we have to include apiVersion and kind, + // since we should be able to extract it from the Message, but + // for now at least, those fields are missing from the proto-buffer. + Unknown u = Unknown.newBuilder().setTypeMeta(TypeMeta.newBuilder().setApiVersion(apiVersion).setKind(kind)) + .setRaw(msg.toByteString()).build(); + return Bytes.concat(MAGIC, u.toByteArray()); } private Unknown parse(InputStream stream) throws ApiException, IOException { - // This isn't really documented anywhere except the code, but - // the proto-buf format is: - // * 4 byte magic number - // * Protocol Buffer encoded object of type runtime.Unknown - // * the 'raw' field in that object contains a Protocol Buffer - // encoding of the actual object. - // TODO: Document this somewhere proper. byte[] magic = new byte[4]; - stream.read(magic); - for (int i = 0; i < MAGIC.length; i++) { - if (magic[i] != MAGIC[i]) { - throw new ApiException("Unexpected magic number: " + magic); - } + ByteStreams.readFully(stream, magic); + if (!Arrays.equals(magic, MAGIC)) { + throw new ApiException("Unexpected magic number: " + magic); } return Unknown.parseFrom(stream); }