Note de traduction.: Les opérateurs (operators) sont des logiciels auxiliaires pour Kubernetes, conçus pour automatiser l'exécution d'actions répétitives sur les objets du cluster lors de certains événements. Nous avons déjà écrit sur les opérateurs dans , où nous avons présenté les idées fondamentales et les principes de leur fonctionnement. Mais si ce document était plutôt un aperçu de l'exploitation de composants prêts à l'emploi pour Kubernetes, la traduction de cet article nouvellement proposé est désormais la perspective d'un développeur ou d'un ingénieur DevOps, préoccupé par la mise en œuvre d'un nouvel opérateur.

J'ai décidé d'écrire ce post avec un exemple de la vie réelle après mes tentatives de trouver de la documentation sur la création d'un opérateur pour Kubernetes, passant par l'examen du code.
L'exemple que je vais décrire est le suivant : dans notre cluster Kubernetes, chaque Namespace représente un environnement de bac à sable pour une certaine équipe, et nous voulions restreindre l'accès afin que les équipes ne puissent jouer que dans leurs propres bacs à sable.
L'objectif souhaité peut être atteint en attribuant à l'utilisateur un groupe qui a RoleBinding pour ce qui est de Namespace et ClusterRole avec le droit d'édition. La représentation YAML ressemblera à ceci :
---
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(, sur )
On peut créer cela RoleBinding manuellement, mais après avoir dépassé la centaine d'espaces de noms, cela devient une tâche fastidieuse. C'est ici que les opérateurs Kubernetes aident — ils permettent d'automatiser la création de ressources Kubernetes en fonction des modifications apportées aux ressources. Dans notre cas, nous voulons créer RoleBinding lors de la création Namespace.
Tout d'abord, définissons une fonction main, qui exécute la configuration requise pour démarrer l'opérateur et appelle ensuite l'action de l'opérateur :
(Note de traduction.: ici et plus loin, les commentaires dans le code sont traduits en russe. De plus, les indentations ont été corrigées en espaces au lieu des [tabulations recommandées en Go] uniquement pour une meilleure lisibilité dans le cadre de la mise en page de Habr. Après chaque extrait, des liens vers l'original sur GitHub sont fournis, où les commentaires en anglais et les tabulations sont préservés.
func main() {
// Configure log output to console STDOUT
log.SetOutput(os.Stdout)
sigs := make(chan os.Signal, 1) // Create a channel for receiving OS signals
stop := make(chan struct{}) // Create a channel for receiving a stop signal
// Register for receiving SIGTERM in the sigs channel
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
// Goroutines can add themselves to the WaitGroup,
// to wait for their completion
wg := &sync.WaitGroup{}
runOutsideCluster := flag.Bool("run-outside-cluster", false, "Set this flag when running outside of the cluster.")
flag.Parse()
// Create a clientset for interacting with the Kubernetes cluster
clientset, err := newClientSet(*runOutsideCluster)
if err != nil {
panic(err.Error())
}
controller.NewNamespaceController(clientset).Run(stop, wg)
<-sigs // Wait for signals (nothing happens until a signal is received)
log.Printf("Shutting down...")
close(stop) // Instruct goroutines to stop
wg.Wait() // Wait for everything to be stopped
}(, sur )
Nous faisons ce qui suit :
- Configurer les gestionnaires pour des signaux spécifiques du système d'exploitation afin de déclencher une fermeture correcte (gracieuse) de l'opérateur.
- Utiliser
WaitGroup, afin d'arrêter correctement tous les goroutines avant de terminer l'application. - Fournir l'accès au cluster par la création de
clientset. - Lançons
NamespaceController, où toute notre logique sera située.
Maintenant, nous avons besoin d'une base pour la logique, et dans notre cas, c'est le mentionné. 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
}(, sur )
Ici, nous configurons SharedIndexInformer, qui attendra efficacement (en utilisant un cache) les modifications dans les espaces de noms. (pour en savoir plus sur les informers, lisez l'article «» — Note de traduction.). Ensuite, nous connectons EventHandler à l'informer, ce qui déclenche la fonction lors de l'ajout d'un espace de noms (Namespace). createRoleBinding.
L'étape suivante consiste à définir cette fonction 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("Échec de la création de Role Binding : %s", err.Error()))
} else {
log.Println(fmt.Sprintf("RoleBinding AD créé pour l'espace de noms : %s", roleBinding.Name))
}
}(, sur )
Nous obtenons l'espace de noms comme obj et le transformons en objet Namespace. Ensuite, nous définissons RoleBinding, basé sur ce qui a été mentionné dans le fichier YAML au début, en utilisant l'objet fourni Namespace et en créant RoleBinding. Enfin, nous enregistrons si la création a réussi.
La dernière fonction à définir est Exécution:
// 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
}(, sur )
Ici, nous disons WaitGroup, que nous démarrerons une goroutine et ensuite nous appelons namespaceInformer, qui a été préalablement défini. Lorsqu'un signal d'arrêt est reçu, il terminera la fonction, indiquera WaitGroup, qu'il n'est plus en cours d'exécution, et que cette fonction se terminera.
Des informations sur la construction et le déploiement de cet opérateur dans le cluster Kubernetes peuvent être trouvées dans .
À ce stade, l'opérateur qui crée RoleBinding lorsqu'il apparaît Namespace dans le cluster Kubernetes, est prêt.
Source : habr.com
