diff --git a/README.md b/README.md index b167402452..dca7e33794 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ We prepared a few examples for common use-cases which are shown below: Build a controller reconciling the state of world by list-watching one or multiple resources. - ([6.0.0+](https://github.com/kubernetes-client/java/tree/client-java-parent-6.0.0)) [LeaderElectionExample](https://github.com/kubernetes-client/java/blob/master/examples/src/main/java/io/kubernetes/client/examples/LeaderElectionExample.java): Leader election utilities to help implement HA controllers. + - ([9.0.0+](https://github.com/kubernetes-client/java/tree/client-java-parent-9.0.0)) [SpringIntegrationControllerExample](https://github.com/kubernetes-client/java/blob/master/examples/src/main/java/io/kubernetes/client/examples/SpringControllerExample.java): + Building a kubernetes controller based on spring framework's bean injection. __list all pods__: diff --git a/examples/pom.xml b/examples/pom.xml index e1b1245d4c..d80a160310 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -26,6 +26,11 @@ client-java-extended ${project.version} + + io.kubernetes + client-java-spring-integration + ${project.version} + io.kubernetes client-java-proto diff --git a/examples/src/main/java/io/kubernetes/client/examples/SpringControllerExample.java b/examples/src/main/java/io/kubernetes/client/examples/SpringControllerExample.java new file mode 100644 index 0000000000..b7bd6ce500 --- /dev/null +++ b/examples/src/main/java/io/kubernetes/client/examples/SpringControllerExample.java @@ -0,0 +1,137 @@ +package io.kubernetes.client.examples; + +import io.kubernetes.client.extended.controller.Controller; +import io.kubernetes.client.extended.controller.reconciler.Reconciler; +import io.kubernetes.client.extended.controller.reconciler.Request; +import io.kubernetes.client.extended.controller.reconciler.Result; +import io.kubernetes.client.informer.SharedInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Node; +import io.kubernetes.client.openapi.models.V1NodeList; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; +import io.kubernetes.client.spring.extended.controller.annotation.*; +import io.kubernetes.client.util.ClientBuilder; +import java.io.IOException; +import java.time.Duration; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@SpringBootApplication +public class SpringControllerExample { + + public static void main(String[] args) { + SpringApplication.run(SpringControllerExample.class, args); + } + + @Configuration + @ComponentScan( + basePackages = "io.kubernetes.client.spring.extended.controller" + ) // Scanning beans under this package is *REQUIRED* for informers/reconciler injections. + public static class AppConfig { + + @Bean + public CommandLineRunner commandLineRunner( + SharedInformerFactory sharedInformerFactory, + @Qualifier("node-printing-controller") Controller nodePrintingController) { + return args -> { + System.out.println("starting informers.."); + sharedInformerFactory.startAllRegisteredInformers(); + + System.out.println("running controller.."); + nodePrintingController.run(); + }; + } + + // *OPTIONAL* + // Injecting and customize your ApiClient, if not specified, fallbacks to {@link + // io.kubernetes.client.util.ClientBuilder#standard} + @Bean + public ApiClient myApiClient() throws IOException { + ApiClient apiClient = ClientBuilder.standard().build(); + return apiClient.setHttpClient( + apiClient.getHttpClient().newBuilder().readTimeout(Duration.ZERO).build()); + } + + // *REQUIRED* + // Injecting your SharedInformerFactory class annotated `@KubernetesInformers` + @Bean("sharedInformerFactory") + public SharedInformerFactory sharedInformerFactory() { + return new MySharedInformerFactory(); + } + + @Bean + public NodePrintingReconciler nodePrintingReconciler( + Lister podLister, Lister nodeLister, SharedInformer nodeInformer) { + return new NodePrintingReconciler(podLister, nodeLister, nodeInformer); + } + } + + @KubernetesInformers({ // Defining what resources is the informer-factory actually watching. + @KubernetesInformer( + apiTypeClass = V1Node.class, + apiListTypeClass = V1NodeList.class, + groupVersionResource = + @GroupVersionResource(apiGroup = "", apiVersion = "v1", resourcePlural = "nodes"), + resyncPeriodMillis = 60 * 1000L + ), + @KubernetesInformer( + apiTypeClass = V1Pod.class, + apiListTypeClass = V1PodList.class, + groupVersionResource = + @GroupVersionResource(apiGroup = "", apiVersion = "v1", resourcePlural = "pods") + ), + }) + public static class MySharedInformerFactory extends SharedInformerFactory {} + + // As long as a reconciler bean attached `@KubernetesReconciler` detected in the context, we will + // be automatically creating a conresponding controller bean implementing {@link + // io.kubernetes.client.extended.controller.Controller} + // with the name specified and registering it to the spring bean-factory. + @KubernetesReconciler( + value = "node-printing-controller", + watches = + @KubernetesReconcilerWatches({ + @KubernetesReconcilerWatch( + apiTypeClass = V1Node.class, + resyncPeriodMillis = 60 * 1000L // fully resync every 1 minute + ), + }) + ) + public static class NodePrintingReconciler implements Reconciler { + + public NodePrintingReconciler( + Lister podLister, Lister nodeLister, SharedInformer nodeInformer) { + this.nodeLister = nodeLister; + this.podLister = podLister; + this.nodeInformer = nodeInformer; + } + + private SharedInformer nodeInformer; + + private Lister nodeLister; + + private Lister podLister; + + // *OPTIONAL* + // If you feed like hold the controller from running util some condition.. + @KubernetesReconcilerReadyFunc + boolean informerReady() { + return nodeInformer.hasSynced(); + } + + @Override + public Result reconcile(Request request) { + V1Node node = nodeLister.get(request.getName()); + System.out.println("triggered reconciling " + node.getMetadata().getName()); + return new Result(false); + } + } +} diff --git a/spring/pom.xml b/spring/pom.xml index a0bb2e4043..37443ad3f7 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -29,6 +29,11 @@ spring-boot ${spring.boot.version} + + org.springframework.boot + spring-boot-autoconfigure + ${spring.boot.version} + junit diff --git a/spring/src/main/java/io/kubernetes/client/spring/extended/controller/KubernetesInformerFactoryProcessor.java b/spring/src/main/java/io/kubernetes/client/spring/extended/controller/KubernetesInformerFactoryProcessor.java index 59853bacde..368b769d3a 100644 --- a/spring/src/main/java/io/kubernetes/client/spring/extended/controller/KubernetesInformerFactoryProcessor.java +++ b/spring/src/main/java/io/kubernetes/client/spring/extended/controller/KubernetesInformerFactoryProcessor.java @@ -16,14 +16,18 @@ import io.kubernetes.client.util.ClientBuilder; import io.kubernetes.client.util.Watchable; import java.io.IOException; +import java.time.Duration; import java.util.Map; import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.stereotype.Component; @@ -37,13 +41,16 @@ * injects informers to spring context with the underlying constructing process hidden from users. */ @Component -public class KubernetesInformerFactoryProcessor implements BeanFactoryPostProcessor, Ordered { +public class KubernetesInformerFactoryProcessor + implements BeanDefinitionRegistryPostProcessor, Ordered { private static final Logger log = LoggerFactory.getLogger(KubernetesInformerFactoryProcessor.class); public static final int ORDER = 0; + private BeanDefinitionRegistry beanDefinitionRegistry; + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { @@ -79,6 +86,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) return; } } + apiClient.setHttpClient( + apiClient.getHttpClient().newBuilder().readTimeout(Duration.ZERO).build()); SharedInformerFactory sharedInformerFactory = beanFactory.getBean(SharedInformerFactory.class); KubernetesInformers kubernetesInformers = @@ -124,12 +133,24 @@ public Watchable watch(CallGeneratorParams params) throws ApiException { ResolvableType informerType = ResolvableType.forClassWithGenerics( SharedInformer.class, kubernetesInformer.apiTypeClass()); - beanFactory.registerResolvableDependency(informerType.resolve(), sharedIndexInformer); + RootBeanDefinition informerBean = new RootBeanDefinition(); + informerBean.setTargetType(informerType); + informerBean.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + informerBean.setAutowireCandidate(true); + String informerBeanName = informerType.toString(); + this.beanDefinitionRegistry.registerBeanDefinition(informerBeanName, informerBean); + beanFactory.registerSingleton(informerBeanName, sharedIndexInformer); Lister lister = new Lister(sharedIndexInformer.getIndexer()); ResolvableType listerType = ResolvableType.forClassWithGenerics(Lister.class, kubernetesInformer.apiTypeClass()); - beanFactory.registerResolvableDependency(listerType.resolve(), lister); + RootBeanDefinition listerBean = new RootBeanDefinition(); + listerBean.setTargetType(listerType); + listerBean.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + listerBean.setAutowireCandidate(true); + String listerBeanName = listerType.toString(); + this.beanDefinitionRegistry.registerBeanDefinition(listerBeanName, listerBean); + beanFactory.registerSingleton(listerBeanName, lister); } } @@ -137,4 +158,10 @@ public Watchable watch(CallGeneratorParams params) throws ApiException { public int getOrder() { return 0; } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) + throws BeansException { + this.beanDefinitionRegistry = registry; + } }