Оглавление
-
Инициализация информеров для получения Pods, Nodes и Ingresses
-
Извлечение данных из информеров и их подготовка для отправки через API
Небольшой дисклеймер
Проект создан для учебных целей, поэтому код в некоторых местах намеренно (или случайно) упрощен.
Например, path для ingress взят просто первый из списка, на каждый вызов API создается отдельный экземпляр клиента и т. д.
-
Введение
Привет, меня зовут Сергей, старший разработчик 80 уровня компании DataBlend (группа компаний GlowByte). Наша команда занимается разработкой продукта ClusterManager, который управляет поведением и мониторит состояния таких продуктов, как GreenPlum, ClickHouse, DWH, Nova и т. д.
Около полутора лет назад у нас появилась необходимость собирать и отображать в удобном виде и разрезах метрики и данные об объектах кластеров Kubernetes, в которых развернут продукт Nova.
Для этих целей был выбран официальный kubernetes-client для Java.
Поначалу мы пошли по пути сбора данных о нодах, подах и так далее по расписанию и сохранению их в БД в удобном виде. Но, как это часто бывает, цели и желания со временем меняются, и жизнь заставила перейти к мгновенному получению и отображению изменений.
Лучше всего для этой цели подходит механизм информеров kubernetes-client.
И сейчас мы посмотрим, с какой стороны их лучше начинать есть.
Напишем простое приложение, которое в реальном времени отслеживает состояние Pods, Nodes и Ingresses и по запросу отдает нам информацию о них. Для этого мы повесим информеры на указанные ресурсы Kubernetes.
Если нужно отслеживать CRD-ресурсы, то информеры, к сожалению, не подойдут.
Получать и хранить информацию о ресурсах Kubernetes будем в памяти приложения.
«А у нас этой памяти – завались, у нас папа на фабрике по производству чипов памяти работает»
как сказал бы кот Матроскин.
-
Описание создания проекта с нуля
Этот пункт не имеет прямого отношения к 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.
На этом подготовка проекта завершена, можно переходить к тому, ради чего это затевалось.
-
Создание клиентов 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.
-
Инициализация информеров для получения 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, зависит от состава собираемых ресурсов.
Например, у нас в проекте внутри такого обработчика выполняется сложный обсчет статусов сервисов, которые зависят от состава подов и пишутся в историю изменений. Без такого условия при запуске приложения мы получали стопку записей об изменении статусов, в то время как кластер спокойно продолжал работать не меняясь.
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; } }
-
Создание 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()); } }
-
Извлечение данных из информеров и их подготовка для отправки через 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); }
-
Проверка результата
Запускаем приложение с параметром:
-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/
Добавить комментарий