Updating Kubernetes Resources With Visitors

Quick post to share my learnings about using Visitor and TypedVisitor to update resources in the fabric8 Java Kubernetes client.

Atomic Updates with Versions

Out of the gate you want to write code so that your update calls won’t clobber any concurrent updates (and vice versa). Kubernetes resources have version ids so the pattern to update them atomically, meaning them is pretty simple.

  1. Fetch current version of resource
  2. Apply Updates to in memory model
  3. Persist model to kubernetes, it will see if the resource version id you’re updating is current or not
  4. check results of call, go to 1 if there was a 409 conflict

There’s a lot of boilerplate to do that properly, fortunately there’s a Visitor pattern in the fabric8 client that handles everything except step two for you. I couldn’t find much in the way of documentation, but there are a couple of quirks that took me some time stepping through code to figure out.

TL;DR

You want eg TypedVisitor<DoneableDeployment> over the more obvious to me Visitor<Deployment> The client needs some helper code in TypedVisitor to figure out what objects to visit; DoneableDeployment has edit-ability that a Deployment object lacks. Note though, you do not want to call the done method inside your visitor code – that will attempt to save the object from inside your visitor, then it will be saved again after the visitor call.

Example Code

Here’s an example of updating an annotation on a Deployment. I separated out the boilerplate call identify the deployment to which the visitor will be applied in visitDeployment.

public class DeploymentExample {
  public Deployment visitDeployment(String deploymentName, String namespace, TypedVisitor<DoneableDeployment> visitor) {
    return kubernetesClient.apps()
        .deployments()
        .inNamespace(namespace)
        .withName(deploymentName)
        .edit()
        .accept(visitor)
        .done();
  }

  public void updateFoobarAnnotation() {
    TypedVisitor<DoneableDeployment> myVisitor = new TypedVisitor<DoneableDeployment>() {
      @Override
      public void visit(DoneableDeployment element) {
        element.editMetadata()
            .addToAnnotations("foobar","washere")
            .endMetadata();
      }
    };
   visitDeployment("name","namespace",myVisitor);
}


Wait, Can I get Less Boilerplate?

When I first saw the visitor bits of the Fabric8 client, I thought I could just go ahead and use lambdas instead of subclassing TypedVisitor, but the type safety checks in the client defeated that. We can work with that though and create a simple adaptor so that we only need implement a Consumer<DoneableDeployment> which will get us back to lambda land. Check it out:

public class TypedVisitorHelper {

  public static <T> TypedVisitor<T> make(Class<T> type, Consumer<T> consumer) {
    return new TypedVisitor<T>() {
      @Override
      public void visit(T element) {
        consumer.accept(element);
      }

      @Override
      public Class<T> getType() {
        return type;
      }
    };
  }
}

That’ll nicely let us express the example above as follows:

public class DeploymentExample {
  public Deployment visitDeployment(String deploymentName, String namespace, Consumer<DoneableDeployment> visitor) {
    return kubernetesClient.apps()
        .deployments()
        .inNamespace(namespace)
        .withName(deploymentName)
        .edit()
        .accept(TypedVisitorHelper.make(DoneableDeployment.class, visitor))
        .done();
  }

  public void updateFoobarAnnotation() {
   visitDeployment("name","namespace",(element) -> element.editMetadata()
        .addToAnnotations("foobar", "washere")
        .endMetadata());
}

Of course the boilerplate visitDeployment will be amortized over the various ways you’ll manipulate your resources.