Прим. прев.: Операторите (operators) са спомагателен софтуер за Kubernetes, предназначен да автоматизира изпълнението на рутинни действия върху обектите на клъстера при определени събития. Вече написахме за операторите в , където разказахме за основополагаещите идеи и принципи на тяхната работа. Но ако този материал беше по-скоро поглед отвъд към експлоатацията на готови компоненти за Kubernetes, то предлаганият сега превод на нова статия е вече виждане на разработчика/DevOps-инженера, ангажиран с реализацията на нов оператор.

Този пост с пример от реалния живот реших да напиша след моите опити да намеря документация за създаването на оператор за 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 може да се създаде и ръчно, но след достигнатa граница от сто пространства на имена, това става изтощително занимание. Тук влизат в действие операторите на 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, "Настройте този флаг, когато работите извън клъстера.")
flag.Parse()
// Създаваме clientset за взаимодействие с клъстера Kubernetes
clientset, err := newClientSet(*runOutsideCluster)
if err != nil {
panic(err.Error())
}
controller.NewNamespaceController(clientset).Run(stop, wg)
<-sigs // Изчакваме сигнали (докато не получим сигнал, не се случва нищо)
log.Printf("Затваряне...")
close(stop) // Казваме на goroutines да спрат
wg.Wait() // Изчакваме, докато всичко е спряно
}(, в )
Ние правим следното:
- Настройваме обработчици на конкретни сигнали от операционната система, за да предизвикаме коректно (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
}(, в )
Тук настройваме SharedIndexInformer, който ефективно (използвайки кеш) ще очаква промени в пространствата от имена (повече за информерите може да прочетете в статията «» — бел. прев.). След това свързваме 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("Неуспешно създадено Role Binding: %s", err.Error()))
} else {
log.Println(fmt.Sprintf("Създаден AD RoleBinding за пространство от имена: %s", roleBinding.Name))
}
}(, в )
Получаваме пространството от имена като 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
}(, в )
Тук казваме WaitGroup, че ще стартираме goroutine и след това извикваме namespaceInformer, който е предварително определен. Когато получим сигнал за спиране, той ще завърши функцията, ще съобщи WaitGroup, че вече не се изпълнява, и тази функция ще приключи.
Информация за компилирането и стартирането на този оператор в кластера Kubernetes може да намерите в .
На този оператор, който създава RoleBinding появявайки се Namespace в кластера Kubernetes, е готов.
Източник: habr.com
