Работа с информерами Java kubernetes client

0e68bf2c5b6f587d06d96e4582ae104c.jpg

Оглавление

  1. Введение

  2. Описание создания проекта с нуля

  3. Создание клиентов API для получения объектов kubernetes

  4. Инициализация информеров для получения  Pods, Nodes и Ingresses

  5. Создание Listener для запуска информеров

  6. Извлечение данных из информеров и их подготовка для отправки через API

  7. Проверка результата

Небольшой дисклеймер

Проект создан для учебных целей, поэтому код в некоторых местах намеренно (или случайно) упрощен.

Например, path для ingress взят просто первый из списка, на каждый вызов API создается отдельный экземпляр клиента и т. д.

cebec6c9f25eba7066ecfffafb14a04a.jpg

  1. Введение

Привет, меня зовут Сергей, старший разработчик 80 уровня компании DataBlend (группа компаний GlowByte). Наша команда занимается разработкой продукта ClusterManager, который управляет поведением и мониторит состояния таких продуктов, как GreenPlum, ClickHouse, DWH, Nova и т. д.

Около полутора лет назад у нас появилась необходимость собирать и отображать в удобном виде и разрезах метрики и данные об объектах кластеров Kubernetes, в которых развернут продукт Nova. 

Для этих целей был выбран официальный kubernetes-client для Java.

Поначалу мы пошли по пути сбора данных о нодах, подах и так далее по расписанию и сохранению их в БД в удобном виде. Но, как это часто бывает, цели и желания со временем меняются, и жизнь заставила перейти к мгновенному получению и отображению изменений.

Лучше всего для этой цели подходит механизм информеров kubernetes-client.

И сейчас мы посмотрим, с какой стороны их лучше начинать есть.

Напишем простое приложение, которое в реальном времени отслеживает состояние Pods, Nodes и Ingresses и по запросу отдает нам информацию о них. Для этого мы повесим информеры на указанные ресурсы Kubernetes.

Если нужно отслеживать CRD-ресурсы, то информеры, к сожалению, не подойдут.

Получать и хранить информацию о ресурсах Kubernetes будем в памяти приложения. 

«А у нас этой памяти — завались, у нас папа на фабрике по производству чипов памяти работает»

как сказал бы кот Матроскин.

  1. Описание создания проекта с нуля

Этот пункт не имеет прямого отношения к kubernetes-client, если нет нужды повторять проект, этот пункт можно пропустить и скачать уже готовый проект.

Ссылка на GitHub проекта.

Для запуска приложения необходимо из папки проекта выполнить команду:  

mvn clean generate-sources

И запустить проект с параметром:

-Dkubernetes.config-file.path=your-path/config.kubeconfig

Заменив »your-path/config.kubeconfig» на путь к своему конфиг-файлу Kubernetes.

Стек проекта:
Java 17, Spring Boot 3.3.0, Mapstruct, Lombok, Kubernetes-client 20.0.1, OpenApi 3.0.

Процесс создания проекта подробно

Первым делом мы создаем новый проект на основе Spring Boot:

2aa7c6708a8f2b63be1383962a1403f7.png2a46e54148aa8869d5f0a2cf03bca8c3.png

Далее добавляем в pom.xml зависимости для генерации контроллеров из спецификации OpenApi 3.0, mapstruct для маппинга DTO и, собственно, kubernetes-client:


   
       org.springframework.boot
       spring-boot-starter-web
   


   
       org.projectlombok
       lombok
       true
   
   
       org.springframework.boot
       spring-boot-starter-test
       test
   


   
       io.swagger.core.v3
       swagger-annotations
       2.2.22
   


   
       javax.validation
       validation-api
       2.0.1.Final
   


   
       org.mapstruct
       mapstruct
       ${mapstruct.version}
   


   
       io.kubernetes
       client-java
       20.0.1
   

Добавляем плагины для генерации контроллеров и DTO из спецификации OpenApi 3.0, а также maven plugin, в котором прописываем процессоры lombok и mapstruct, не забудьте заменить пути на свои:


   
       
           org.apache.maven.plugins
           maven-compiler-plugin
           3.8.1
           
               ${java.version}
               ${java.version}
               
                   
                       org.mapstruct
                       mapstruct-processor
                       ${mapstruct.version}
                   
                   
                       org.projectlombok
                       lombok
                       1.18.22
                   
               
           
       
       
           org.openapitools
           openapi-generator-maven-plugin
           7.6.0
           
               
                   core
                   
                       generate
                   
                   
                       src/main/resources/openapi.yml
                       target/generated-sources/openapi
                       spring
                       spring-cloud
                       ru.kapustin.kubernetesmanager.controller
                       ru.kapustin.kubernetesmanager.model
                       false
                       src/main/resources/templates
                       
                           false
                           true
                           true
                       
                   
               
           
       
   

Создаем файл openapi.yml со спецификацией OpenApi 3.0, из которой будут сгенерированы интерфейсы контроллеров и DTOшки:

openapi: 3.0.1
servers:
 - url: '{protocol}:{domain}/kubernetes-manager/api'


info:
 title: Kubernetes manager Service API
 description: Kubernetes manager Service API
 version: 1.0.0


paths:
 /pod/list:
   get:
     tags:
       - ResourceList
     operationId: getPods
     description: Get list of pods
     responses:
       200:
         description: Get list of pods
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/PodListResponse'


 /node/list:
   get:
     tags:
       - ResourceList
     operationId: getNodes
     description: Get list of nodes
     responses:
       200:
         description: Get list of nodes
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/NodeListResponse'


 /ingress/{namespace}/list:
   get:
     tags:
       - ResourceList
     operationId: getIngresses
     description: Get list of ingresses
     parameters:
       - name: namespace
         in: path
         required: true
         schema:
             type: string
     responses:
       200:
         description: Get list of ingresses
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/IngressListResponse'


components:
 schemas:
   PodListResponse:
     type: object
     properties:
       pods:
         type: array
         items:
           $ref: '#/components/schemas/Pod'
       total:
         type: integer
         format: int32


   Pod:
     type: object
     properties:
       name:
         type: string
       namespace:
         type: string
       status:
         type: string
       restartCount:
         type: integer
         format: int32
       creationTimestamp:
         type: string
         format: date-time
       labels:
         type: object
         additionalProperties:
           type: string
       annotations:
         type: object
         additionalProperties:
           type: string


   NodeListResponse:
     type: object
     properties:
       nodes:
         type: array
         items:
           $ref: '#/components/schemas/Node'
       total:
         type: integer
         format: int32


   Node:
     type: object
     properties:
       name:
         type: string
       status:
         type: string
       labels:
         type: object
         additionalProperties:
           type: string
       annotations:
         type: object
         additionalProperties:
           type: string


   IngressListResponse:
     type: object
     properties:
       ingresses:
         type: array
         items:
           $ref: '#/components/schemas/Ingress'
       total:
         type: integer
         format: int32


   Ingress:
     type: object
     properties:
       name:
         type: string
       namespace:
         type: string
       host:
         type: string
       path:
         type: string

Заполняем файл application.yml (либо application.properties, как удобно)

server:
 servlet:
   context-path: /kubernetes-manager/api
 port: 8080


kubernetes:
 config-file:
   path: ${kubernetes.config-file.path}

В переменную kubernetes.config-file.path мы будем передавать путь к нашему конфиг-файлу Kubernetes.

Теперь генерируем интерфейсы контроллеров и модели командой:
mvn clean generate-sources 

Если все прошло успешно, то в папке target мы увидим такую картину:

ba2f0e0c3d00c47fa8f822f7582941f9.png

Теперь помечаем package generated-sources как »Generated Source Root», в Intellij IDEA для этого вызываем контекстное меню на нужной папке и выбираем »Mark Directory as»/ «Generated Source Root». Папка в интерфейсе посинеет.

Если у вас не IDEA, мои полномочия все, разбирайтесь.

Создаем контроллер ResourceListController, имплементируем сгенерированный интерфейс ResourceListApi:

@RestController
@RequiredArgsConstructor
public class ResourceListController implements ResourceListApi {
   private final PodListService podListService;
   private final NodeListService nodeListService;
   private final IngressListService ingressListService;


   @Override
   public ResponseEntity getIngresses() {
       return ResponseEntity.ok(ingressListService.getIngresses());
   }


   @Override
   public ResponseEntity getNodes() {
       return ResponseEntity.ok(nodeListService.getNodes());
   }


   @Override
   public ResponseEntity getPods() {
       return ResponseEntity.ok(podListService.getPods());
   }
}

Классы PodListService, NodeListService и IngressListService будут созданы в пункте 6.

На этом подготовка проекта завершена, можно переходить к тому, ради чего это затевалось.

  1. Создание клиентов API для получения объектов Kubernetes

На момент написания статьи последняя версия клиента — 20.0.1. От версии к версии функционал библиотеки, структура классов, модели данных и т. д. у клиента может меняться, учитывайте это.
Радует, что выпускаются также новые версии клиента с поддержкой старой структуры данных и методов. Например, версия 20.0.1-legacy поддерживает код, написанный для 18 версии клиента.

Также версия 20.0.1 прекратила поддержку Java 8.

Структура нашего приложения будет выглядеть таким образом:

d5da61faae7168bd03cd7a0c83cb691e.png

Создаем класс KubernetesResourceService, отвечающий за создание и выдачу api для подключения к Kubernetes

@Service
public class KubernetesResourceService {


   @Value("${kubernetes.config-file.path}")
   private String configFilePath;


   private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesResourceService.class);


   public Optional getCoreV1Api() {
       Optional clientOptional = getApiClient();
       if (clientOptional.isEmpty()) {
           return Optional.empty();
       }
       ApiClient client = clientOptional.get();
       CoreV1Api api = new CoreV1Api(client);
       return Optional.of(api);
   }


   public Optional getSharedInformerFactory() {
       Optional clientOptional = getApiClient();
       if (clientOptional.isEmpty()) {
           LOGGER.warn("ApiClient is null.");
           return Optional.empty();
       }
       ApiClient client = clientOptional.get();
       client.setReadTimeout(0);
       SharedInformerFactory factory = new SharedInformerFactory(client);
       return Optional.of(factory);
   }


   public Optional getNetworkingApi() {
       Optional clientOptional = getApiClient();
       if (clientOptional.isEmpty()) {
           return Optional.empty();
       }
       ApiClient client = clientOptional.get();
       NetworkingV1Api api = new NetworkingV1Api();
       api.setApiClient(client);
       return Optional.of(api);
   }


   private Optional configFile() {
       try {
           Path filePath = Paths.get(configFilePath);
           byte[] fileBytes = Files.readAllBytes(filePath);
           String configFile = new String(fileBytes);
           return Optional.of(configFile);
       } catch (IOException e) {
           LOGGER.error("Error while getting Kubernetes configFile: {}", e.getMessage());
           return Optional.empty();
       }
   }


   private Optional getApiClient() {
       Optional configFileOptional = configFile();
       if (configFileOptional.isEmpty()) {
           LOGGER.error("Config file is empty or null.");
           return Optional.empty();
       }
       String configFile = configFileOptional.get();


       try (Reader reader = new StringReader(configFile)){
           KubeConfig kubeConfig = KubeConfig.loadKubeConfig(reader);
           ApiClient client = ClientBuilder.kubeconfig(kubeConfig).build();
           client.setReadTimeout(60000);
           return Optional.ofNullable(client);
       } catch (IOException e) {
           LOGGER.error("Error while getting kubernetes client from config file");
           return Optional.empty();
       }
   }
}




   private Optional getApiClient() {
       Optional configFileOptional = configFile();
       if (configFileOptional.isEmpty()) {
           LOGGER.error("Config file is empty or null.");
           return Optional.empty();
       }
       String configFile = configFileOptional.get();


       try (Reader reader = new StringReader(configFile)){
           KubeConfig kubeConfig = KubeConfig.loadKubeConfig(reader);
           ApiClient client = ClientBuilder.kubeconfig(kubeConfig).build();
           client.setReadTimeout(60000);
           return Optional.ofNullable(client);
       } catch (IOException e) {
           LOGGER.error("Error while getting kubernetes client from config file");
           return Optional.empty();
       }
   }
}

Переменная configFilePath – это наш путь к конфиг-файлу Kubernetes, значение которой мы получаем из системного свойства, указанного при запуске.

Метод getConfig получает содержимое файла.

Метод getApiClient создает клиент на основе конфиг-файла.

ApiClient: Базовый клиент для взаимодействия с Kubernetes API. Он управляет соединениями, аутентификацией и общими настройками.

При необходимости в нем можно также указать URL, Credentials и т. д.

Строка »client.setReadTimeout (60000); » настраивает время ожидания (timeout) для операций чтения на клиенте Kubernetes (ApiClient). Мы установили время ожидания 60 секунд. Значение 0 означает, что время ожидания будет бесконечным.

При создании SharedInformerFactory нужно установить client.setReadTimeout (0);  — так как его соединение должно быть бессрочным.

На основе ApiClient создаются другие объекты в методах getCoreV1Api, getNetworkingApi, getSharedInformerFactory — с их помощью мы обращаемся к Kubernetes.

  1. Инициализация информеров для получения  Pods, Nodes и Ingresses

Теперь нам нужно создать информеры, которые будут запускаться вместе с приложением, загружать нужные нам типы объектов и следить за их актуальностью.

Создаем класс InitKubernetesResourceService

@Service
@RequiredArgsConstructor
public class InitKubernetesResourceService {
   private static final Logger LOGGER = LoggerFactory.getLogger(InitKubernetesResourceService.class);


   private final KubernetesResourceService kubernetesResourceService;
   private final KubernetesResourceInformerFactoryService informerFactoryService;
   private final KubernetesResourceInformerContextBuilderService contextBuilderService;
   private final KubernetesResourceInformerContextManager contextManager;


   public void watchResources() {
       Optional informerFactoryOptional = kubernetesResourceService.getSharedInformerFactory();
       if (informerFactoryOptional.isEmpty()) {
           LOGGER.error("Failed to initialize KubernetesApiFactory due to missing SharedInformerFactory.");
           return;
       }
       SharedInformerFactory informerFactory = informerFactoryOptional.get();


       Optional coreV1ApiOptional = kubernetesResourceService.getCoreV1Api();
       if (coreV1ApiOptional.isEmpty()) {
           LOGGER.error("Failed to initialize KubernetesApiFactory due to missing CoreV1Api.");
           return;
       }
       CoreV1Api coreV1Api = coreV1ApiOptional.get();


       Optional networkingV1ApiOptional = kubernetesResourceService.getNetworkingApi();
       if (networkingV1ApiOptional.isEmpty()) {
           LOGGER.error("Failed to initialize KubernetesApiFactory due to missing NetworkingV1Api.");
           return;
       }
       NetworkingV1Api networkingV1Api = networkingV1ApiOptional.get();


       informerFactoryService.registerInformers(informerFactory, coreV1Api, networkingV1Api);


       KubernetesResourceInformerContext context = contextBuilderService.buildContext(informerFactory);


       contextManager.putContext(context);


       informerFactory.startAllRegisteredInformers();
   }
}

Здесь мы получаем объекты SharedInformerFactory, CoreV1Api, NetworkingV1Api, с их помощью зарегистрируем информеры в классе KubernetesResourceInformerFactoryService, сохраним ссылки на них в классе KubernetesResourceInformerContext. После чего передадим объект KubernetesResourceInformerContext для хранения и выдачи в KubernetesResourceInformerContextManager.

В конце запустим все информеры informerFactory.startAllRegisteredInformers ();

По-хорошему, когда информеры больше не нужны, их нужно остановить методом informerFactory.stopAllRegisteredInformers (true); , но я буду плохим.

4.1 Создание и регистрация информеров

@Service
@RequiredArgsConstructor
public class KubernetesResourceInformerFactoryService {
   private static final Long RESYNC_PERIOD_MILLISECONDS = 600000L;
   private static final Integer TIMEOUT = 300;


   private final ResourceEventHandlerBuilder handlerBuilder;


   public void registerInformers(SharedInformerFactory informerFactory, CoreV1Api coreV1Api, NetworkingV1Api networkingV1Api) {
       CallGenerator podCallGenerator = getPodCallGenerator(coreV1Api);
       CallGenerator nodeCallGenerator = getNodeCallGenerator(coreV1Api);
       CallGenerator ingressCallGenerator = getIngressCallGenerator(networkingV1Api);


       informerFactory.sharedIndexInformerFor(podCallGenerator, V1Pod.class, V1PodList.class, RESYNC_PERIOD_MILLISECONDS);
       informerFactory.sharedIndexInformerFor(nodeCallGenerator, V1Node.class, V1NodeList.class, RESYNC_PERIOD_MILLISECONDS);
       informerFactory.sharedIndexInformerFor(ingressCallGenerator, V1Ingress.class, V1IngressList.class, RESYNC_PERIOD_MILLISECONDS);




       SharedIndexInformer podInformer = informerFactory.getExistingSharedIndexInformer(V1Pod.class);




       ResourceEventHandler podResourceEventHandler = handlerBuilder.getPodResourceEventHandler(podInformer);


       podInformer.addEventHandler(podResourceEventHandler);
   }


   private CallGenerator getPodCallGenerator(CoreV1Api coreV1Api) {
       return (CallGeneratorParams params) -> coreV1Api.listPodForAllNamespaces()
               .resourceVersion(params.resourceVersion)
               .watch(params.watch)
               .timeoutSeconds(TIMEOUT)
               .buildCall(null);
   }


   private CallGenerator getNodeCallGenerator(CoreV1Api coreV1Api) {
       return (CallGeneratorParams params) -> coreV1Api.listNode()
               .resourceVersion(params.resourceVersion)
               .watch(params.watch)
               .timeoutSeconds(TIMEOUT)
               .buildCall(null);
   }


   private CallGenerator getIngressCallGenerator(NetworkingV1Api networkingV1Api) {
       return (CallGeneratorParams params) -> networkingV1Api.listIngressForAllNamespaces()
               .resourceVersion(params.resourceVersion)
               .watch(params.watch)
               .timeoutSeconds(TIMEOUT)
               .buildCall(null);
   }
}

Для того, чтобы создать информеры, мы должны создать объекты CallGenerator для каждого информера.

Также нам нужен отдельный информер для каждого типа ресурсов.

Переменная RESYNC_PERIOD_MILLISECONDS отвечает за период времени в миллисекундах между повторными синхронизациями.

Информеры меняют состояние объектов у себя внутри сразу при изменении их в Kubernetes, но также время от времени проводят полную ресинхронизацию на случай, если какое-то событие было пропущено.

Также хочу заметить, что в данном примере мы вешаем информеры на все Pods, Nodes, Ingresses кластера, но есть возможность фильтровать их по неймспейсам, лейблам и т. д. еще на этапе создания информера. Также можно создать несколько информеров для одного ресурса, чтобы забирать Pods только из определенных неймспейсов с указанными лейблами, например.

Регистрируем информеры методом informerFactory.sharedIndexInformerFor (podCallGenerator, V1Pod.class, V1PodList.class, RESYNC_PERIOD_MILLISECONDS), указывая соответствующие генераторы и классы для них.

Также можно создать обработчики событий для информеров.

В нашем примере мы создадим такой обработчик для информера Pods, чтобы логировать события, происходящие с подами.

Для этого получим сам информер podInformer из фабрики и передадим его в ResourceEventHandlerBuilder. Полученный обработчик podResourceEventHandler добавим в информер podInformer.addEventHandler (podResourceEventHandler)

@Service
@RequiredArgsConstructor
public class ResourceEventHandlerBuilder {
   private static final Logger LOGGER = LoggerFactory.getLogger(ResourceEventHandlerBuilder.class);


   public ResourceEventHandler getPodResourceEventHandler(SharedIndexInformer podInformer) {
       return new ResourceEventHandler() {
           @Override
           public void onAdd(V1Pod pod) {
               if(podInformer.hasSynced()){
                   LOGGER.info("Pod {} added", pod.getMetadata().getName());
               }
           }


           @Override
           public void onUpdate(V1Pod oldPod, V1Pod newPod) {
               if(podInformer.hasSynced()){
                   LOGGER.info("Pod {} updated", newPod.getMetadata().getName());
               }
           }


           @Override
           public void onDelete(V1Pod pod, boolean deletedFinalStateUnknown) {
               if(podInformer.hasSynced()){
                   LOGGER.info("Pod {} deleted", pod.getMetadata().getName());
               }
           }
       };
   }
}

Сам билдер достаточно прост. Мы создаем новый объект ResourceEventHandler и переопределяем три метода, добавляя в них логирование, которое срабатывает тогда, когда информер уже синхронизирован.

podInformer.hasSynced () возвращает true в случае, если информер уже был первоначально синхронизирован. Это важно, если код, выполняемый внутри методов ResourceEventHandler, зависит от состава собираемых ресурсов.

Например, у нас в проекте внутри такого обработчика выполняется сложный обсчет статусов сервисов, которые зависят от состава подов и пишутся в историю изменений. Без такого условия при запуске приложения мы получали стопку записей об изменении статусов, в то время как кластер спокойно продолжал работать не меняясь.

Девопс кластера Nova, когда увидел 50 FAILED и WARNING в истории статусов

Девопс кластера Nova, когда увидел 50 FAILED и WARNING в истории статусов

4.2 Хранение ссылок на информеры

Ссылки на информеры хранятся в record KubernetesResourceInformerContext

public record KubernetesResourceInformerContext(
       SharedIndexInformer podInformer,
       SharedIndexInformer nodeInformer,
       SharedIndexInformer ingressInformer
){
}

В общем-то мы можем хранить ссылку на саму фабрику SharedInformerFactory,   получая информеры непосредственно из нее, но это не очень удобно.

Управляет объектом KubernetesResourceInformerContext класс KubernetesResourceInformerContextManager

@Service
public class KubernetesResourceInformerContextManager {
   private final AtomicReference informerContextRef = new AtomicReference<>();


   public KubernetesResourceInformerContext getContext() {
       return this.informerContextRef.get();
   }


   public void putContext(KubernetesResourceInformerContext context) {
       this.informerContextRef.set(context);
   }
}
  1. Создание Listener для запуска информеров

Теперь создадим Listener, который после запуска приложения создаст информеры и сложит их в KubernetesResourceInformerContext

@Service
@RequiredArgsConstructor
public class AppStartUpEventListener {
   public static final Logger LOGGER = LoggerFactory.getLogger(AppStartUpEventListener.class);


   private final InitKubernetesResourceService kubernetesResourceService;


   @EventListener(ApplicationReadyEvent.class)
   public void applicationReady() {
       LOGGER.info("Start [{}]", ApplicationReadyEvent.class.getName());
       CompletableFuture
               .runAsync(() -> {})
               .thenRunAsync(() -> {
                   try {
                       kubernetesResourceService.watchResources();
                   } catch (Exception e) {
                       LOGGER.error("ERROR on application start up event", e);
                   }
               });
       LOGGER.info("Stop [{}]", ApplicationReadyEvent.class.getName());
   }
}
  1. Извлечение данных из информеров и их подготовка для отправки через API

Мы молодцы. Информеры создаются при запуске приложения и собирают инфу о Pods, Nodes, Ingresses.

5fb926bbe7ffa5eeec61f5406d9f17c0.png

Теперь мы создадим класс KubernetesObjectsFetcherService, который будет извлекать эти важные данные.

@Service
@RequiredArgsConstructor
public class KubernetesObjectsFetcherService {
   private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesObjectsFetcherService.class);


   private final KubernetesResourceInformerContextManager contextManager;


   public List getPods() {
       KubernetesResourceInformerContext context = contextManager.getContext();
       if (context == null) {
           LOGGER.error("PodInformerContext is null");
           return List.of();
       }
       return getV1Pods(context);
   }


   public List getNodes() {
       KubernetesResourceInformerContext context = contextManager.getContext();
       if (context == null) {
           LOGGER.error("NodeInformerContext is null");
           return List.of();
       }
       return getV1Nodes(context);
   }


   public List getNamespacedIngresses(String namespace) {
       KubernetesResourceInformerContext context = contextManager.getContext();
       if (context == null) {
           LOGGER.error("IngressInformerContext is null");
           return List.of();
       }
       List ingresses = getV1Ingresses(context);
       return filteredIngresses(namespace, ingresses);
   }


   protected List filteredIngresses(String namespace, List ingresses) {
       return Optional.ofNullable(ingresses).orElse(List.of())
               .stream()
               .filter(ingress -> ingress.getMetadata() != null)
               .filter(ingress -> ingress.getMetadata().getNamespace() != null)
               .filter(ingress -> ingress.getMetadata().getNamespace().equals(namespace))
               .toList();
   }


   protected List getV1Pods(KubernetesResourceInformerContext context) {
       return Optional.ofNullable(context).map(KubernetesResourceInformerContext::podInformer).map(SharedIndexInformer::getIndexer).map(Store::list).orElse(List.of());
   }


   protected List getV1Nodes(KubernetesResourceInformerContext context) {
       return Optional.ofNullable(context).map(KubernetesResourceInformerContext::nodeInformer).map(SharedIndexInformer::getIndexer).map(Store::list).orElse(List.of());
   }


   protected List getV1Ingresses(KubernetesResourceInformerContext context) {
       return Optional.ofNullable(context).map(KubernetesResourceInformerContext::ingressInformer).map(SharedIndexInformer::getIndexer).map(Store::list).orElse(List.of());
   }
}

Для ингрессов я создал дополнительную фильтрацию по неймспейсам, так как наша спецификация предполагает это.

Далее создадим 3 класса бизнес-логики, которые отвечают за извлечение и преобразование объектов в нужный вид.

Класс IngressListService

@Service
@RequiredArgsConstructor
public class IngressListService {
   private final ResourcesMapper mapper;
   private final KubernetesObjectsFetcherService kubernetesObjectsFetcherService;


   public IngressListResponse getIngresses(String namespace) {
       List v1Ingresses = kubernetesObjectsFetcherService.getNamespacedIngresses(namespace);
       List ingresses = getIngressItems(v1Ingresses);
       Integer total = ingresses.size();
       return getResponse(ingresses, total);
   }


   private IngressListResponse getResponse(List ingresses, Integer total) {
       return new IngressListResponse().ingresses(ingresses).total(total);
   }


   private List getIngressItems(List v1Ingresses) {
       return v1Ingresses.stream()
               .map(this::mapIngress)
               .filter(Objects::nonNull)
               .toList();
   }


   private Ingress mapIngress(V1Ingress v1Ingress) {
       String name = getName(v1Ingress).orElse(null);
       String namespace = getNamespace(v1Ingress).orElse(null);
       String host = getHost(v1Ingress).orElse(null);
       String path = getPath(v1Ingress).orElse(null);
       return mapper.mapIngress(name, namespace, host, path);
   }


   private Optional getPath(V1Ingress v1Ingress) {
       return Optional.ofNullable(v1Ingress)
               .map(V1Ingress::getSpec)
               .map(V1IngressSpec::getRules)
               .stream()
               .flatMap(Collection::stream)
               .flatMap(this::getV1HTTPIngressPathStream)
               .map(V1HTTPIngressPath::getPath)
               .findFirst();
   }


   private Stream getV1HTTPIngressPathStream(V1IngressRule rule) {
       return Optional.ofNullable(rule.getHttp())
               .map(http -> http.getPaths().stream())
               .orElseGet(Stream::empty);
   }


   private Optional getHost(V1Ingress v1Ingress) {
       return Optional.ofNullable(v1Ingress)
               .map(V1Ingress::getSpec)
               .map(V1IngressSpec::getRules)
               .map(List::stream)
               .orElseGet(Stream::empty)
               .map(V1IngressRule::getHost)
               .findFirst();
   }


   private Optional getNamespace(V1Ingress v1Ingress) {
       return Optional.ofNullable(v1Ingress)
               .map(V1Ingress::getMetadata)
               .map(V1ObjectMeta::getNamespace);
   }


   private Optional getName(V1Ingress v1Ingress) {
       return Optional.ofNullable(v1Ingress)
               .map(V1Ingress::getMetadata)
               .map(V1ObjectMeta::getName);
   }
}

Класс NodeListService

@Service
@RequiredArgsConstructor
public class NodeListService {
   private final ResourcesMapper mapper;
   private final KubernetesObjectsFetcherService kubernetesResourceFetcherService;
   public NodeListResponse getNodes() {
       List v1Nodes = kubernetesResourceFetcherService.getNodes();
       List nodes = getNodeItems(v1Nodes);
       Integer total = nodes.size();
       return getResponse(nodes, total);
   }


   private NodeListResponse getResponse(List nodes, Integer total) {
       return new NodeListResponse().nodes(nodes).total(total);
   }


   private List getNodeItems(List v1Nodes) {
       return v1Nodes.stream()
               .map(this::mapNode)
               .filter(Objects::nonNull)
               .toList();
   }


   private Node mapNode(V1Node v1Node) {
       String name = getName(v1Node).orElse(null);
       String status = getStatus(v1Node).orElse(null);
       Map labels = getLabels(v1Node);
       Map annotations = getAnnotations(v1Node);
       return mapper.mapNode(name, status, labels, annotations);
   }


   private Map getAnnotations(V1Node v1Node) {
       return Optional.ofNullable(v1Node)
               .map(V1Node::getMetadata)
               .map(V1ObjectMeta::getAnnotations)
               .orElse(Map.of());
   }


   private Map getLabels(V1Node v1Node) {
       return Optional.ofNullable(v1Node)
               .map(V1Node::getMetadata)
               .map(V1ObjectMeta::getLabels)
               .orElse(Map.of());
   }


   private Optional getStatus(V1Node v1Node) {
       return Optional.ofNullable(v1Node)
               .map(V1Node::getStatus)
               .map(V1NodeStatus::getPhase);
   }


   private Optional getName(V1Node v1Node) {
       return Optional.ofNullable(v1Node)
               .map(V1Node::getMetadata)
               .map(V1ObjectMeta::getName);
   }
}

Класс PodListService

@Service
@RequiredArgsConstructor
public class PodListService {
   private final ResourcesMapper mapper;
   private final KubernetesObjectsFetcherService kubernetesObjectsFetcherService;


   public PodListResponse getPods() {
       List v1Pods = kubernetesObjectsFetcherService.getPods();
       List pods = getPodItems(v1Pods);
       Integer total = pods.size();
       return getResponse(pods, total);
   }


   private PodListResponse getResponse(List pods, Integer total) {
       return new PodListResponse().pods(pods).total(total);
   }


   private List getPodItems(List v1Pods) {
       return v1Pods.stream()
               .map(this::mapPod)
               .filter(Objects::nonNull)
               .toList();
   }


   private Pod mapPod(V1Pod v1Pod) {
       String name = getName(v1Pod).orElse(null);
       String namespace = getNamespace(v1Pod).orElse(null);
       String status = getStatus(v1Pod).orElse(null);
       Integer restartCount = getRestartCount(v1Pod).orElse(0);
       OffsetDateTime creationTimestamp = geCreationTimestamp(v1Pod).orElse(null);
       Map labels = getLabels(v1Pod);
       Map annotations = getAnnotations(v1Pod);
       return mapper.mapPod(name, namespace, status, restartCount, creationTimestamp, labels, annotations);
   }


   protected Map getAnnotations(V1Pod v1Pod) {
       return Optional.ofNullable(v1Pod)
               .map(V1Pod::getMetadata)
               .map(V1ObjectMeta::getAnnotations)
               .orElse(Map.of());
   }


   protected Map getLabels(V1Pod v1Pod) {
       return Optional.ofNullable(v1Pod)
               .map(V1Pod::getMetadata)
               .map(V1ObjectMeta::getLabels)
               .orElse(Map.of());
   }


   protected Optional getStatus(V1Pod v1Pod) {
       return Optional.ofNullable(v1Pod)
               .map(V1Pod::getStatus)
               .map(V1PodStatus::getPhase);
   }


   protected Optional getNamespace(V1Pod v1Pod) {
       return Optional.ofNullable(v1Pod)
               .map(V1Pod::getMetadata)
               .map(V1ObjectMeta::getNamespace);
   }


   protected Optional getName(V1Pod v1Pod) {
       return Optional.ofNullable(v1Pod)
               .map(V1Pod::getMetadata)
               .map(V1ObjectMeta::getName);
   }


   protected Optional geCreationTimestamp(V1Pod v1Pod) {
       return Optional.ofNullable(v1Pod)
               .map(V1Pod::getMetadata)
               .map(V1ObjectMeta::getCreationTimestamp);
   }


   protected Optional getRestartCount(V1Pod pod) {
       return Optional.ofNullable(pod)
               .map(V1Pod::getStatus)
               .map(V1PodStatus::getContainerStatuses)
               .filter(statuses -> !statuses.isEmpty())
               .map(statuses -> statuses.get(0))
               .map(V1ContainerStatus::getRestartCount);
   }
}

А также маппер ResourcesMapper

@Mapper(componentModel = "spring", nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
public interface ResourcesMapper {


   @Mapping(target = "name", source = "name")
   @Mapping(target = "namespace", source = "namespace")
   @Mapping(target = "status", source = "status")
   @Mapping(target = "restartCount", source = "restartCount")
   @Mapping(target = "creationTimestamp", source = "creationTimestamp")
   @Mapping(target = "labels", source = "labels")
   @Mapping(target = "annotations", source = "annotations")
   Pod mapPod(String name,
              String namespace,
              String status,
              Integer restartCount,
              OffsetDateTime creationTimestamp,
              Map labels,
              Map annotations);


   @Mapping(target = "name", source = "name")
   @Mapping(target = "status", source = "status")
   @Mapping(target = "labels", source = "labels")
   @Mapping(target = "annotations", source = "annotations")
   Node mapNode(String name, String status, Map labels, Map annotations);


   @Mapping(target = "name", source = "name")
   @Mapping(target = "namespace", source = "namespace")
   @Mapping(target = "host", source = "host")
   @Mapping(target = "path", source = "path")
   Ingress mapIngress(String name, String namespace, String host, String path);
}
  1. Проверка результата

Запускаем приложение с параметром:

-Dkubernetes.config-file.path=your-path/config.kubeconfig

Заменив »your-path/config.kubeconfig» на путь к своему конфиг-файлу Kubernetes

Запускаем Postman и выполняем GET-запрос:

http://localhost:8080/kubernetes-manager/api/pod/list

Получаем json вида:

{
    "pods": [
        {
            "name": "ip-masq-agent-tsnsk",
            "namespace": "kube-system",
            "status": "Running",
            "restartCount": 0,
            "creationTimestamp": "2024-01-29T12:15:42Z",
            "labels": {
                "controller-revision-hash": "6d59d8409d",
                "k8s-app": "ip-masq-agent",
                "pod-template-generation": "1"
            },
            "annotations": {}
        },
        {
            "name": "catalog-76756d44fc-b4gdj",
            "namespace": "impala",
            "status": "Running",
            "restartCount": 0,
            "creationTimestamp": "2024-06-14T16:29:41Z",
            "labels": {
                "app": "catalog",
                "cm-role-type": "Catalog",
                "cm-service": "Impala-6",
                "nova-process-configmap": "true",
                "pod-template-hash": "74356df4fc"
            },
            "annotations": {
                "nova-master-secret": "master-secret",
                "seccomp.security.alpha.kubernetes.io/pod": "runtime/default"
            }
        }
    ],
    "total": 350
}

На этом у меня все.

Спасибо всем, кто осилил.

«Ставьте лайки, звездочки и колокольчики», как сказал мне когда-то миграционный полицейский в ответ на фразу «Адвокат говорит, что вы правы».

© Habrahabr.ru