Escribindo un operador para Kubernetes en Golang

Nota. transl.: Os operadores son software auxiliar para Kubernetes, deseñados para automatizar a execución de accións rutineiras en obxectos do clúster cando ocorren certos eventos. Xa escribimos sobre os operadores en Este artigo, onde falaron das ideas e principios fundamentais do seu traballo. Pero se ese material era máis ben visto dende o lado da operación de compoñentes preparados para Kubernetes, entón a tradución do novo artigo que agora se propón xa é a visión dun desenvolvedor/enxeñeiro de DevOps desconcertado pola implementación dun novo operador.

Escribindo un operador para Kubernetes en Golang

Decidín escribir esta publicación cun exemplo da vida real despois dos meus intentos de atopar documentación sobre a creación dun operador para Kubernetes, que pasou por estudar o código.

O exemplo que se describirá é este: no noso clúster de Kubernetes, cada un Namespace representa o entorno sandbox dun equipo e queriamos limitar o acceso a eles para que os equipos só puidesen xogar nos seus propios sandbox.

Podes conseguir o que queiras asignando a un usuario un grupo que teña RoleBinding a específico Namespace и ClusterRole con dereitos de edición. A representación de YAML terá o seguinte aspecto:

---
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

(vinculación de roles.yamlEn cru)

Crea un RoleBinding Podes facelo manualmente, pero despois de cruzar a marca de cen espazos de nomes, convértese nunha tarefa tediosa. Aquí é onde os operadores de Kubernetes resultan útiles: permítenche automatizar a creación de recursos de Kubernetes en función dos cambios nos recursos. No noso caso queremos crear RoleBinding ao crear Namespace.

En primeiro lugar, imos definir a función mainque fai a configuración necesaria para executar a instrución e logo chama a acción da instrución:

(Nota. transl.: aquí e abaixo os comentarios do código están traducidos ao ruso. Ademais, a sangría foi corrixida a espazos en lugar das pestanas [recomendado en Ir] só co propósito de mellorar a lexibilidade no deseño Habr. Despois de cada listaxe hai ligazóns ao orixinal en GitHub, onde se almacenan comentarios e pestanas en inglés.)

func main() {
  // Устанавливаем вывод логов в консольный STDOUT
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Создаем канал для получения сигналов ОС
  stop := make(chan struct{})     // Создаем канал для получения стоп-сигнала

  // Регистрируем получение SIGTERM в канале sigs
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Goroutines могут сами добавлять себя в WaitGroup,
 // чтобы завершения их выполнения дожидались
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Set this flag when running outside of the cluster.")
  flag.Parse()
  // Создаем clientset для взаимодействия с кластером Kubernetes
  clientset, err := newClientSet(*runOutsideCluster)

  if err != nil {
    panic(err.Error())
  }

  controller.NewNamespaceController(clientset).Run(stop, wg)

  <-sigs // Ждем сигналов (до получения сигнала более ничего не происходит)
  log.Printf("Shutting down...")

  close(stop) // Говорим goroutines остановиться
  wg.Wait()   // Ожидаем, что все остановлено
}

(principal.goEn cru)

Facemos o seguinte:

  1. Configuramos un controlador para sinais específicos do sistema operativo para provocar a terminación graciosa do operador.
  2. Usamos WaitGrouppara deter con gracia todas as goroutines antes de finalizar a aplicación.
  3. Ofrecemos acceso ao clúster mediante a creación clientset.
  4. Lanzamento NamespaceController, no que se situará toda a nosa lóxica.

Agora necesitamos unha base para a lóxica, e no noso caso esta é a mencionada 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
}

(controlador.goEn cru)

Aquí configuramos SharedIndexInformer, que efectivamente (usando unha caché) agardará por cambios nos espazos de nomes (ler máis sobre informantes no artigo "Como funciona o programador de Kubernetes?"- aprox. tradución). Despois disto conectamos EventHandler ao informante, de xeito que ao engadir un espazo de nomes (Namespace) chámase función createRoleBinding.

O seguinte paso é 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("Failed to create Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Created AD RoleBinding for Namespace: %s", roleBinding.Name))
  }
}

(controlador.goEn cru)

Obtemos o espazo de nomes como obj e convertelo nun obxecto Namespace. Despois definimos RoleBinding, baseado no ficheiro YAML mencionado ao principio, utilizando o obxecto proporcionado Namespace e creando RoleBinding. Finalmente, rexistramos se a creación foi exitosa.

A última función a definir é 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
}

(controlador.goEn cru)

Aquí estamos a falar WaitGroupque lancemos a goroutine e despois chamemos namespaceInformer, que foi previamente definido. Cando chegue o sinal de parada, finalizará a función, informa WaitGroup, que xa non se executa, e esta función sairá.

Pódese atopar información sobre a creación e execución desta declaración nun clúster de Kubernetes en repositorios en GitHub.

Iso é para o operador que crea RoleBinding cando Namespace no clúster de Kubernetes, listo.

Fonte: www.habr.com

Engadir un comentario