Construindo Operadores de Kubernetes Usando Kubebuilder e Client-Go

Conceitos Fundamentais

No ecossistema do Kubernetes, a API é baseada em REST. Os recursos são definidos por três componentes principais:

Grupo, Versão e Recurso (GVR)


# O identificador de um recurso é composto por:
# Grupo: define o namespace da API, como 'batch' para jobs.
# Versão: estágio de maturidade, como v1, v1beta1.
# Recurso: a instância concreta, como 'pods' ou 'jobs'.
# Exemplo de consulta:
kubectl api-resources | grep jobs

Grupo, Versão e Tipo (GVK)


# O GVK mapeia um tipo Go para um recurso Kubernetes.
# Ao criar um CRD, o GVK é definido no manifesto.
kubectl get customresourcedefinitions mycrds.example.com -o jsonpath='{.spec.group}/{.spec.versions[*].name}'

Scheme

O Scheme no controller-runtime mantém o mapeaemnto entre GVKs e structs Go, permitindo a serialização e deserialização de objetos.

Desenvolvimenot com Kubebuilder

Configuração do Ambiente


# Instale as ferramentas necessárias
go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest
curl -sSL https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) -o kubebuilder
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

Inicialização do Projeto


# Crie um diretório para o projeto e inicialize o módulo Go
mkdir meu-operador && cd meu-operador
go mod init github.com/exemplo/meu-operador
# Gere a estrutura básica do operador
kubebuilder init --domain exemplo.com
# Adicione um novo recurso customizado (API)
kubebuilder create api --group controle --version v1alpha1 --kind Agendamento

Estrutura do Projeto

Ao inicializar, o Kubebuilder gera uma estrutura padrão com diretórios como cmd/, config/, e api/. O diretório config/ contém manifests para RBAC, CRDs e webhooks, enquanto api/ armazena os tipos definidos pelo usuário.

Definição de APIs

Os tipos de recursos são definidos em arquivos Go. Os campos devem usar tags JSON e anotações para validação.


package v1alpha1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Especificação desejada para o recurso Agendamento
type AgendamentoSpec struct {
    // Cronograma de execução
    // +kubebuilder:validation:Pattern=`^(\*|\d+)(\/\d+)?(\s+(\*|\d+)(\/\d+)?){4}$`
    ExpressaoCron string `json:"expressaoCron"`
    // Template do Job a ser criado
    TemplateJob BatchV1.JobTemplateSpec `json:"templateJob"`
    // Limite de tempo para início
    // +kubebuilder:validation:Minimum=0
    LimiteInicioSeg *int64 `json:"limiteInicioSeg,omitempty"`
}

// Status observado do recurso
type AgendamentoStatus struct {
    UltimoAgendamento *metav1.Time `json:"ultimoAgendamento,omitempty"`
    JobsAtivos        []string     `json:"jobsAtivos,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type Agendamento struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec   AgendamentoSpec   `json:"spec,omitempty"`
    Status AgendamentoStatus `json:"status,omitempty"`
}

Imlpementação do Controlador

O controlador contém a lógica de reconciliação para alinhar o estado atual com o estado desejado.


package controller

import (
    "context"
    "time"

    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/log"

    v1alpha1 "github.com/exemplo/meu-operador/api/v1alpha1"
)

// Reconcilhador para o recurso Agendamento
type AgendamentoReconciler struct {
    client.Client
}

func (r *AgendamentoReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.FromContext(ctx)
    var agendamento v1alpha1.Agendamento

    // Obter o recurso atual
    if err := r.Get(ctx, req.NamespacedName, &agendamento); err != nil {
        logger.Error(err, "Falha ao buscar Agendamento")
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // Lógica de reconciliação aqui
    // Atualizar status, gerenciar Jobs, etc.

    logger.Info("Reconciliação concluída", "recurso", agendamento.Name)
    return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

func (r *AgendamentoReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&v1alpha1.Agendamento{}).
        Complete(r)
}

Webhooks

Webhooks permitem interceptar requisições de API para validação ou mutação.


# Gerar scaffold para webhook
kubebuilder create webhook --group controle --version v1alpha1 --kind Agendamento --defaulting --programmatic-validation

# Exemplo de anotação no tipo Go
// +kubebuilder:webhook:path=/mutate-controle-v1alpha1-agendamento,mutating=true,failurePolicy=fail,groups=controle,resources=agendamentos,verbs=create;update,versions=v1alpha1,name=magendamento.kb.io
type Agendamento struct { ... }

Interagindo com a API via Client-Go

Clientes Disponíveis

  • RESTClient: cliente base para requisições HTTP.
  • Clientset: cliente tipado para recursos internos do Kubernetes.
  • DynamicClient: cliente para recursos arbitários, incluindo CRDs.

Exemplo com Clientset


package main

import (
    "context"
    "fmt"

    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", "")
    if err != nil {
        panic(err.Error())
    }
    cli, err := kubernetes.NewForConfig(config)
    if err != nil {
        panic(err.Error())
    }

    // Listar todos os Pods no namespace default
    pods, err := cli.CoreV1().Pods("default").List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        panic(err.Error())
    }
    for _, pod := range pods.Items {
        fmt.Printf("Pod: %s\n", pod.Name)
    }
}

Usando Dynamic Client para CRDs


package main

import (
    "context"
    "fmt"

    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/client-go/dynamic"
    "k8s.io/client-go/tools/clientcmd"
)

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", "")
    if err != nil {
        panic(err.Error())
    }
    dynClient, err := dynamic.NewForConfig(config)
    if err != nil {
        panic(err.Error())
    }

    gvr := schema.GroupVersionResource{
        Group:    "controle.exemplo.com",
        Version:  "v1alpha1",
        Resource: "agendamentos",
    }

    // Criar um novo recurso Agendamento
    novoAgendamento := &unstructured.Unstructured{
        Object: map[string]interface{}{
            "apiVersion": "controle.exemplo.com/v1alpha1",
            "kind":       "Agendamento",
            "metadata": map[string]interface{}{
                "name":      "meu-agendamento",
                "namespace": "default",
            },
            "spec": map[string]interface{}{
                "expressaoCron": "*/5 * * * *",
            },
        },
    }

    resultado, err := dynClient.Resource(gvr).Namespace("default").Create(context.TODO(), novoAgendamento, metav1.CreateOptions{})
    if err != nil {
        panic(err.Error())
    }
    fmt.Printf("Recurso criado: %s\n", resultado.GetName())
}

Projeto Prático: Monitoramento de Sub-rede

Este exemplo demonstra um operador que calcula o uso de IPs em uma sub-rede definida via CRD.

Definição do Recurso SubRede


package v1alpha1

type SubRedeSpec struct {
    CIDRs []string `json:"cidrs"`
}

type SubRedeStatus struct {
    CIDRsUtilizados []string `json:"cidrsUtilizados,omitempty"`
    TotalIPs        int      `json:"totalIPs,omitempty"`
    IPsUtilizados   int      `json:"ipsUtilizados,omitempty"`
    IPsDisponiveis  int      `json:"ipsDisponiveis,omitempty"`
    Gateways        []string `json:"gateways,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Total",type=integer,JSONPath=".status.totalIPs"
type SubRede struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`
    Spec   SubRedeSpec   `json:"spec,omitempty"`
    Status SubRedeStatus `json:"status,omitempty"`
}

Lógica do Controlador


package controller

import (
    "context"
    "net"
    "time"

    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/log"

    v1alpha1 "github.com/exemplo/meu-operador/api/v1alpha1"
)

type SubRedeReconciler struct {
    client.Client
}

func (r *SubRedeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    logger := log.FromContext(ctx)
    var rede v1alpha1.SubRede

    if err := r.Get(ctx, req.NamespacedName, &rede); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // Calcular estatísticas da sub-rede
    totalIPs, gateways := calcularIPs(rede.Spec.CIDRs)
    rede.Status.TotalIPs = totalIPs
    rede.Status.Gateways = gateways
    // A lógica real buscaria pods para contar IPs utilizados
    rede.Status.IPsUtilizados = 0
    rede.Status.IPsDisponiveis = totalIPs

    if err := r.Status().Update(ctx, &rede); err != nil {
        logger.Error(err, "Falha ao atualizar status")
        return ctrl.Result{}, err
    }

    return ctrl.Result{RequeueAfter: 60 * time.Second}, nil
}

func calcularIPs(cidrs []string) (int, []string) {
    var total int
    var gws []string
    for _, cidr := range cidrs {
        _, ipNet, _ := net.ParseCIDR(cidr)
        bits, size := ipNet.Mask.Size()
        total += 1<<uint :="ipNet.IP" broadcast="" complete="" ctrl.manager="" ctrl.newcontrollermanagedby="" da="" de="" desconta="" e="" endere="" error="" for="" func="" gateway="" gw="" gw.string="" gws="append(gws," ip="" o="" primeiro="" rede="" return="" setupwithmanager="" sub-rede="" total=""></uint>

Tags: kubernetes-operator kubebuilder client-go go custom-resource-definition

Publicado em 6-21 17:44