Пишемо оператора для Kubernetes на Golang

Прим. перев.: Оператори (operators) - це допоміжне програмне забезпечення для Kubernetes, покликане автоматизувати виконання рутинних дій над об'єктами кластера при певних подіях. Ми вже писали про операторів у цієї статті, де розповідали про основні ідеї та принципи їх роботи. Але якщо цей матеріал був скоріше поглядом з боку експлуатації готових компонентів для Kubernetes, то пропонований тепер переклад нової статті — це вже бачення розробника/DevOps-інженера, спантеличеного реалізацією нового оператора.

Пишемо оператора для Kubernetes на Golang

Цей пост із прикладом із реального життя я вирішив написати після своїх спроб знайти документацію щодо створення оператора для Kubernetes, які пройшли через вивчення коду.

Приклад, який буде описаний, такий: у нашому кластері Kubernetes кожен Namespace представляє оточення-пісочницю якоїсь команди, і ми хотіли обмежити доступ до них так, щоб команди могли грати лише у своїх пісочницях.

Досягти бажаного можна призначенням користувача групи, яка має RoleBinding до конкретних Namespace и ClusterRole із правом на редагування. YAML-подання виглядатиме так:

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

(rolebinding.yaml, в сировина)

Створювати такий RoleBinding можна і вручну, але після подолання позначки у сотню просторів імен це стає стомлюючим заняттям. Саме тут допомагають оператори Kubernetes — вони дозволяють автоматизувати створення ресурсів Kubernetes, ґрунтуючись на змінах ресурсів. У нашому випадку ми хочемо створювати RoleBinding при створенні Namespace.

Насамперед визначимо функцію main, яка виконує необхідне налаштування для запуску оператора, а потім викликає дію оператора:

(Прим. перев.: тут і далі коментарі в коді перекладені російською мовою Крім того, відступи виправлені на прогалини замість [рекомендованих у Go] табів виключно з метою кращої читання в рамках верстки Хабра. Після кожного лістингу наведено посилання на оригінал на GitHub, де збережені англомовні коментарі та таби.)

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()   // Ожидаем, что все остановлено
}

(main.go, в сировина)

Ми робимо таке:

  1. Налаштовуємо обробника конкретних сигналів операційної системи, щоб викликати коректне завершення роботи оператора.
  2. використовуємо WaitGroup, щоб коректно зупинити всі goroutines перед завершенням роботи програми.
  3. Надаємо доступ до кластера створенням clientset.
  4. запускаємо NamespaceController, В якому буде розташована вся наша логіка.

Тепер потрібна основа для логіки і в нашому випадку це згаданий 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
}

(controller.go, в сировина)

Тут ми налаштовуємо SharedIndexInformer, який буде ефективно (використовуючи кеш) очікувати змін у просторах імен (детальніше про informers читайте у статті «Як насправді працює планувальник Kubernetes?- прим. перев.). Після цього ми підключаємо EventHandler до інформера, завдяки чому при додаванні простору імен (Namespace) викликається функція createRoleBinding.

Наступний крок – визначити цю функцію 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))
  }
}

(controller.go, в сировина)

Ми отримуємо простір імен як obj і перетворимо його на об'єкт Namespace. Потім визначаємо RoleBinding, ґрунтуючись на згаданому на початку YAML-файлі, використовуючи наданий об'єкт Namespace та створюючи RoleBinding. Зрештою, логуємо, чи успішно пройшло створення.

Остання функція, яку необхідно визначити, 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
}

(controller.go, в сировина)

Тут ми говоримо WaitGroup, що запустимо goroutine і потім викликаємо namespaceInformer, який було попередньо визначено. Коли надійде сигнал зупинки, він завершить функцію. WaitGroupщо більше не виконується, і ця функція завершить свою роботу.

Інформацію про складання та запуск цього оператора в кластері Kubernetes можна знайти в репозиторії на GitHub.

На цьому оператор, який створює RoleBinding при появі Namespace у кластері Kubernetes, готовий.

Джерело: habr.com

Додати коментар або відгук