Nota de traducción.: Los operadores son software auxiliar para Kubernetes, destinado a automatizar la ejecución de acciones rutinarias sobre los objetos del clúster ante ciertos eventos. Ya hemos escrito sobre los operadores en , donde hablamos sobre las ideas fundamentales y los principios de su funcionamiento. Pero mientras ese material representaba más bien una mirada desde el lado de la explotación de componentes listos para Kubernetes, esta traducción de un nuevo artículo representa la visión de un desarrollador/ingeniero DevOps, preocupado por la implementación de un nuevo operador.

Decidí escribir este post con un ejemplo de la vida real después de mis intentos de encontrar documentación sobre cómo crear un operador para Kubernetes, que pasaron por el estudio del código.
El ejemplo que se describirá es el siguiente: en nuestro clúster de Kubernetes, cada Namespace representa un entorno de prueba de algún equipo, y queríamos limitar el acceso a ellos de manera que los equipos pudieran jugar solo en sus entornos de prueba.
Para lograr lo deseado, se puede asignar al usuario un grupo que tenga RoleBinding a un Namespace y ClusterRole con permiso de edición. La representación en YAML sería así:
---
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(, en )
Este tipo de RoleBinding también se puede crear manualmente, pero después de superar la marca de cien espacios de nombres, se convierte en una tarea tediosa. Aquí es donde los operadores de Kubernetes ayudan: permiten automatizar la creación de recursos de Kubernetes, basándose en cambios en los recursos. En nuestro caso queremos crear RoleBinding al crear Namespace.
Primero, definiremos la función main, que realiza la configuración necesaria para iniciar el operador y luego llama a la acción del operador:
(Nota de traducción.: aquí y en adelante, los comentarios en el código han sido traducidos al español. Además, los espacios se corrigieron para que fueran espacios en lugar de [tabulaciones recomendadas en Go] exclusivamente con el fin de mejorar la legibilidad en el marco del diseño de Habra. Después de cada listado, se incluyen enlaces a la versión original en GitHub, donde se conservan los comentarios en inglés y las tabulaciones.
func main() {
// Establecemos la salida de logs en STDOUT de la consola
log.SetOutput(os.Stdout)
sigs := make(chan os.Signal, 1) // Creamos un canal para recibir señales del sistema operativo
stop := make(chan struct{}) // Creamos un canal para recibir la señal de parada
// Registramos la recepción de SIGTERM en el canal sigs
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
// Las goroutines pueden añadirse a sí mismas en WaitGroup,
// para que esperemos a que terminen su ejecución
wg := &sync.WaitGroup{}
runOutsideCluster := flag.Bool("run-outside-cluster", false, "Establezca este flag cuando esté trabajando fuera del clúster.")
flag.Parse()
// Creamos clientset para interactuar con el clúster de Kubernetes
clientset, err := newClientSet(*runOutsideCluster)
if err != nil {
panic(err.Error())
}
controller.NewNamespaceController(clientset).Run(stop, wg)
<-sigs // Esperamos señales (no ocurrirá nada más hasta recibir una señal)
log.Printf("Apagando...")
close(stop) // Decimos a las goroutines que se detengan
wg.Wait() // Esperamos a que todas se detengan
}(, en )
Hacemos lo siguiente:
- Configuramos un manejador para señales específicas del sistema operativo, para invocar un cierre correcto (graceful) del operador.
- Usamos
WaitGroup, para detener correctamente todas las goroutines antes de cerrar la aplicación. - Proporcionamos acceso al clúster creando
clientset. - Iniciamos
NamespaceController, donde se albergará toda nuestra lógica.
Ahora necesitamos la base para la lógica, y en nuestro caso esto es el mencionado 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
}(, en )
Aquí configuramos SharedIndexInformer, que esperará de manera eficiente (usando caché) los cambios en los espacios de nombres (para más información sobre informers, lea el artículo "» — nota del traductor.). Después de esto, conectamos EventHandler al informador, por lo que cuando se añade un espacio de nombres (Namespace) se invoca la función createRoleBinding.
El siguiente paso es definir esta función 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("Error al crear Role Binding: %s", err.Error()))
} else {
log.Println(fmt.Sprintf("RoleBinding AD creado para el espacio de nombres: %s", roleBinding.Name))
}
}(, en )
Obtenemos el espacio de nombres como obj y lo convertimos en un objeto Namespace. Luego definimos RoleBinding, basándonos en lo mencionado al principio del archivo YAML, utilizando el objeto proporcionado Namespace y creando RoleBinding. Finalmente, registramos si la creación fue exitosa.
La última función que necesitamos definir es Ejecutar:
// 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
}(, en )
Aquí decimos WaitGroup, que iniciaremos una goroutine y luego llamamos a namespaceInformer, que fue previamente definido. Cuando llegue la señal de detención, finalizará la función, informará WaitGroup, que ya no se está ejecutando, y esta función finalizará su trabajo.
La información sobre la construcción y ejecución de este operador en el clúster de Kubernetes se puede encontrar en .
En este punto, el operador que crea RoleBinding al aparecer Namespace en el clúster de Kubernetes, está listo.
Fuente: habr.com
