de1ux

gRPC with Typescript and Go (part 2)

12/22/2018

In part 1 we:

Getting logs from Kubernetes

Right now, our GetLogs method is not very interesting.

func (service) GetLogs(_ context.Context, request *api.GetLogsRequest) (*api.GetLogsResponse, error) {
    panic("implement me")
}

Let’s import the lovely Kubernetes client-go and get to work returning real logs.

Get a client to communicate with the Kubernetes cluster

func getClientSet() (*kubernetes.Clientset, error) {
    config, err := clientcmd.BuildConfigFromFlags("", filepath.Join(homedir.HomeDir(), ".kube", "config"))
    if err != nil {
        return nil, err
    }

    return kubernetes.NewForConfig(config)
}

And return some logs given a podName.

func getJobPodLogStream(client *kubernetes.Clientset, podName string) rest.Result {
    return client.
        RESTClient().
        Get().
        Prefix("api/v1").
        Namespace("default").
        Name(podName).
        Resource("pods").
        SubResource("log").
        Param("timestamps", "true").
        Do()
}

If you prefer, client-go also exposes a GetLogs method that abstracts calling the RESTClient directly.

Now consume our new functions in GetLogs

func (service) GetLogs(_ context.Context, request *api.GetLogsRequest) (*api.GetLogsResponse, error) {
    client, err := getClientSet()
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Failed to create clientset: %s", err)
    }

    result := getJobPodLogStream(client, request.PodName)
    if result.Error() != nil {
        return nil, status.Errorf(codes.Internal, "Failed to get logs: %s", result.Error())
    }

    b, err := result.Raw()
    if err != nil {
        return nil, status.Errorf(codes.Internal, "Failed to coerce logs to bytes: %s", err)
    }

    lines := strings.Split(string(b), "\n")
    return &api.GetLogsResponse{Data: lines}, nil
}

Each RPC to GetLogs will

Testing

You’ll (obviously) need a pod to attempt to fetch logs from - here’s the one I used

apiVersion: apps/v1
kind: Deployment
metadata:
  name: random-logger
  labels:
    app: random-logger
spec:
  replicas: 1
  selector:
    matchLabels:
      app: random-logger
  template:
    metadata:
      labels:
        app: random-logger
    spec:
      containers:
      - name: random-logger
        image: chentex/random-logger

We can temporarily invoke the GetLogs method manually by modifying main.go. It’s crude, but sanity checks our Kubernetes logic without needing a client to call GetLogs.

func main() {
    grpcServer := grpc.NewServer()

    s := &service{}
    logs, err := s.GetLogs(context.Background(), &api.GetLogsRequest{PodName: "random-logger-7f68f7949-88zrw"})
    if err != nil {
        panic(err)
    }
    println(strings.Join(logs.Data, "\n"))
}

Which outputs

2018-12-21T02:17:49.719640287Z 2018-12-21T02:17:49+0000 WARN variable not in use.
2018-12-21T02:17:59.725955725Z 2018-12-21T02:17:59+0000 WARN variable not in use.
2018-12-21T02:18:04.729879584Z 2018-12-21T02:18:04+0000 ERROR something happened in this execution.
2018-12-21T02:18:09.733047306Z 2018-12-21T02:18:09+0000 INFO takes the value and converts it to string.
2018-12-21T02:18:13.737322556Z 2018-12-21T02:18:13+0000 INFO takes the value and converts it to string.
2018-12-21T02:18:23.746870251Z 2018-12-21T02:18:23+0000 WARN variable not in use.
...

Next we’ll build a Typescript client to tie all the pieces together and produce a working app