Оглавление

  1. Введение

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

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

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

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

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

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

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

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

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

  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:

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

<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
   </dependency>


   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <optional>true</optional>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
   </dependency>


   <dependency>
       <groupId>io.swagger.core.v3</groupId>
       <artifactId>swagger-annotations</artifactId>
       <version>2.2.22</version>
   </dependency>


   <dependency>
       <groupId>javax.validation</groupId>
       <artifactId>validation-api</artifactId>
       <version>2.0.1.Final</version>
   </dependency>


   <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct</artifactId>
       <version>${mapstruct.version}</version>
   </dependency>


   <dependency>
       <groupId>io.kubernetes</groupId>
       <artifactId>client-java</artifactId>
       <version>20.0.1</version>
   </dependency>
</dependencies>

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

<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.8.1</version>
           <configuration>
               <source>${java.version}</source>
               <target>${java.version}</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${mapstruct.version}</version>
                   </path>
                   <path>
                       <groupId>org.projectlombok</groupId>
                       <artifactId>lombok</artifactId>
                       <version>1.18.22</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
       <plugin>
           <groupId>org.openapitools</groupId>
           <artifactId>openapi-generator-maven-plugin</artifactId>
           <version>7.6.0</version>
           <executions>
               <execution>
                   <id>core</id>
                   <goals>
                       <goal>generate</goal>
                   </goals>
                   <configuration>
                       <inputSpec>src/main/resources/openapi.yml</inputSpec>
                       <output>target/generated-sources/openapi</output>
                       <generatorName>spring</generatorName>
                       <library>spring-cloud</library>
                       <apiPackage>ru.kapustin.kubernetesmanager.controller</apiPackage>
                       <modelPackage>ru.kapustin.kubernetesmanager.model</modelPackage>
                       <generateSupportingFiles>false</generateSupportingFiles>
                       <templateDirectory>src/main/resources/templates</templateDirectory>
                       <configOptions>
                           <openApiNullable>false</openApiNullable>
                           <interfaceOnly>true</interfaceOnly>
                           <useTags>true</useTags>
                       </configOptions>
                   </configuration>
               </execution>
           </executions>
       </plugin>
   </plugins>
</build>

Создаем файл 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 мы увидим такую картину:

Теперь помечаем 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<IngressList> getIngresses() {
       return ResponseEntity.ok(ingressListService.getIngresses());
   }


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


   @Override
   public ResponseEntity<PodList> 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.

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

Создаем класс 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<CoreV1Api> getCoreV1Api() {
       Optional<ApiClient> clientOptional = getApiClient();
       if (clientOptional.isEmpty()) {
           return Optional.empty();
       }
       ApiClient client = clientOptional.get();
       CoreV1Api api = new CoreV1Api(client);
       return Optional.of(api);
   }


   public Optional<SharedInformerFactory> getSharedInformerFactory() {
       Optional<ApiClient> 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<NetworkingV1Api> getNetworkingApi() {
       Optional<ApiClient> 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<String> 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<ApiClient> getApiClient() {
       Optional<String> 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<ApiClient> getApiClient() {
       Optional<String> 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<SharedInformerFactory> informerFactoryOptional = kubernetesResourceService.getSharedInformerFactory();
       if (informerFactoryOptional.isEmpty()) {
           LOGGER.error("Failed to initialize KubernetesApiFactory due to missing SharedInformerFactory.");
           return;
       }
       SharedInformerFactory informerFactory = informerFactoryOptional.get();


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


       Optional<NetworkingV1Api> 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<V1Pod> podInformer = informerFactory.getExistingSharedIndexInformer(V1Pod.class);




       ResourceEventHandler<V1Pod> 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<V1Pod> getPodResourceEventHandler(SharedIndexInformer<V1Pod> podInformer) {
       return new ResourceEventHandler<V1Pod>() {
           @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<V1Pod> podInformer,
       SharedIndexInformer<V1Node> nodeInformer,
       SharedIndexInformer<V1Ingress> ingressInformer
){
}

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

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

@Service
public class KubernetesResourceInformerContextManager {
    private KubernetesResourceInformerContext informerContext;

    public KubernetesResourceInformerContext getContext() {
        return informerContext;
    }

    public void putContext(KubernetesResourceInformerContext context) {
        informerContext = 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.

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

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


   private final KubernetesResourceInformerContextManager contextManager;


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


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


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


   protected List<V1Ingress> filteredIngresses(String namespace, List<V1Ingress> 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<V1Pod> getV1Pods(KubernetesResourceInformerContext context) {
       return Optional.ofNullable(context).map(KubernetesResourceInformerContext::podInformer).map(SharedIndexInformer::getIndexer).map(Store::list).orElse(List.of());
   }


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


   protected List<V1Ingress> 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<V1Ingress> v1Ingresses = kubernetesObjectsFetcherService.getNamespacedIngresses(namespace);
       List<Ingress> ingresses = getIngressItems(v1Ingresses);
       Integer total = ingresses.size();
       return getResponse(ingresses, total);
   }


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


   private List<Ingress> getIngressItems(List<V1Ingress> 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<String> 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<V1HTTPIngressPath> getV1HTTPIngressPathStream(V1IngressRule rule) {
       return Optional.ofNullable(rule.getHttp())
               .map(http -> http.getPaths().stream())
               .orElseGet(Stream::empty);
   }


   private Optional<String> 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<String> getNamespace(V1Ingress v1Ingress) {
       return Optional.ofNullable(v1Ingress)
               .map(V1Ingress::getMetadata)
               .map(V1ObjectMeta::getNamespace);
   }


   private Optional<String> 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<V1Node> v1Nodes = kubernetesResourceFetcherService.getNodes();
       List<Node> nodes = getNodeItems(v1Nodes);
       Integer total = nodes.size();
       return getResponse(nodes, total);
   }


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


   private List<Node> getNodeItems(List<V1Node> 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<String, String> labels = getLabels(v1Node);
       Map<String, String> annotations = getAnnotations(v1Node);
       return mapper.mapNode(name, status, labels, annotations);
   }


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


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


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


   private Optional<String> 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<V1Pod> v1Pods = kubernetesObjectsFetcherService.getPods();
       List<Pod> pods = getPodItems(v1Pods);
       Integer total = pods.size();
       return getResponse(pods, total);
   }


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


   private List<Pod> getPodItems(List<V1Pod> 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<String, String> labels = getLabels(v1Pod);
       Map<String, String> annotations = getAnnotations(v1Pod);
       return mapper.mapPod(name, namespace, status, restartCount, creationTimestamp, labels, annotations);
   }


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


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


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


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


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


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


   protected Optional<Integer> 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<String, String> labels,
              Map<String, String> 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<String, String> labels, Map<String, String> 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
}

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

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

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

Комментарии (0)