dajudge / controllers-101

Hi, Spring fans! In this installment we look at getting started writing controllers

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

You're Going to Get Through This

Generate a new spring native project from start.spring.io

We'll need a new project from the Spring Initializr. Make sure to select Spring Native and Reactive Web, and Lombok.

Add to pom.xml

The Spring Initializr will get us most of the way (it does _a lot _ of code generation) but we need to add some extra dependencies. The first dependency is the official Java client for Kubernetes. The second dependency is a set of custom Spring Native hints that Josh wrote for a bunch of different projects. In an ideal world, this won't be required forever. When Spring Framework 6 drops, this kind of stuff might be better placed in the official Java client itself. If you want to use the RedHat Fabric8 Java client, well there are hints for that, too! And conceptually, a lot of what we're going to look at is the same in both Fabric 8 and the official Java client, since they're both code-generated from the same OpenAPI definitions. The packages and particulars may change, of course.

Anyway, add:

        <dependency>
            <groupId>io.kubernetes</groupId>
            <artifactId>client-java-spring-integration</artifactId>
            <version>15.0.0</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.joshlong</groupId>
            <artifactId>hints</artifactId>
            <version>0.0.1</version>
        </dependency>

Stage

Copy bin and k8s from the source code to the new project generated from the Spring Initializr

Authenticate with GitHub Because Reasons

You'll need a GitHub Personal Access Token (PAT). You can get the PAT from GitHub Settings

cat ~/TOKEN.txt | docker login https://docker.pkg.github.com -u USERNAME --password-stdin 

Deploy the CRD to Kubernetes

k apply -f k8s/crds/foo.yaml

We should be able to do

k get crds

And see the newly minted CRD. Now would also be an apropos time to show the audience the soul-annihilatingly tedious definition of the CRD itself (foo.yaml). This CRD is why the K8s community can't have nice things.

We should also show the test.yaml, but don't apply it yet. This way people get the distinction between the archetypal definition of the CRD and an instance of the CRD.

Run the Code Generator for the Image

We'll need a little script to help us code-generate the Java code for our CRDs. Copy the regen_crd_java.sh script from our backup directory to the new project, in a directory called bin:

./bin/regen_crd_java.sh

You should have some source code dumped in a directory specified in gen to be moved to the src/main/java/io/spring/models folder. Introduce the code generated Java classes.

And then a Miracle Happens

There are several main concepts we need to understand before the code we're about to write makes sense.

Controller

Kubernetes is an edge-leveled reconciling controller. Basically, it will spin up a loop that evaluates some system state and if that system state should ever drift from the operator's desired state, the controller's job is to make that state so. So, llogically, a K8s CRD is two things: a CRD definition and a controller that reacts to the lifecycle of new instances of that CRD. We've already defined the CRD itself and looked at the generated code for the CRD instance itself. We're halfway there! We just need the controller itself. That'll be our first Spring @Bean.

    @Bean
    Controller controller(SharedInformerFactory sharedInformerFactory,
                          SharedIndexInformer<V1Foo> fooNodeInformer,
                          Reconciler reconciler) {
        var builder = ControllerBuilder //
                .defaultBuilder(sharedInformerFactory)//
                .watch((q) -> ControllerBuilder //
                        .controllerWatchBuilder(V1Foo.class, q)
                        .withResyncPeriod(Duration.ofHours(1)).build() //
                ) //
                .withWorkerCount(2);
        return builder
                .withReconciler(reconciler) //
                .withReadyFunc(fooNodeInformer::hasSynced) // optional: only start once the index is synced
                .withName("fooController") ///
                .build();

    }

Things are broken! We don't have any of the three dependencies expressed here: SharedInformerFactory, Reconciler, and SharedIndexInformer<V1Foo>.

    @Bean
    SharedIndexInformer<V1Foo> fooNodeInformer(
            SharedInformerFactory sharedInformerFactory,
            GenericKubernetesApi<V1Foo, V1FooList> api) {
        return sharedInformerFactory.sharedIndexInformerFor(api, V1Foo.class, 0);
    }

This in turn implies a dependency on GenericKubernetesApi<V1Foo,V1FooList>.

 @Bean
    GenericKubernetesApi<V1Foo, V1FooList> foosApi(ApiClient apiClient) {
        return new GenericKubernetesApi<>(V1Foo.class, V1FooList.class, "spring.io", "v1",
                "foos", apiClient);
    }

We'll also need a GenericKubernetesApi for Deployments, too, so let's get that out of the way now:

   @Bean
    GenericKubernetesApi<V1Deployment, V1DeploymentList> deploymentsApi(ApiClient apiClient) {
        return new GenericKubernetesApi<>(V1Deployment.class, V1DeploymentList.class,
                "", "v1", "deployments",
                apiClient);
    }

They're identical, except for their generic parameters and the string definitions of the group, and pluralized form of their nouns. These API clients let us talk to the API server about a given type of CRD, in this case Foo and Deployment, respectively.

We need the SharedIndexInformer<V1Foo>, too.

What's an Informer, you ask? An informer "is a" - and we're not making this iup - controller together with the ability to distribute its Queue-related operations to an appropriate event handler. There are SharedInformers that share data across multiple instances of the Informer so that they're not duplicated. A SharedInformer has a shared data cache and is capable of distributing notifications for changes to the cache to multiple listeners who registered with it. There is one behavior change compared to a standard Informer: when you receive a notification, the cache will be at least as fresh as the notification, but it may be more fresh. You should not depend on the contents of the cache exactly matching the state implied by the notification. The notification is binding. SharedIndexInformer only adds one more thing to the picture: the ability to lookup items by various keys. So, a controller sometimes needs a conceptually-a-controller to be a controller. Got it? Got it.

Next, we'll need to define the Reconciler itself:

   @Bean
    Reconciler reconciler(
            @Value("classpath:/deployment.yaml") Resource resourceForDeploymentYaml,
            AppsV1Api coreV1Api,
            SharedIndexInformer<V1Foo> fooNodeInformer,
        GenericKubernetesApi<V1Deployment, V1DeploymentList> deploymentApi) {
        return new FooReconciler(coreV1Api, resourceForDeploymentYaml, fooNodeInformer, deploymentApi);
    }

Here's where the rubber meets the road: our reconciler will create a new Deployment every time a new Foo is created. We like you too much to programmatically build up the Deployment from scratch in Java, so we'll just reuse a pre-written YAML definition (/deployment.yaml) of a Deployment and then reify it, changing some of its parameters, and submit that.

We'll also need references fo the GenericKubernetesAPi for Deployments and a new thing, called the AppsV1Api. This API sidesteps all the caching and indexing and allows us to talk directly to the API server. You could achieve this without using the API, but it simplifies things sometimes and it's instructional to see it in action, so:

  @Bean
    AppsV1Api appsV1Api(ApiClient apiClient) {
        return new AppsV1Api(apiClient);
    }

Deploy an Instance of the foo Object

k apply -f k8s/crds/test.yml

Resources

About

Hi, Spring fans! In this installment we look at getting started writing controllers


Languages

Language:Java 97.9%Language:Shell 2.1%