Nota di traduzione.: Gli operatori (operators) sono software ausiliari per Kubernetes, progettati per automatizzare l'esecuzione di azioni routine sugli oggetti del cluster in occasione di determinati eventi. Abbiamo già scritto sugli operatori in , dove abbiamo parlato delle idee fondamentali e dei principi del loro funzionamento. Ma se quel materiale era più che altro un punto di vista sull'uso dei componenti già pronti per Kubernetes, la traduzione di questo nuovo articolo rappresenta la visione di uno sviluppatore/DevOps engineer impegnato nella realizzazione di un nuovo operatore.

Ho deciso di scrivere questo post con un esempio della vita reale dopo aver tentato di trovare documentazione sulla creazione di un operatore per Kubernetes, che è passata attraverso lo studio del codice.
L'esempio che verrà descritto è il seguente: nel nostro cluster Kubernetes ogni Namespace rappresenta un ambiente sandbox di un certo team, e volevamo limitare l'accesso a essi in modo che i team potessero giocare solo nelle loro sandbox.
Per raggiungere il risultato desiderato, si può assegnare all'utente un gruppo che ha una RoleBinding a specifici Namespace e ClusterRole con diritto di modifica. La rappresentazione YAML sarà 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(, in )
Un tale RoleBinding può essere creato anche manualmente, ma dopo aver oltrepassato la soglia di cento spazi dei nomi, diventa un compito stancante. Qui entrano in aiuto gli operatori Kubernetes: consentono di automatizzare la creazione delle risorse Kubernetes, basandosi sulle modifiche alle risorse. Nel nostro caso vogliamo creare RoleBinding al momento della creazione Namespace.
Per prima cosa definiamo la funzione main, che esegue la configurazione necessaria per avviare l'operatore e poi chiama l'azione dell'operatore:
(Nota di traduzione.: qui e oltre, i commenti nel codice sono tradotti in italiano. Inoltre, le rientranze sono state corrette in spazi invece di [tabulazioni raccomandate in Go] esclusivamente con lo scopo di migliorare la leggibilità nell'ambito del layout di Habr. Dopo ogni listing ci sono i link all'originale su GitHub, dove sono conservati i commenti in lingua inglese e le tabulazioni.)
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 i segnali del sistema operativo
stop := make(chan struct{}) // Creiamo un canale per ricevere il segnale di arresto
// Registriamo per ricevere il SIGTERM nel canale sigs
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
// Le goroutine possono aggiungere se stesse al WaitGroup,
// per attendere il completamento della loro esecuzione
wg := &sync.WaitGroup{}
runOutsideCluster := flag.Bool("run-outside-cluster", false, "Imposta questo flag quando esegui al di fuori del cluster.")
flag.Parse()
// Creiamo 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 i segnali (fino a quando non riceviamo un segnale, non succede nulla)
log.Printf("Spegnimento in corso...")
close(stop) // Diciamo alle goroutine di fermarsi
wg.Wait() // Aspettiamo che tutto sia fermo
}(, in )
Facciamo quanto segue:
- Configuriamo i gestori per segnali specifici del sistema operativo, per invocare una chiusura corretta (graceful) dell'operatore.
- Usa
WaitGroup, per fermare correttamente tutte le goroutine prima della chiusura dell'applicazione. - Fornendo accesso al cluster creando
clientset. - Avviamo
NamespaceController, in cui risiederà tutta la nostra logica.
Ora ci serve la base per la logica, ed in nostro caso è 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
}(, in )
Qui configuriamo SharedIndexInformer, che attenderà in modo efficiente (utilizzando la cache) le modifiche negli spazi dei nomi (per saperne di più sugli informers, leggi l'articolo "» — Nota di traduzione.). Dopodiché colleghiamo EventHandler all'informer, così che quando viene aggiunto uno spazio dei nomi (Namespace) venga invocata 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("Impossibile creare il Role Binding: %s", err.Error()))
} else {
log.Println(fmt.Sprintf("Creato AD RoleBinding per Namespace: %s", roleBinding.Name))
}
}(, in )
Riceviamo lo spazio dei nomi come obj e lo trasformiamo 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 è andata a buon fine.
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
}(, in )
Qui diciamo WaitGroup, che avvieremo una goroutine e poi chiamiamo namespaceInformer, che è stata precedentemente definita. Quando arriva un segnale di arresto, terminerà la funzione, comunicando WaitGroup, che non è più in esecuzione, e questa funzione concluderà il suo lavoro.
Le informazioni sulla build e l'esecuzione di questo operatore nel cluster Kubernetes possono essere trovate in .
In questo modo, l'operatore che crea RoleBinding alla comparsa di Namespace nel cluster Kubernetes è pronto.
Fonte: habr.com
