Этот пост с примером из реальной жизни я решил написать после своих попыток найти документацию по созданию оператора для Kubernetes, прошедших через изучение кода.
Пример, который будет описан, таков: в нашем кластере Kubernetes каждый
Namespace представляет окружение-песочницу какой-то команды, и мы хотели ограничить доступ к ним так, чтобы команды могли играть только в своих песочницах.Достичь желаемого можно назначением пользователю группы, у которой есть
RoleBinding к конкретным Namespace и ClusterRole с правом на редактирование. YAML-представление будет выглядеть так:--- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1beta1 metadata: name: kubernetes-team-1 namespace: team-1 subjects: - kind: Group name: kubernetes-team-1 apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io
(rolebinding.yaml, в raw)
Создавать такой
RoleBinding можно и вручную, но после преодоления отметки в сотню пространств имён это становится утомительным занятием. Как раз здесь помогают операторы Kubernetes — они позволяют автоматизировать создание ресурсов Kubernetes, основываясь на изменениях в ресурсах. В нашем случае мы хотим создавать RoleBinding при создании Namespace.Первым делом определим функцию
main, которая выполняет требуемую настройку для запуска оператора и затем вызывает действие оператора:(Прим. перев.: здесь и далее комментарии в коде переведены на русский язык. Кроме того, отступы исправлены на пробелы вместо [рекомендуемых в Go] табов исключительно с целью лучшей читаемости в рамках вёрстки Хабра. После каждого листинга приведены ссылки на оригинал на GitHub, где сохранены англоязычные комментарии и табы.)
func main() { // Устанавливаем вывод логов в консольный STDOUT log.SetOutput(os.Stdout) sigs := make(chan os.Signal, 1) // Создаем канал для получения сигналов ОС stop := make(chan struct{}) // Создаем канал для получения стоп-сигнала // Регистрируем получение SIGTERM в канале sigs signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) // Goroutines могут сами добавлять себя в WaitGroup, // чтобы завершения их выполнения дожидались wg := &sync.WaitGroup{} runOutsideCluster := flag.Bool("run-outside-cluster", false, "Set this flag when running outside of the cluster.") flag.Parse() // Создаем clientset для взаимодействия с кластером Kubernetes clientset, err := newClientSet(*runOutsideCluster) if err != nil { panic(err.Error()) } controller.NewNamespaceController(clientset).Run(stop, wg) <-sigs // Ждем сигналов (до получения сигнала более ничего не происходит) log.Printf("Shutting down...") close(stop) // Говорим goroutines остановиться wg.Wait() // Ожидаем, что все остановлено }
(main.go, в raw)
Мы делаем следующее:
- Настраиваем обработчика конкретных сигналов операционной системы, чтобы вызвать корректное (graceful) завершение работы оператора.
- Используем
WaitGroup, чтобы корректно остановить все goroutines перед завершением работы приложения. - Предоставляем доступ к кластеру созданием
clientset. - Запускаем
NamespaceController, в котором будет расположена вся наша логика.
Теперь нужна основа для логики, и в нашем случае это упомянутый
NamespaceController:// NamespaceController следит через Kubernetes API за изменениями // в пространствах имен и создает RoleBinding для конкретного namespace. type NamespaceController struct { namespaceInformer cache.SharedIndexInformer kclient *kubernetes.Clientset } // NewNamespaceController создает новый NewNamespaceController func NewNamespaceController(kclient *kubernetes.Clientset) *NamespaceController { namespaceWatcher := &NamespaceController{} // Создаем информер для слежения за Namespaces namespaceInformer := cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return kclient.Core().Namespaces().List(options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { return kclient.Core().Namespaces().Watch(options) }, }, &v1.Namespace{}, 3*time.Minute, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, ) namespaceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: namespaceWatcher.createRoleBinding, }) namespaceWatcher.kclient = kclient namespaceWatcher.namespaceInformer = namespaceInformer return namespaceWatcher }
(controller.go, в raw)
Здесь мы настраиваем
SharedIndexInformer, который будет эффективно (используя кэш) ожидать изменений в пространствах имён (подробнее об informers читайте в статье «Как на самом деле работает планировщик Kubernetes?» — прим. перев.). После этого мы подключаем EventHandler к информеру, благодаря чему при добавлении пространства имён (Namespace) вызывается функция createRoleBinding.Следующий шаг — определить эту функцию
createRoleBinding:func (c *NamespaceController) createRoleBinding(obj interface{}) { namespaceObj := obj.(*v1.Namespace) namespaceName := namespaceObj.Name roleBinding := &v1beta1.RoleBinding{ TypeMeta: metav1.TypeMeta{ Kind: "RoleBinding", APIVersion: "rbac.authorization.k8s.io/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("ad-kubernetes-%s", namespaceName), Namespace: namespaceName, }, Subjects: []v1beta1.Subject{ v1beta1.Subject{ Kind: "Group", Name: fmt.Sprintf("ad-kubernetes-%s", namespaceName), }, }, RoleRef: v1beta1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: "edit", }, } _, err := c.kclient.Rbac().RoleBindings(namespaceName).Create(roleBinding) if err != nil { log.Println(fmt.Sprintf("Failed to create Role Binding: %s", err.Error())) } else { log.Println(fmt.Sprintf("Created AD RoleBinding for Namespace: %s", roleBinding.Name)) } }
(controller.go, в raw)
Мы получаем пространство имён как
obj и преобразуем его в объект Namespace. Затем определяем RoleBinding, основываясь на упомянутом в начале YAML-файле, используя предоставленный объект Namespace и создавая RoleBinding. Наконец, логируем, успешно ли прошло создание.Последняя функция, которую необходимо определить, —
Run:// Run запускает процесс ожидания изменений в пространствах имён // и действия в соответствии с этими изменениями. func (c *NamespaceController) Run(stopCh <-chan struct{}, wg *sync.WaitGroup) { // Когда эта функция завершена, пометим как выполненную defer wg.Done() // Инкрементируем wait group, т.к. собираемся вызвать goroutine wg.Add(1) // Вызываем goroutine go c.namespaceInformer.Run(stopCh) // Ожидаем получения стоп-сигнала <-stopCh }
(controller.go, в raw)
Здесь мы говорим
WaitGroup, что запустим goroutine и затем вызываем namespaceInformer, который был предварительно определён. Когда поступит сигнал остановки, он завершит функцию, сообщит WaitGroup, что больше не выполняется, и эта функция завершит свою работу.Информацию о сборке и запуске этого оператора в кластере Kubernetes можно найти в репозитории на GitHub.
На этом оператор, который создаёт
RoleBinding при появлении Namespace в кластере Kubernetes, готов.