Usando client-go para `kubectl apply` contra la API de Kubernetes directamente con múltiples tipos en un solo archivo YAML

10

Estoy usando https://github.com/kubernetes/client-go y todo funciona bien.

Tengo un manifiesto (YAML) para el panel oficial de Kubernetes: https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml

Quiero imitar kubectl applyeste manifiesto en el código Go, usando client-go.

Entiendo que necesito hacer una (des) clasificación de los bytes YAML en los tipos de API correctos definidos en el paquete: https://github.com/kubernetes/api

He Createeditado con éxito tipos de API individuales en mi clúster, pero ¿cómo hago esto para un manifiesto que contiene una lista de tipos que no son iguales ? ¿Hay algún recurso kind: List*que admita estos diferentes tipos?

Mi solución actual es dividir el archivo YAML usando csplit--- como delimitador

csplit /path/to/recommended.yaml /---/ '{*}' --prefix='dashboard.' --suffix-format='%03d.yaml'

Luego, recorro las nuevas (14) partes que se crearon, leo sus bytes, enciendo el tipo de objeto devuelto por el decodificador UniversalDeserializer y llamo a los métodos API correctos usando mi conjunto de clientes k8s.

Me gustaría hacer esto mediante programación para realizar actualizaciones a cualquier versión nueva del tablero en mi clúster. También tendré que hacer esto para el servidor de métricas y muchos otros recursos. El método alternativo (quizás más simple) es enviar mi código con kubectl instalado en la imagen del contenedor y llamar directamente kubectl apply -f -; pero eso significa que también necesito escribir la configuración de kube en el disco o tal vez pasarla en línea para que kubectl pueda usarla.

Este problema me pareció útil: https://github.com/kubernetes/client-go/issues/193 El decodificador vive aquí: https://github.com/kubernetes/apimachinery/tree/master/pkg/runtime/ serializador

Está expuesto en client-go here: https://github.com/kubernetes/client-go/blob/master/kubernetes/scheme/register.go#L69

También he echado un vistazo al método RunConvert que utiliza kubectl: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/convert/convert.go#L139 y supongo que yo puedo proporcionar mis propios genericoclioptions.IOStreams para obtener la salida?

Parece que RunConvert está en una ruta de desuso

También he visto otras preguntas etiquetadas [cliente-ir] pero la mayoría usa ejemplos antiguos o usa un archivo YAML con un único kinddefinido, y la API ha cambiado desde entonces.

Editar: como necesito hacer esto para más de un clúster y estoy creando clústeres mediante programación (AWS EKS API + CloudFormation / eksctl ), me gustaría minimizar la sobrecarga de crear ServiceAccounts en muchos contextos de clúster, en muchas cuentas de AWS. Idealmente, el único paso de autenticación involucrado en la creación de mi conjunto de clientes es usar aws-iam-authenticator para obtener un token usando datos de clúster (nombre, región, certificado de CA, etc.). No ha habido un lanzamiento de aws-iam-authenticator durante un tiempo, pero el contenido de masterpermite el uso de una función de cuenta cruzada de roles de terceros y una identificación externa para pasar. OMI, esto es más limpio que usar un ServiceAccount(y IRSA) porque hay otros servicios de AWS con los que la aplicación (la API de back-end que crea y aplica complementos a estos clústeres) necesita interactuar.

Editar: Recientemente encontré https://github.com/ericchiang/k8s . Definitivamente es más simple de usar que cliente-go, a un alto nivel, pero no admite este comportamiento.

Simón
fuente
1
En lugar de escribir la configuración de Kube en el disco del contenedor, intente usar la cuenta de servicio kubernetes.io/docs/tasks/configure-pod-container/…
KFC_
1
¿Por qué no lees el contenido del archivo YAML y lo divides ^---$en tu código?
Shawyeok
@Shawyeok Porque eso todavía requiere que sepa qué tipos hay en el archivo. No hay forma de obtener el tipo dinámicamente sin probar varios tipos esperados (objetos de Kubernetes), y si el tipo esperado no está presente, el objeto no se aplicará al clúster (lo que genera aún más problemas). Esto también resultaría en tener que escribir una gran cantidad de código para un solo componente que no escala para varios componentes. Más allá de la decodificación está llamando al método API correcto para aplicar el objeto a un clúster.
Simon

Respuestas:

3

Parece que has descubierto cómo deserializar archivos YAML en Kubernetes runtime.Object, pero el problema es implementar dinámicamente runtime.Objectsin escribir un código especial para cada Tipo.

kubectllogra esto al interactuar con la API REST directamente. Específicamente, a través de resource.Helper .

En mi código, tengo algo como:

import (
    meta "k8s.io/apimachinery/pkg/api/meta"
    "k8s.io/cli-runtime/pkg/resource"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/client-go/restmapper"
    "k8s.io/apimachinery/pkg/runtime"
)

func createObject(kubeClientset kubernetes.Interface, restConfig rest.Config, obj runtime.Object) error {
    // Create a REST mapper that tracks information about the available resources in the cluster.
    groupResources, err := restmapper.GetAPIGroupResources(kubeClientset.Discovery())
    if err != nil {
        return err
    }
    rm := restmapper.NewDiscoveryRESTMapper(groupResources)

    // Get some metadata needed to make the REST request.
    gvk := obj.GetObjectKind().GroupVersionKind()
    gk := schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}
    mapping, err := rm.RESTMapping(gk, gvk.Version)
    if err != nil {
        return err
    }

    name, err := meta.NewAccessor().Name(obj)
    if err != nil {
        return err
    }

    // Create a client specifically for creating the object.
    restClient, err := newRestClient(restConfig, mapping.GroupVersionKind.GroupVersion())
    if err != nil {
        return err
    }

    // Use the REST helper to create the object in the "default" namespace.
    restHelper := resource.NewHelper(restClient, mapping)
    return restHelper.Create("default", false, obj, &metav1.CreateOptions{})
}

func newRestClient(restConfig rest.Config, gv schema.GroupVersion) (rest.Interface, error) {
    restConfig.ContentConfig = resource.UnstructuredPlusDefaultContentConfig()
    restConfig.GroupVersion = &gv
    if len(gv.Group) == 0 {
        restConfig.APIPath = "/api"
    } else {
        restConfig.APIPath = "/apis"
    }

    return rest.RESTClientFor(&restConfig)
}
Kevin Lin
fuente
Hola Kevin, gracias por tu respuesta! No he tenido la oportunidad de probar esto, pero no estaba al tanto package restmappery parece muy prometedor. Aceptando la respuesta por ahora, pero la revisaremos pronto.
Simon
1
¡Espero que funcione para ti!
Kevin Lin