Пішам аператара для 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. Наладжваем апрацоўшчыка пэўных сігналаў аперацыйнай сістэмы, каб выклікаць карэктнае (graceful) завяршэнне працы аператара.
  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

Дадаць каментар