Utilisation de client-go pour `kubectl apply` contre l'API Kubernetes directement avec plusieurs types dans un seul fichier YAML

10

J'utilise https://github.com/kubernetes/client-go et tout fonctionne bien.

J'ai un manifeste (YAML) pour le tableau de bord officiel de Kubernetes: https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml

Je veux imiter kubectl applyce manifeste dans le code Go, en utilisant client-go.

Je comprends que je dois faire un (dé) regroupement des octets YAML dans les types d'API corrects définis dans le package: https://github.com/kubernetes/api

J'ai réussi Createà éditer des types d'API uniques dans mon cluster, mais comment dois-je procéder pour un manifeste contenant une liste de types différents ? Existe-t-il une ressource kind: List*qui prend en charge ces différents types?

Ma solution de contournement actuelle consiste à diviser le fichier YAML en utilisant csplitavec --- comme délimiteur

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

Ensuite, je fais une boucle sur les nouvelles (14) parties qui ont été créées, je lis leurs octets, j'allume le type de l'objet renvoyé par le décodeur de UniversalDeserializer et j'appelle les bonnes méthodes API à l'aide de mon ensemble de clients k8s.

Je voudrais le faire par programmation pour mettre à jour toutes les nouvelles versions du tableau de bord dans mon cluster. Je devrai également le faire pour le serveur de métriques et de nombreuses autres ressources. La méthode alternative (peut-être plus simple) consiste à expédier mon code avec kubectl installé sur l'image du conteneur et à appeler directement kubectl apply -f -; mais cela signifie que je dois également écrire la configuration de kube sur le disque ou peut-être la passer en ligne pour que kubectl puisse l'utiliser.

J'ai trouvé ce problème utile: https://github.com/kubernetes/client-go/issues/193 Le décodeur vit ici: https://github.com/kubernetes/apimachinery/tree/master/pkg/runtime/ sérialiseur

Il est exposé dans Client-Go ici: https://github.com/kubernetes/client-go/blob/master/kubernetes/scheme/register.go#L69

J'ai également jeté un coup d'œil à la méthode RunConvert utilisée par kubectl: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/convert/convert.go#L139 et supposer que je peut fournir mes propres génériquesclioptions.IOStreams pour obtenir la sortie?

Il semble que RunConvert soit sur un chemin de dépréciation

J'ai également examiné d'autres questions étiquetées [client-go] mais la plupart utilisent d'anciens exemples ou utilisent un fichier YAML avec un seul kinddéfini, et l'API a changé depuis.

Edit: parce que je dois le faire pour plus d'un cluster et que je crée des clusters par programme (AWS EKS API + CloudFormation / eksctl ), je voudrais minimiser la surcharge de création de ServiceAccounts dans de nombreux contextes de cluster, sur de nombreux comptes AWS. Idéalement, la seule étape d'authentification impliquée dans la création de mon ensemble de clients consiste à utiliser aws-iam- authentator pour obtenir un jeton à l'aide des données du cluster (nom, région, certificat CA, etc.). Il n'y a pas eu de sortie de aws-iam-authentator depuis un moment, mais le contenu de masterpermet l'utilisation d'un rôle de compte croisé de rôle tiers et d'un ID externe. OMI, c'est plus propre que d'utiliser un ServiceAccount(et IRSA) car il existe d'autres services AWS avec lesquels l'application (l'API backend qui crée et applique des modules complémentaires à ces clusters) doit interagir.

Edit: J'ai récemment trouvé https://github.com/ericchiang/k8s . Il est certainement plus simple à utiliser que client-go, à un niveau élevé, mais ne prend pas en charge ce comportement.

Simon
la source
1
Au lieu d'écrire la configuration de kube sur le disque du conteneur, essayez d'utiliser le compte de service kubernetes.io/docs/tasks/configure-pod-container/…
KFC_
1
Pourquoi ne lisez-vous pas simplement le contenu du fichier YAML et le divisez- ^---$vous dans votre code?
Shawyeok
@Shawyeok Parce que cela nécessite encore que je sache quels types sont dans le fichier. Il n'y a aucun moyen d'obtenir le type dynamiquement sans tester contre plusieurs types attendus (objets Kubernetes), et si le type attendu n'est pas présent, l'objet ne sera pas appliqué au cluster (ce qui entraîne encore plus de problèmes). Cela entraînerait également d'avoir à écrire beaucoup de code pour un seul composant qui n'est pas mis à l'échelle pour plusieurs composants. Au-delà du décodage, on appelle la bonne méthode API pour appliquer l'objet à un cluster.
Simon

Réponses:

3

Il semble que vous ayez compris comment désérialiser des fichiers YAML dans Kubernetes runtime.Object, mais le problème est de déployer dynamiquement un runtime.Objectsans écrire de code spécial pour chaque type.

kubectlatteint cet objectif en interagissant directement avec l' API REST . Plus précisément, via resource.Helper .

Dans mon code, j'ai quelque chose comme:

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
la source
Bonjour Kevin, merci pour ta réponse! Je n'ai pas eu l'occasion de l'essayer, mais je n'étais pas au courant package restmapperet cela semble très prometteur. J'accepte la réponse pour l'instant, mais je la reverrai bientôt.
Simon
1
J'espère que ça marche pour toi!
Kevin Lin