Verwenden Sie client-go to `kubectl apply` für die Kubernetes-API direkt mit mehreren Typen in einer einzelnen YAML-Datei

10

Ich verwende https://github.com/kubernetes/client-go und alles funktioniert gut.

Ich habe ein Manifest (YAML) für das offizielle Kubernetes-Dashboard: https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml

Ich möchte kubectl applydieses Manifest im Go-Code mit client-go nachahmen .

Ich verstehe, dass ich einige (un) Marshalling der YAML-Bytes in die richtigen API-Typen durchführen muss, die im Paket definiert sind: https://github.com/kubernetes/api

Ich habe Createeinzelne API-Typen erfolgreich in meinem Cluster bearbeitet. Wie kann ich dies für ein Manifest tun, das eine Liste von Typen enthält, die nicht identisch sind ? Gibt es eine Ressource kind: List*, die diese verschiedenen Typen unterstützt?

Meine aktuelle Problemumgehung besteht darin, die YAML-Datei csplitmit --- als Trennzeichen zu teilen

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

Als nächstes durchlaufe ich die neuen (14) Teile, die erstellt wurden, lese ihre Bytes, schalte den Typ des vom UniversalDeserializer-Decoder zurückgegebenen Objekts ein und rufe die richtigen API-Methoden mit meinem k8s-Clientset auf.

Ich möchte dies programmgesteuert tun, um Aktualisierungen neuer Versionen des Dashboards in meinem Cluster vorzunehmen. Ich muss dies auch für den Metrics Server und viele andere Ressourcen tun. Die alternative (vielleicht einfachere) Methode besteht darin, meinen Code mit kubectl im Container-Image zu versenden und direkt aufzurufen kubectl apply -f -. Das bedeutet aber auch, dass ich die kube-Konfiguration auf die Festplatte schreiben oder sie inline übergeben muss, damit kubectl sie verwenden kann.

Ich fand dieses Problem hilfreich: https://github.com/kubernetes/client-go/issues/193 Der Decoder lebt hier: https://github.com/kubernetes/apimachinery/tree/master/pkg/runtime/ Serializer

Es wird in client-go hier angezeigt : https://github.com/kubernetes/client-go/blob/master/kubernetes/scheme/register.go#L69

Ich habe mir auch die von kubectl verwendete RunConvert-Methode angesehen: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/convert/convert.go#L139 und gehe davon aus, dass ich kann ich meine eigenen genericclioptions.IOStreams bereitstellen , um die Ausgabe zu erhalten?

Es sieht so aus, als ob sich RunConvert auf einem Verfallspfad befindet

Ich habe mir auch andere Fragen mit dem Tag [client-go] angesehen, aber die meisten verwenden alte Beispiele oder verwenden eine YAML-Datei mit einer einzigen kinddefinierten, und die API hat sich seitdem geändert.

Bearbeiten: Da ich dies für mehr als einen Cluster tun muss und Cluster programmgesteuert erstelle (AWS EKS API + CloudFormation / eksctl ), möchte ich den Aufwand für das Erstellen von ServiceAccounts in vielen Clusterkontexten und in vielen AWS-Konten minimieren . Im Idealfall besteht der einzige Authentifizierungsschritt beim Erstellen meines Clientsets in der Verwendung von aws-iam-authenticator , um ein Token mithilfe von Clusterdaten (Name, Region, CA-Zertifikat usw.) abzurufen. Es gibt seit einiger Zeit keine Veröffentlichung von aws-iam-authentulator mehr, aber der Inhalt von masterermöglicht die Weitergabe einer kontoübergreifenden Rolle eines Drittanbieters und einer externen ID. IMO, das ist sauberer als mit a ServiceAccount(und IRSA) Da es andere AWS-Services gibt, muss die Anwendung (die Backend-API, die Add-Ons erstellt und auf diese Cluster anwendet) interagieren.

Bearbeiten: Ich habe kürzlich https://github.com/ericchiang/k8s gefunden . Es ist definitiv einfacher zu verwenden als Client-Go auf hoher Ebene, unterstützt dieses Verhalten jedoch nicht.

Simon
quelle
1
Anstatt kube config auf die Festplatte des Containers zu schreiben, verwenden Sie das Dienstkonto kubernetes.io/docs/tasks/configure-pod-container/…
KFC_
1
Warum lesen Sie nicht einfach den Inhalt der YAML-Datei und teilen ihn ^---$in Ihrem Code auf?
Shawyeok
@Shawyeok Da ich immer noch wissen muss, welche Typen in der Datei enthalten sind. Es gibt keine Möglichkeit, den Typ dynamisch abzurufen, ohne mehrere erwartete Typen (Kubernetes-Objekte) zu testen. Wenn der erwartete Typ nicht vorhanden ist, wird das Objekt nicht auf den Cluster angewendet (was zu noch mehr Problemen führt). Dies würde auch dazu führen, dass viel Code für eine einzelne Komponente geschrieben werden muss, die nicht für mehrere Komponenten skaliert werden kann. Über die Dekodierung hinaus wird die richtige API-Methode aufgerufen, um das Objekt auf einen Cluster anzuwenden.
Simon

Antworten:

3

Es hört sich so an, als hätten Sie herausgefunden, wie YAML-Dateien in Kubernetes deserialisiert werden können runtime.Object, aber das Problem besteht darin, einen dynamisch bereitzustellen, runtime.Objectohne für jede Art speziellen Code zu schreiben.

kubectlDies wird durch die direkte Interaktion mit der REST-API erreicht . Insbesondere über resource.Helper .

In meinem Code habe ich so etwas wie:

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
quelle
Hallo Kevin, danke für deine Antwort! Ich hatte keine Chance, dies zu versuchen, aber ich war mir dessen nicht bewusst package restmapperund das sieht sehr vielversprechend aus. Akzeptiere die Antwort vorerst, werde sie aber bald wieder besuchen.
Simon
1
Hoffe es funktioniert bei dir!
Kevin Lin