Operators are software extensions to Kubernetes that make use of custom resources to manage applications and their components. Operators follow Kubernetes principles, notably the control loop. — from kubernetes.io В данной статье я постарался изложить на что обратить внимание при написании оператора на golang и на нюансы, которые описываются вскользь или вовсе не описываются в официальном туториале или других статьях подобного вида. В данной статье я кратко покажу:  Как подготовить окружение для создания оператора Как писать программу и что мы можем сделать внутри основной функции обработки событий (реконсилера) Когда вызывается реконсилер и как этим управлять Как выходить из реконсилера Как консистентно создавать и удалять объекты кластера Для примера мы создадим secret-operator который будет: Создавать необходимые секреты во всех неймспейсах кластера Создавать секреты при создании нового неймспейса Восстанавливать секрет, если его кто-то удалит Удалять всех потомков, если удаляется наш корневой объект Что данный оператор НЕ делает, для упрощения кода: Не обрабатываются изменения секретов Не реализована логика выбора неймспейса      Паттерн оператор реализуемый controller-runtime (kubebuilder, operator-sdk) очень похож на паттерн Наблюдатель (2). Мы “подписываемся” на события k8s на создание/изменение/удаление объектов на которые мы должны реагировать. При изменении данных ресурсов вызывается функция reconcile в которую передается имя "родительского" объекта к которому относятся данные события. В функции reconcile описывается проверка состояний родительских/дочерних/остальных объектов и реакция на данные события. Более подробно о том как происходит подписка на события и работа reconcile-loop описано далее. Скачайте необходимый архив для необходимой ОС по ссылке. Распакуйте архив например в директорию /opt/go-1.19.4 Создайте рабочую директорию для go и пропишите переменные окружения Скачиваем и проверяем необходимый бинарный исполняемый файл (ссылка) Если у вас нет предпочтительной IDE используйте Goland, скачать можно здесь. Триал 30 дней при регистрации по электронной почте. После  открытия первого проекта останется только прописать GOROOT|GOPATH в настройках (File -> settings -> Go) Описание на официальном сайте тут Исходный код проекта храниться на github  Создадим новый проект: Создаем новый API и контроллер: Правим название докер образа на тот что необходим в Makefile Подсказка по командам SDK Код оператора разделен на 2 основные части: api - объявляющую нашу новую "родительскую" сущность для k8s controller - код который читает желаемое состояние объектов k8s и стремиться применить его в k8s После выполнения команды operator-sdk create api сгенерировались файлы по пути api/v1alpha1 в файле multisecret_types.go описана наша новая “родительская” сущность для которой мы и будем писать большую часть последующего кода. Добавим в секцию spec необходимые поля, которые мы в последующем будем помещать в наши секреты, не забываем аннотации json - чтобы k8s смог в последующем эти данные сериализовать.  После каждого изменения данной части кода запускаем make generate для автогенерации необходимой части кода в файле zz_generated.deepcopy.go Так же добавляем описание структуры статусов нашего "родительского" объекта Наш контроллер выполняет следующую логику: По вызову реконсилера запрашивается объект multiv1alpha1.MultiSecret{} Выходим Если он не существует Если "родительский" объект в стадии удаления, удаляем все "дочерние" секреты и выходим (Finalizer) Проверяем какие "дочерние" секреты существуют по всем пространствам (namespace), в соответствии со спецификацией "родительского" объекта создаем или удаляем их. Reconcile - основная функция внутри которой мы проверяем состояние объектов и приводим их к желаемому состоянию. Функция обязательно должна быть идемпотентной, Вы не знаете в какой момент времени жизни объектов она вызовется. Из reconcile функции может быть несколько вариантов выхода в зависимости от того необходимо ли еще перезапустить цикл или нет. ctrl.Result{}, err - случилась ошибка при выполнении из-за которой мы не можем продолжить выполнение, возвращаем ее чтобы перезапустить цикл позже. ctrl.Result{Requeue: true}, nil - ошибки выполнения отсутствуют, но мы возвращаем управление контроллеру, что бы он мог обработать и другие объекты, и позже вернуться к текущему снова. ctrl.Result{}, nil   - перезапуск цикла не требуется, желаемое состояние = существующему. ctrl.Result{RequeueAfter: 60 * time.Second}, nil - перезапустить цикл после определенного времени. Можно использовать для уверенности в том что состояние объектов будет проверено и применено в данный интервал времени. Объект в k8s может быть создан, обновлен или быть в стадии удаления. Дополнительно к этому контроллер имеет свой кэш состояния объектов, с одной стороны это дает возможность не заботиться о том сколько раз мы запрашиваем состояние объекта, но так же надо понимать что мы можем “успеть” удалить один и тот же объект 2 раза. Либо реконсилер может быть вызван уже на удаленный объект. Для обработки таких ситуаций надо сравнивать возвращаемую ошибку на отсутствие объекта функцией IsNotFound(err) После каждого запроса объекта и удаления, надо понимать какой тип ошибки вернулся, это проблема доступа к API или такого объекта не существует, и уже на основании этого принимать решение что делать далее. В приведенном примере если наш объект multiSecret не существует, то мы ничего не делаем, если же это ошибка доступа к API мы возвращаем ошибку чтобы reconcileLoop снова встал в очередь на выполнение. Функция SetupWithManager - устанавливает события на которые должен срабатывать reconciler, а так же делает сопоставление, reconciler какого объекта необходимо вызывать. Первым идет метод For - для "родительского" объекта. Тут нам не надо ничего придумывать, на его события создания/изменения/удаления будет вызываться reconciler Если Вы задумали контроллер, который будет обрабатывать события только на объекты созданные им самим, и объекты будут находится в том же пространстве (namespace), тогда можно использовать хендлеры через метод Owns https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/ Главное не забывать делать ссылку на “родительский” объект из “дочерних” Из приведенного ранее ТЗ нам надо отрабатывать:  События изменений нашего “родительского” объекта Объектов которые мы будем создавать (secrets) в разных пространствах (namespace) Объекты которые нам не принадлежат, создание новых пространств (namespace) Что мы тут видим - мы следим за объектами corev1.Secret, вызываем функцию secretHandlerFunc - которая делает сопоставление к какому "родительскому" объекту он относится. Сама функция действует несложно, ищем в объекте необходимые аннотации, и возвращаем вызов необходимого реконсилера "родительского" объекта. Предикат указывает на то когда срабатывать. Данный предикат срабатывает на любое изменение версии объекта, создание и удаление сюда тоже входит. По corev1.Namespace похожее поведение, но отрабатываем только создание пространства и в nsHandlerFunc отдаем вызовы на все реконсилеры наших “родительских” объектов. Более подробно о слежении за ресурсами можно почитать здесь. Финализаторы ставятся на объект чтобы была возможность выполнить необходимые действия при удалении объекта, например как в нашем случае удалить все "дочерние" секреты. Если мы создаем "дочерние" объекты в том же пространстве (namespace) что и "родительский" объект, то финализаторы для удаления  "дочерних" нам не нужны.  Хватит указания на "родительский" объект, а k8s удалит их сам. подробнее В нашем случае при удалении "родительского" объекта мы хотим удалять и все "дочерние" во всех пространствах (namespaces) для этого будем использовать финализатор. Идея простая: k8s при удалении объекта проставляет время удаления и смотрит есть ли у него финализаторы. Пока они есть - объект не удаляется и дается время чтобы контролеры могли закончить необходимые действия. В нашем коде пишем следующую логику: Если родительский объект не в стадии удаления и у него нет нашего финализатора, то мы его добавляем Если родительский объект в стадии удаления, удаляем все "дочерние" и в случае успеха удаляем запись финализатора Добавим статусы нашему объекту, чтобы они красиво выводились по get запросу: По коду добавляем счетчики: Добавляем обновление статуса при выходе из функции. Обновление будет срабатывать только при выходе без ошибок, для этого мы задали имена выходным параметрам: Здесь же можно посмотреть как выполняется patch статуса объекта: Дополнительно чтобы работал вывод статуса с get необходимо дополнительно задать маркеры для генерации CRD, ссылка на дополнительную информацию. Для того чтобы в последствии понимать, что делает наш контроллер, добавим генерацию событий (Events), которые можно будет видеть, в том числе и в выводе describe объекта multisecret. Для этого в структуру реконсилера добавляем рекордер и права в маркере: В main.go добавляем инициализацию рекордера:   В последующем при создании и удалении секретов, пишем события: В данной статье я постарался показать как расширить стандартный оператор из примера до рабочего состояния. А так же рассмотрели как следить за ресурсами, как изменять статус ресурса, как управлять реконсилером,  как писать события, как сделать свой финализатор. ЗЫ: Большое спасибо коллегам из ИТ-Сумма за посильный вклад в создание данной статьи.