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

от автора

Оглавление

  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 } 

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/827794/