Пишем оператор за 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, в raw)

Такъв RoleBinding може да се създаде и ръчно, но след достигнатa граница от сто пространства на имена, това става изтощително занимание. Тук влизат в действие операторите на 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, "Настройте този флаг, когато работите извън клъстера.")
  flag.Parse()
  // Създаваме clientset за взаимодействие с клъстера Kubernetes
  clientset, err := newClientSet(*runOutsideCluster)

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

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

  <-sigs // Изчакваме сигнали (докато не получим сигнал, не се случва нищо)
  log.Printf("Затваряне...")

  close(stop) // Казваме на goroutines да спрат
  wg.Wait()   // Изчакваме, докато всичко е спряно
}

(main.go, в raw)

Ние правим следното:

  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, в raw)

Тук настройваме SharedIndexInformer, който ефективно (използвайки кеш) ще очаква промени в пространствата от имена (повече за информерите може да прочетете в статията «Как наистина работи планировчикът на 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("Неуспешно създадено Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Създаден AD RoleBinding за пространство от имена: %s", roleBinding.Name))
  }
}

(controller.go, в raw)

Получаваме пространството от имена като 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, в raw)

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

Информация за компилирането и стартирането на този оператор в кластера Kubernetes може да намерите в репозитория на GitHub.

На този оператор, който създава RoleBinding появявайки се Namespace в кластера Kubernetes, е готов.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster