Оглавление
Инициализация информеров для получения 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
}
На этом у меня все.
Спасибо всем, кто осилил.
«Ставьте лайки, звездочки и колокольчики», как сказал мне когда‑то миграционный полицейский в ответ на фразу «Адвокат говорит, что вы правы».