Napisanie operatora dla Kubernetes w Golang

Notatka. przeł.: Operatory to oprogramowanie pomocnicze dla Kubernetesa, zaprojektowane w celu automatyzacji wykonywania rutynowych akcji na obiektach klastra w przypadku wystąpienia określonych zdarzeń. O operatorach pisaliśmy już w ten artykuł, gdzie opowiadali o podstawowych ideach i zasadach swojej pracy. Gdyby jednak ten materiał był bardziej spojrzeniem od strony obsługi gotowych komponentów dla Kubernetesa, to tłumaczenie nowego proponowanego artykułu jest już wizją developera/inżyniera DevOps zdumionego wdrożeniem nowego operatora.

Napisanie operatora dla Kubernetes w Golang

Zdecydowałem się napisać ten post na przykładzie z życia wziętym, po tym jak próbowałem znaleźć dokumentację dotyczącą tworzenia operatora dla Kubernetesa, które przeszły przez przestudiowanie kodu.

Przykład, który zostanie opisany, jest następujący: w naszym klastrze Kubernetes, każdy Namespace reprezentuje środowisko piaskownicy zespołu i chcieliśmy ograniczyć do niego dostęp, aby zespoły mogły grać tylko w swoich własnych piaskownicach.

Możesz osiągnąć to, co chcesz, przypisując użytkownikowi grupę, która ma RoleBinding do konkretnego Namespace и ClusterRole z prawami redakcyjnymi. Reprezentacja YAML będzie wyglądać następująco:

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

(powiązanie roli.yamlw surowy)

Stworzyć jeden RoleBinding Można to zrobić ręcznie, ale po przekroczeniu granicy stu przestrzeni nazw staje się to żmudnym zadaniem. I tu z pomocą przychodzą operatorzy Kubernetesa – pozwalają zautomatyzować tworzenie zasobów Kubernetesa na podstawie zmian w zasobach. W naszym przypadku chcemy stworzyć RoleBinding podczas tworzenia Namespace.

Na początek zdefiniujmy funkcję mainktóry wykonuje wymaganą konfigurację, aby uruchomić instrukcję, a następnie wywołuje akcję instrukcji:

(Notatka. przeł.: tutaj i poniżej komentarze w kodzie są przetłumaczone na język rosyjski. Dodatkowo wcięcie zostało poprawione na spacje zamiast tabulatorów [zalecane w Go] wyłącznie w celu lepszej czytelności w obrębie układu Habr. Po każdym wpisie znajdują się linki do oryginału w GitHubie, gdzie przechowywane są anglojęzyczne komentarze i zakładki.)

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

(główne.gow surowy)

Wykonujemy następujące czynności:

  1. Konfigurujemy procedurę obsługi dla określonych sygnałów systemu operacyjnego, aby spowodować płynne zakończenie działania operatora.
  2. Używamy WaitGroupaby bezpiecznie zatrzymać wszystkie goroutines przed zakończeniem aplikacji.
  3. Dostęp do klastra zapewniamy poprzez utworzenie clientset.
  4. Biegnij NamespaceController, w którym będzie zlokalizowana cała nasza logika.

Teraz potrzebujemy podstawy dla logiki, a w naszym przypadku jest to ta, o której mowa 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
}

(kontroler.gow surowy)

Tutaj konfigurujemy SharedIndexInformer, który będzie skutecznie (przy użyciu pamięci podręcznej) czekać na zmiany w przestrzeniach nazw (więcej o informatorach w artykule „Jak właściwie działa harmonogram Kubernetes?"- około. tłumaczenie). Po tym się łączymy EventHandler do informatora, aby podczas dodawania przestrzeni nazw (Namespace) wywoływana jest funkcja createRoleBinding.

Następnym krokiem jest zdefiniowanie tej funkcji 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))
  }
}

(kontroler.gow surowy)

Otrzymujemy przestrzeń nazw jako obj i przekonwertuj go na obiekt Namespace. Następnie definiujemy RoleBinding, w oparciu o wspomniany na początku plik YAML, korzystając z dostarczonego obiektu Namespace i tworzenie RoleBinding. Na koniec rejestrujemy, czy utworzenie się powiodło.

Ostatnią zdefiniowaną funkcją jest 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
}

(kontroler.gow surowy)

Tutaj rozmawiamy WaitGroupże uruchamiamy goroutine, a następnie wywołujemy namespaceInformer, co zostało wcześniej zdefiniowane. Gdy nadejdzie sygnał stopu, funkcja zakończy działanie, poinformuj WaitGroup, który nie jest już wykonywany i funkcja ta zakończy się.

Informacje na temat budowania i uruchamiania tej instrukcji w klastrze Kubernetes można znaleźć w repozytoria na GitHubie.

To wszystko dla operatora, który tworzy RoleBinding Kiedy Namespace w klastrze Kubernetes, gotowe.

Źródło: www.habr.com

Dodaj komentarz