Píšeme operátora pre Kubernetes v Golang

Pozn. prekl.: Operátori (operators) sú pomocný softvér pre Kubernetes, ktorý má za cieľ automatizovať vykonávanie rutinných úkonov nad objektmi klastru pri určitých udalostiach. Už sme písali o operadoroch v tomto článku, kde sme sa venovali základným myšlienkam a princípom ich fungovania. Ale zatiaľ čo ten materiál bol skôr pohľadom z hľadiska prevádzky hotových komponentov pre Kubernetes, tak tento preklad novej článku je už pohľadom vývojára/DevOps inžiniera, ktorého zaujíma implementácia nového operátora.

Píšeme operátora pre Kubernetes v Golang

Tento príspevok s príkladom z reálneho života som sa rozhodol napísať po svojich pokusoch nájsť dokumentáciu na vytvorenie operátora pre Kubernetes, ktoré prešli cez štúdium kódu.

Príklad, ktorý bude popísaný, je nasledujúci: v našom klastri Kubernetes každý Namespace predstavuje sandboxové prostredie nejakého tímu, a my sme chceli obmedziť prístup k nim tak, aby tímy mohli hrať len vo svojich pieskoviskách.

Dosiahnutie želaného je možné priradením užívateľovi skupiny, ktorá má RoleBinding k konkrétnym Namespace a ClusterRole s právom na úpravy. YAML predstavenie bude vyzerať takto:

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

Vytvoriť takýto RoleBinding môžete aj manuálne, ale po prekročení hranice sto priestorov názvov sa to stáva únavnou činnosťou. Práve tu pomáhajú operátori Kubernetes — umožňujú automatizovať vytváranie zdrojov Kubernetes na základe zmien v zdrojoch. V našom prípade chceme vytvárať RoleBinding pri vytváraní Namespace.

Najprv určíme funkciu main, ktorá vykonáva požadované nastavenie pre spustenie operátora a následne vyvoláva akciu operátora:

(Pozn. prekl.: tu a ďalej sú komentáre v kóde preložené do slovenčiny. Okrem toho, odsadenia sú opravené na medzery namiesto [odporúčaných v Go] tabulátorov výlučne za účelom lepšej čitateľnosti v rámci rozloženia Habra. Po každom výpise sú uvedené odkazy na originál na GitHub, kde sú zachované anglické komentáre a tabulátory.)

func main() {
  // Nastavíme výstup logov do konzolového STDOUT
  log.SetOutput(os.Stdout)

  sigs := make(chan os.Signal, 1) // Vytvoríme kanál na prijímanie signálov OS
  stop := make(chan struct{})     // Vytvoríme kanál na prijímanie signálu na zastavenie

  // Registrujeme prijímanie SIGTERM do kanála sigs
  signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) 

  // Gorutiny sa môžu samy pridávať do WaitGroup,
  // aby čakala na dokončenie ich vykonania
  wg := &sync.WaitGroup{} 

  runOutsideCluster := flag.Bool("run-outside-cluster", false, "Nastavte toto flag pri spúšťaní mimo klastra.")
  flag.Parse()
  // Vytvoríme clientset na interakciu s klastrom Kubernetes
  clientset, err := newClientSet(*runOutsideCluster)

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

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

  <-sigs // Čakáme na signály (do prijatia signálu sa viac nič nedeje)
  log.Printf("Zatvárame...")

  close(stop) // Hovoríme gorutinám, aby sa zastavili
  wg.Wait()   // Čakáme, kým je všetko zastavené
}

(main.go, v raw)

Robíme nasledovné:

  1. Nastavujeme spracovateľa konkrétnych signálov operačného systému, aby sme vyvolali korektné (graceful) ukončenie prevádzky operátora.
  2. Používame WaitGroup, aby sme korektne zastavili všetky gorutiny pred ukončením aplikácie.
  3. Poskytujeme prístup k klastru vytvorením clientset.
  4. Spúšťame NamespaceController, v ktorom sa nachádza celá naša logika.

Teraz potrebujeme základ pre logiku, a v našom prípade to je spomínaný 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, v raw)

Tu nastavujeme SharedIndexInformer, ktorý efektívne (využívajúc cache) čaká na zmeny v názvových priestoroch (viac o informeroch čítajte v článku „Ako naozaj funguje plánovač Kubernetes?» — pozn. prekl.). Potom pripojíme EventHandler k informeru, vďaka čomu sa pri pridávaní názvového priestoru (Namespace) vyvolá funkcia createRoleBinding.

Ďalší krok — definovať túto funkciu 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("Nepodarilo sa vytvoriť Role Binding: %s", err.Error()))
  } else {
    log.Println(fmt.Sprintf("Vytvorený AD RoleBinding pre Namespace: %s", roleBinding.Name))
  }
}

(controller.go, v raw)

Získavame názvový priestor ako obj a pretransformujeme ho na objekt NamespacePotom určíme RoleBinding, na základe toho, čo bolo uvedené na začiatku v YAML súbore, pomocou poskytnutého objektu Namespace a vytvoríme RoleBinding. Nakoniec logujeme, či bolo vytvorenie úspešné.

Posledná funkcia, ktorú je potrebné určiť, je Spustiť:

// 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, v raw)

Tu hovoríme WaitGroup, že spustíme goroutine a následne voláme namespaceInformer, ktorý bol predtým definovaný. Keď príde signál na zastavenie, dokončí funkciu a oznámi WaitGroup, že sa už nevykonáva, a táto funkcia ukončí svoju činnosť.

Informácie o zostavení a spustení tohto operátora v Kubernetese nájdete v repozitári na GitHub.

Na tomto operátore, ktorý vytvára RoleBinding po objavení sa Namespace v Kubernetese, je pripravený.

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster