Nota do tradutor.: Operadores (operators) são softwares auxiliares para Kubernetes, destinados a automatizar a execução de ações rotineiras sobre objetos do cluster com base em certos eventos. Já escrevemos sobre operadores em , onde discutimos os fundamentos e princípios de seu funcionamento. Contudo, o material anterior era mais uma perspectiva sobre a operação de componentes prontos para Kubernetes, enquanto a tradução deste novo artigo representa a visão de um desenvolvedor / engenheiro DevOps, intrigado com a implementação de um novo operador.

Decidi escrever este post com um exemplo da vida real após minhas tentativas de encontrar documentação sobre a criação de um operador para Kubernetes, que envolveram o estudo do código.
O exemplo que será descrito é o seguinte: em nosso cluster Kubernetes, cada Namespace representa um ambiente sandbox de alguma equipe, e queríamos restringir o acesso a eles de forma que as equipes pudessem brincar apenas em suas próprias areias.
Isso pode ser alcançado atribuindo ao usuário um grupo que possua RoleBinding para as Namespace e ClusterRole com permissão para editar. A representação em YAML ficará assim:
---
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(, neste teste de 2015 )
É possível criar um desse RoleBinding manualmente, mas após ultrapassar a marca de uma centena de namespaces torna-se uma tarefa cansativa. É aqui que os operadores Kubernetes ajudam — eles permitem automatizar a criação de recursos Kubernetes, baseando-se em mudanças nos recursos. Em nosso caso, queremos criar RoleBinding quando a criação Namespace.
Primeiro, vamos definir a função main, que executa a configuração necessária para iniciar o operador e em seguida chama a ação do operador:
(Nota do tradutor.: aqui e adiante, os comentários no código foram traduzidos para o português. Além disso, as indentação foram corrigidas para espaços em vez de [tabs recomendados no Go] apenas para melhor legibilidade no layout do Habra. Após cada listagem, são fornecidos links para o original no GitHub, onde os comentários em inglês e as tabs estão preservados.)
func main() {
// Configuramos a saída de logs para o STDOUT do console
log.SetOutput(os.Stdout)
sigs := make(chan os.Signal, 1) // Criamos um canal para receber sinais do sistema operacional
stop := make(chan struct{}) // Criamos um canal para receber o sinal de parada
// Registramos o recebimento de SIGTERM no canal sigs
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
// Goroutines podem se adicionar ao WaitGroup,
// para aguardar a conclusão de sua execução
wg := &sync.WaitGroup{}
runOutsideCluster := flag.Bool("run-outside-cluster", false, "Defina esta flag ao executar fora do cluster.")
flag.Parse()
// Criamos um clientset para interagir com o cluster Kubernetes
clientset, err := newClientSet(*runOutsideCluster)
if err != nil {
panic(err.Error())
}
controller.NewNamespaceController(clientset).Run(stop, wg)
<-sigs // Aguardamos sinais (nada mais acontece até que um sinal seja recebido)
log.Printf("Encerrando...")
close(stop) // Dizemos para as goroutines pararem
wg.Wait() // Esperamos que tudo tenha parado
}(, neste teste de 2015 )
Estamos realizando o seguinte:
- Configuramos manipuladores para sinais específicos do sistema operacional para realizar uma finalização correta (graceful) do operador.
- Usaremos
WaitGroup, para parar corretamente todas as goroutines antes de encerrar o aplicativo. - Fornecemos acesso ao cluster criando um
clientset. - Iniciamos
NamespaceController, onde toda a nossa lógica será alocada.
Agora precisamos de uma base para a lógica, e no nosso caso isso é o mencionado 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
}(, neste teste de 2015 )
Aqui configuramos um SharedIndexInformer, que aguardará efetivamente (usando cache) por mudanças nos namespaces (leia mais sobre informers no artigo "» — nota do tradutor.). Depois disso, conectamos um EventHandler ao informer, fazendo com que, ao adicionar um namespace (Namespace), a função createRoleBinding.
O próximo passo é definir essa função 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("Falha ao criar Role Binding: %s", err.Error()))
} else {
log.Println(fmt.Sprintf("Created AD RoleBinding for Namespace: %s", roleBinding.Name))
}
}(, neste teste de 2015 )
Recebemos o namespace como obj e transformamos isso em um objeto Namespace. Em seguida, definimos RoleBinding, com base no mencionado no início do arquivo YAML, usando o objeto fornecido Namespace e criando RoleBinding. Por fim, registramos se a criação foi bem-sucedida.
A última função que precisamos definir é 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
}(, neste teste de 2015 )
Aqui estamos dizendo WaitGroup, que iniciaremos a goroutine e então chamamos namespaceInformer, que foi previamente definido. Quando um sinal de parada for recebido, ele encerrará a função, informará WaitGroup, que não está mais em execução, e essa função encerrará seu trabalho.
As informações sobre a construção e execução deste operador no cluster Kubernetes podem ser encontradas em .
É neste ponto que o operador que cria RoleBinding quando surge Namespace no cluster Kubernetes, está pronto.
Fonte: habr.com
