Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add create, update and delete protocol buffer methods. #71

Merged
merged 3 commits into from
Sep 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<PodList> 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<Namespace> 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);

}
}
136 changes: 114 additions & 22 deletions util/src/main/java/io/kubernetes/client/ProtoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Message> {
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
Expand Down Expand Up @@ -56,56 +90,114 @@ 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 extends Message> T get(T.Builder builder, String path) throws ApiException, IOException {
return (T) request(builder, path, "GET");
public <T extends Message> ObjectOrStatus<T> get(T.Builder builder, String path) throws ApiException, IOException {
return request(builder, path, "GET", null, null, null);
}

/**
* List is fluent, semantic sugar method on top of get, which is intended
* 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 extends Message> T list(T.Builder listObj, String path) throws ApiException, IOException {
public <T extends Message> ObjectOrStatus<T> 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 <T extends Message> ObjectOrStatus<T> 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 <T extends Message> ObjectOrStatus<T> 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 <T extends Message> 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 extends Message> T request(T.Builder builder, String path, String method) throws ApiException, IOException {
public <T extends Message> ObjectOrStatus<T> request(T.Builder builder, String path, String method, T body, String apiVersion,
String kind) throws ApiException, IOException {
HashMap<String, String> headers = new HashMap<String, String>();
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<Pair>(), new ArrayList<Pair>(), null,
headers, new HashMap<String, Object>(), 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);
}
Expand Down