Nota del traduttore.: Gli operatori — sono software ausiliari per Kubernetes, progettati per automatizzare l'esecuzione di azioni routinarie sugli oggetti del cluster in determinate circostanze. Abbiamo già scritto sugli operatori in , dove abbiamo trattato le idee e i principi fondamentali del loro funzionamento. Tuttavia, se quel materiale rappresentava più una prospettiva sull'uso di componenti già pronti per Kubernetes, la traduzione di questo nuovo articolo offre invece la visione di uno sviluppatore/ingegnere DevOps impegnato nella realizzazione di un nuovo operatore.

Ho deciso di scrivere questo post con un esempio della vita reale dopo i miei tentativi di trovare documentazione sulla creazione di un operatore per Kubernetes, che sono passati attraverso lo studio del codice.
L'esempio che sarà descritto è il seguente: nel nostro cluster Kubernetes ogni Namespace rappresenta un ambiente sandbox di un team, e volevamo limitare l'accesso in modo che i team potessero giocare solo nei propri sandbox.
Questo obiettivo può essere raggiunto assegnando all'utente un gruppo che ha RoleBinding a un Namespace e ClusterRole con diritto di modifica. La rappresentazione YAML apparirà così:
---
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(, nel )
Creare questo RoleBinding è possibile anche manualmente, ma dopo aver superato la soglia di cento spazi dei nomi, diventa un compito noioso. Qui entra in gioco l'operatore Kubernetes: consente di automatizzare la creazione delle risorse Kubernetes in base alle modifiche delle risorse. Nel nostro caso, vogliamo creare RoleBinding durante la creazione Namespace.
iniziamo definendo la funzione main, che esegue la configurazione necessaria per avviare l'operatore e quindi richiama l'azione dell'operatore:
(Nota del traduttore.: da qui in poi i commenti nel codice sono tradotti in italiano. Inoltre, gli spazi sono stati corretti in spazi al posto delle [tabs raccomandate in Go] esclusivamente per una maggiore leggibilità nel layout di Habra. Dopo ogni elenco, sono forniti i collegamenti all'originale su GitHub, dove sono mantenuti i commenti in lingua inglese e i tab.
func main() {
// Impostiamo l'output dei log su STDOUT della console
log.SetOutput(os.Stdout)
sigs := make(chan os.Signal, 1) // Creiamo un canale per ricevere segnali OS
stop := make(chan struct{}) // Creiamo un canale per ricevere il segnale di arresto
// Registriamo la ricezione di SIGTERM nel canale sigs
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
// Le Goroutines possono aggiungersi da sole al WaitGroup,
// in modo che la loro terminazione venga attesa
wg := &sync.WaitGroup{}
runOutsideCluster := flag.Bool("run-outside-cluster", false, "Impostare questo flag quando si esegue al di fuori del cluster.")
flag.Parse()
// Creiamo un clientset per interagire con il cluster Kubernetes
clientset, err := newClientSet(*runOutsideCluster)
if err != nil {
panic(err.Error())
}
controller.NewNamespaceController(clientset).Run(stop, wg)
<-sigs // Aspettiamo segnali (fino a ricevere un segnale non succede nulla)
log.Printf("Interruzione in corso...")
close(stop) // Diamo il segnale alle goroutines di fermarsi
wg.Wait() // Attendiamo che tutto sia fermo
}(, nel )
Facciamo quanto segue:
- Configuriamo un gestore per segnali specifici del sistema operativo per avviare una chiusura corretta (graceful) dell'operatore.
- Utilizziamo
WaitGroup, per fermare correttamente tutte le goroutines prima della chiusura dell'applicazione. - Forniamo accesso al cluster creando un
clientset. - Avviamo
NamespaceController, in cui sarà situata tutta la nostra logica.
Ora ci serve una base per la logica, e nel nostro caso questo è il menzionato 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
}(, nel )
Qui configuriamo SharedIndexInformer, che attenderà efficacemente (utilizzando la cache) le modifiche nei namespace (per saperne di più sugli informers, leggi l'articolo «» — nota del traduttore.). Dopodiché, colleghiamo EventHandler all'informer, grazie al quale, quando viene aggiunto un namespace (Namespace), viene chiamata la funzione createRoleBinding.
Il passo successivo è definire questa funzione 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))
}
}(, nel )
Otteniamo il namespace come obj e lo convertiamo in un oggetto Namespace. Poi definiamo RoleBinding, basandoci su quanto menzionato all'inizio nel file YAML, utilizzando l'oggetto fornito Namespace e creando RoleBinding. Infine, registriamo se la creazione è avvenuta con successo.
L'ultima funzione da definire è Esegui:
// 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
}(, nel )
Qui diciamo WaitGroup, che avvieremo una goroutine e poi chiamiamo namespaceInformer, che è stato precedentemente definito. Quando arriva il segnale di arresto, concluderà la funzione, comunicherà WaitGroup, che non è più in esecuzione, e questa funzione terminerà il suo lavoro.
Informazioni sulla costruzione e l'esecuzione di questo operatore nel cluster Kubernetes possono essere trovate in .
A questo punto, l'operatore che crea RoleBinding all'arrivo Namespace nel cluster Kubernetes è pronto.
Fonte: habr.com
