Przyp. tłum.: Operatorzy (operators) to pomocnicze oprogramowanie dla Kubernetes, mające na celu automatyzację wykonywania rutynowych działań na obiektach klastra w określonych zdarzeniach. Już pisaliśmy o operatorach w , gdzie opowiadaliśmy o podstawowych ideach i zasadach ich działania. Jeśli tamten materiał był raczej spojrzeniem z perspektywy eksploatacji gotowych komponentów dla Kubernetes, to obecnie proponowany przekład nowego artykułu to już wizja dewelopera/DevOps-inżyniera, zmartwionego realizacją nowego operatora.

Ten post z przykładem z życia wziętym postanowiłem napisać po własnych próbach znalezienia dokumentacji dotyczącej tworzenia operatora dla Kubernetes, które przeszły przez badanie kodu.
Przykład, który będzie opisany, jest następujący: w naszym klastrze Kubernetes każdy Namespace reprezentuje środowisko-piaskownicę jakiejś drużyny, i chcieliśmy ograniczyć dostęp do nich tak, aby drużyny mogły bawić się tylko w swoich piaskownicach.
Osiągnięcie tego celu można zrealizować, przypisując użytkownikowi grupę, która ma RoleBinding do konkretnych Namespace i ClusterRole z prawem do edytowania. Reprezentacja YAML będzie wyglądać tak:
---
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(, w )
Taką RoleBinding można stworzyć i ręcznie, ale po przekroczeniu liczby stu przestrzeni nazw staje się to żmudnym zajęciem. Właśnie tutaj pomagają operatorzy Kubernetes — pozwalają zautomatyzować tworzenie zasobów Kubernetes w oparciu o zmiany w zasobach. W naszym przypadku chcemy tworzyć RoleBinding przy tworzeniu Namespace.
Na początku zdefiniujmy funkcję main, która wykonuje wymaganą konfigurację do uruchomienia operatora, a następnie wywołuje działanie operatora:
(Przyp. tłum.: tutaj i dalej komentarze w kodzie zostały przetłumaczone na język polski. Ponadto wcięcia zostały poprawione na spacje zamiast [zalecanych w Go] tabulatorów, wyłącznie w celu lepszej czytelności w ramach układu Habra. Po każdym bloku kodu podane są linki do oryginału na GitHub, gdzie zachowane są anglojęzyczne komentarze i tabulatory.)
func main() {
// Ustawiamy wyjście logów na konsolowy STDOUT
log.SetOutput(os.Stdout)
sigs := make(chan os.Signal, 1) // Tworzymy kanał do odbierania sygnałów OS
stop := make(chan struct{}) // Tworzymy kanał do odbierania sygnału stopu
// Rejestrujemy odbieranie SIGTERM w kanale sigs
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
// Goroutines mogą same dodawać się do WaitGroup,
// aby zakończenie ich wykonania było oczekiwana
wg := &sync.WaitGroup{}
runOutsideCluster := flag.Bool("run-outside-cluster", false, "Ustaw tę flagę, gdy uruchamiasz poza klastrem.")
flag.Parse()
// Tworzymy clientset do interakcji z klastrem Kubernetes
clientset, err := newClientSet(*runOutsideCluster)
if err != nil {
panic(err.Error())
}
controller.NewNamespaceController(clientset).Run(stop, wg)
<-sigs // Czekamy na sygnały (do otrzymania sygnału nic więcej się nie dzieje)
log.Printf("Zamykamy...")
close(stop) // Mówimy goroutines, aby się zatrzymały
wg.Wait() // Czekamy, aż wszystko zostanie zatrzymane
}(, w )
Robimy to:
- Konfigurujemy obsługę konkretnych sygnałów systemu operacyjnego, aby prawidłowo zakończyć działanie operatora.
- Używamy
WaitGroup, aby prawidłowo zatrzymać wszystkie goroutines przed zakończeniem działania aplikacji. - Zapewniamy dostęp do klastra poprzez stworzenie
clientset. - Uruchamiamy
NamespaceController, w którym mieści się cała nasza logika.
Teraz potrzebujemy podstawy dla logiki, a w naszym przypadku jest to wspomniany 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
}(, w )
Tutaj konfigurujemy SharedIndexInformer, który efektywnie (wykorzystując cache) będzie oczekiwał na zmiany w przestrzeniach nazw (szczegóły na temat informersów można znaleźć w artykule „» — przyp. tłum.). Następnie podłączamy EventHandler do informera, dzięki czemu podczas dodawania przestrzeni nazw (Namespace) wywoływana jest funkcja createRoleBinding.
Następnym krokiem jest zdefiniowanie tej funkcji 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("Nie udało się utworzyć Role Binding: %s", err.Error()))
} else {
log.Println(fmt.Sprintf("Utworzono AD RoleBinding dla Namespace: %s", roleBinding.Name))
}
}(, w )
Otrzymujemy przestrzeń nazw jako ‘ zdefiniowana jest metoda i przekształcamy go w obiekt Namespace. Następnie definiujemy RoleBinding, opierając się na wspomnianym na początku pliku YAML, używając dostarczonego obiektu Namespace i tworząc RoleBinding. Na koniec logujemy, czy utworzenie przebiegło pomyślnie.
Ostatnia funkcja, którą należy zdefiniować, to 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
}(, w )
Tutaj mówimy WaitGroup, że uruchomimy goroutine, a następnie wywołujemy namespaceInformer, który został wcześniej zdefiniowany. Kiedy nadejdzie sygnał zatrzymania, zakończy funkcję, informując WaitGroup, że już nie jest wykonywana, i ta funkcja zakończy swoje działanie.
Informacje o budowaniu i uruchamianiu tego operatora w klastrze Kubernetes można znaleźć w .
Na tym operator, który tworzy RoleBinding w momencie pojawienia się Namespace w klastrze Kubernetes, jest gotowy.
Źródło: habr.com
