跳到主要内容

Docker 安装 Rancher

· 阅读需 2 分钟

Docker 安装 Rancher

docker run -d --privileged --restart=unless-stopped -p 8081:80 -p 30002:443 -v /docker/rancher/rancher:/var/lib/rancher -v /docker/rancher/auditlog:/var/log/auditlog --name rancher rancher/rancher:stable 

根据提示,将集群接入 Rancher 中

故障解决

# 删除 Rancher 依赖的 namespace 出现报错
Error from server (Forbidden): error when creating "STDIN": serviceaccounts "cattle" is forbidden: unable to create new content in namespace cattle-system because it is being terminated
Error from server (Forbidden): error when creating "STDIN": secrets "cattle-credentials-855d8e8" is forbidden: unable to create new content in namespace cattle-system because it is being terminated
Error from server (Forbidden): error when creating "STDIN": deployments.apps "cattle-cluster-agent" is forbidden: unable to create new content in namespace cattle-system because it is being terminated
Error from server (Forbidden): error when creating "STDIN": daemonsets.apps "cattle-node-agent" is forbidden: unable to create new content in namespace cattle-system because it is being terminated

# 解决办法:
kubectl patch namespace cattle-system -p '{"metadata":{"finalizers":[]}}' --type='merge' -n cattle-system
kubectl delete namespace cattle-system --grace-period=0 --force
kubectl patch namespace cattle-global-data -p '{"metadata":{"finalizers":[]}}' --type='merge' -n cattle-system
kubectl delete namespace cattle-global-data --grace-period=0 --force
kubectl patch namespace local -p '{"metadata":{"finalizers":[]}}' --type='merge' -n cattle-system
for resource in `kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get -o name -n local`; do kubectl patch $resource -p '{"metadata": {"finalizers": []}}' --type='merge' -n local; done
kubectl delete namespace local --grace-period=0 --force

# 重新将集群加入 Rancher 中

Client-go 四种客户端交互对象

· 阅读需 16 分钟

Client-go 四种客户端交互对象

![Client-go四种客户端交互对象.png](/img/Client-go 四种客户端交互对象/Client-go四种客户端交互对象.png)

RESTClient 是最基础的客户端。 RESTClientHTTP Request 进⾏了封装,实现了 RESTful ⻛格的 API 。 ClientSetDynamicClientDiscoveryClient 客户端都是基于 RESTClient 实现的。

ClientSetRESTClient 的基础上封装了对 ResourceVersion 的管理⽅法。每⼀个 Resource 可以理解为⼀个客户端,⽽ ClientSet 则是多个客户端的集合,每⼀个 ResourceVersion 都以函数的⽅式暴露给开发者。 ClientSet 只能够处理 Kubernetes 内 置资源,它是通过 client-gen 代码⽣成器⾃动⽣成的。

DynamicClientClientSet 最⼤的不同之处是, ClientSet 仅能访问 Kubernetes ⾃带的资源(即 Client 集合内的资源),不能直接访问 CRD ⾃定义资源。 DynamicClient 能够处理 Kubernetes 中的所有资源对象,包括 Kubernetes 内置资源与 CRD ⾃定义资源。

DiscoveryClient 发现客户端,⽤于发现 kube-apiserver 所⽀持的资源组、资源版本、资源信息(即 Group、Versions、Resources )。

以上 4 种客户端: RESTClientClientSetDynamicClientDiscoveryClient 都可以通过 kubeconfig 配置信息连接到指定的 Kubernetes API Server

RestClient 客户端对象

RESTClient 是最基础的客户端。其他的 ClientSetDynamicClientDiscoveryClient 都是基于 RESTClient 实现的。 RESTClientHTTP Request 进⾏了封装,实现了 RESTful ⻛格的 API 。它具有很⾼的灵活性,数据不依赖于⽅法和资源,因此 RESTClient 能够处理多种类型的调⽤,返回不同的数据格式。

类似于 kubectl 命令,通过 RESTClient 列出 kube-system 运⾏的 Pod 资源对象, RESTClient Example 代码⽰例 :

package main

import (
"context"
"flag"
"fmt"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"path/filepath"
)

func main() {
var kubeconfig *string

// home是家目录,如果能取得家目录的值,就可以用来做默认值
if home:=homedir.HomeDir(); home != "" {
// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
// 如果取不到当前用户的家目录,就没办法设置kubeconfig的默认目录了,只能从入参中取
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}

flag.Parse()

// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

// kubeconfig加载失败就直接退出了
if err != nil {
panic(err.Error())
}

// 参考path : /api/v1/namespaces/{namespace}/pods
config.APIPath = "api"
// pod的group是空字符串
config.GroupVersion = &corev1.SchemeGroupVersion
// 指定序列化工具
config.NegotiatedSerializer = scheme.Codecs

// 根据配置信息构建restClient实例
restClient, err := rest.RESTClientFor(config)

if err!=nil {
panic(err.Error())
}

// 保存pod结果的数据结构实例
result := &corev1.PodList{}

// 指定namespace
namespace := "kube-system"
// 设置请求参数,然后发起请求
// GET请求
err = restClient.Get().
// 指定namespace,参考path : /api/v1/namespaces/{namespace}/pods
Namespace(namespace).
// 查找多个pod,参考path : /api/v1/namespaces/{namespace}/pods
Resource("pods").
// 指定大小限制和序列化工具
VersionedParams(&metav1.ListOptions{Limit:100}, scheme.ParameterCodec).
// 请求
Do(context.TODO()).
// 结果存入result
Into(result)

if err != nil {
panic(err.Error())
}

// 打印名称
fmt.Printf("Namespace\t Status\t\t Name\n")

// 每个pod都打印Namespace、Status.Phase、Name三个字段
for _, d := range result.Items {
fmt.Printf("%v\t %v\t %v\n",
d.Namespace,
d.Status.Phase,
d.Name)
}
}

运⾏以上代码,列出 kube-system 命名空间下的所有 Pod 资源对象的相关信息。⾸先加载 kubeconfig 配置信息,并设置 config.APIPath 请求的 HTTP 路径。然后设置 config.GroupVersion 请求的资源组/资源版本。最后设置 config.NegotiatedSerializer 数据的编解码器。

rest.RESTClientFor 函数通过 kubeconfig 配置信息实例化 RESTClient 对象, RESTClient 对象构建 HTTP 请求参数,例如 Get 函数设置请求⽅法为 get 操作,它还⽀持 PostPutDeletePatch 等请求⽅法。 Namespace 函数设置请求的命名空间。 Resource 函数设置请求的资源名称。 VersionedParams 函数将⼀些查询选项(如 limit、TimeoutSeconds 等)添加到请求参数中。通过 Do 函数执⾏该请求,并将 kube-apiserver 返回的结果( Result 对象)解析到 corev1.PodList 对象中。最终格式化输出结果。

RESTClient 发送请求的过程对 Go 语⾔标准库 net/http 进⾏了封装,由 Do→request 函数实现,代码⽰例如下:

代码路径: vendor/k8s.io/client-go/rest/request.go

func (r *Request) Do(ctx context.Context) Result { 
var result Result
err := r.request(ctx, func(req *http.Request, resp *http.Response) {
result = r.transformResponse(resp, req)
})
if err != nil {
return Result{err: err}
}return result
}

请求发送之前需要根据请求参数⽣成请求的 RESTful URL ,由 r.URL.String 函数完成。例如,在 RESTClient Example 代码⽰例中,根据请求参数⽣成请求的 RESTful URL http://127.0.0.1:8080/api/v1/namespaces/kube-system/pods?limit=500 ,其中 api 参数为 v1namespace 参数为 system ,请求的资源为 podslimit 参数表⽰最多检索出 500 条信息。

最后通过 Go 语⾔标准库 net/httpRESTful URL (即 kube-apiserver )发送请求,请求得到的结果存放在 http.ResponseBody 对象中, fn 函数(即 transformResponse )将结果转换为资源对象。当函数退出时,会通过 resp.Body.Close 命令进⾏关闭,防⽌内存溢出。

ClientSet客户端对象

RESTClient 是⼀种最基础的客户端,使⽤时需要指定 ResourceVersion 等信息,编写代码时需要提前知道 Resource 所在的 Group 和对应的 Version 信息。相⽐ RESTClientClientSet 使⽤起来更加便捷,⼀般情况下,开发者对 Kubernetes 进⾏⼆次开发时通常使⽤ ClientSet

ClientSetRESTClient 的基础上封装了对 ResourceVersion 的管理⽅法。每⼀个 Resource 可以理解为⼀个客户端,⽽ ClientSet 则是多个客户端的集合,每⼀个 ResourceVersion 都以函数的⽅式暴露给开发者,例如, ClientSet 提供的 RbacV1CoreV1NetworkingV1 等接⼝函数

注意: ClientSet 仅能访问 Kubernetes ⾃⾝内置的资源(即客户端集合内的资源),不能直接访问 CRD ⾃定义资源。 如果需要 ClientSet 访问 CRD ⾃定义资源,可以通过 client-gen 代码⽣成器重新⽣成 ClientSet ,在 ClientSet 集合中⾃动⽣成与 CRD 操作相关的接⼝

![ClientSet.png](/img/Client-go 四种客户端交互对象/ClientSet.png)

通过 ClientSet 创建⼀个新的命名空间 xiaoyuan 以及⼀个新的 deploymentClientSet Example 代码,类似于 kubectl 命令

⽰例如下:

package main

import (
"context"
"flag"
"fmt"
"path/filepath"

appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)

const (
NAMESPACE = "xiaoyuan"
DEPLOYMENTNAME = "kubecto-deploy"
IMAGE = "nginx:1.13"
PORT = 80
REPLICAS = 2
)

func main() {
var kubeconfig *string
// home是家目录,如果能取得家目录的值,就可以用来做默认值
if home := homedir.HomeDir(); home != "" {
// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) kubeconfig文件的绝对路径")
} else {
kubeconfig = flag.String("kubeconfig", "", "kubeconfig文件的绝对路径")
}
flag.Parse()

// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
// kubeconfig加载失败就直接退出了
if err != nil {
panic(err)
}
// 实例化clientset对象
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
// 引用namespace的函数
createNamespace(clientset)

// 引用deployment的函数
createDeployment(clientset)
}

// 新建namespace
func createNamespace(clientset *kubernetes.Clientset) {
namespaceClient := clientset.CoreV1().Namespaces()

namespace := &apiv1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: NAMESPACE,
},
}

result, err := namespaceClient.Create(context.TODO(), namespace, metav1.CreateOptions{})

if err!=nil {
panic(err.Error())
}

fmt.Printf("Create namespace %s \n", result.GetName())
}
// 新建deployment
func createDeployment(clientset *kubernetes.Clientset) {
//如果希望在default命名空间下场景可以引用apiv1.NamespaceDefault默认字符
//deploymentsClient := clientset.AppsV1().Deployments(apiv1.NamespaceDefault)
//拿到deployment的客户端
deploymentsClient := clientset.AppsV1().Deployments(NAMESPACE)

deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: DEPLOYMENTNAME,
},
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(REPLICAS),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "kubecto",
},
},
Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "kubecto",
},
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{
{
Name: "web",
Image: IMAGE,
Ports: []apiv1.ContainerPort{
{
Name: "http",
Protocol: apiv1.ProtocolTCP,
ContainerPort: PORT,
},
},
},
},
},
},
},
}

// Create Deployment
fmt.Println("Creating deployment...")
result, err := deploymentsClient.Create(context.TODO(), deployment, metav1.CreateOptions{})
if err != nil {
panic(err)
}
fmt.Printf("Created deployment %q.\n", result.GetObjectMeta().GetName())
}
//引用replicas带入副本集
func int32Ptr(i int32) *int32 { return &i }

运⾏以上代码,会创建 2nginxdeployment 。⾸先加载 kubeconfig 配置信息, kubernetes.NewForConfig 通过 kubeconfig 配置信息实例化 clientset 对象,该对象⽤于管理所有 Resource 的客户端。

DynamicClient客户端对象

DynamicClient 是⼀种动态客户端,它可以对任意 Kubernetes 资源进⾏ RESTful 操作,包括 CRD ⾃定义资源。 DynamicClientClientSet 操作类似,同样封装了 RESTClient ,同样提供了 CreateUpdateDeleteGetListWatchPatch 等⽅法

DynamicClientClientSet 最⼤的不同之处是, ClientSet 仅能访问 Kubernetes ⾃带的资源(即客户端集合内的资源),不能直接访问 CRD ⾃定义资源。 ClientSet 需要预先实现每种 ResourceVersion 的操作,其内部的数据都是结构化数据(即已知数据结构)。⽽ DynamicClient 内部实现了 Unstructured ,⽤于处理⾮结构化数据结构(即⽆法提前预知数据结构),这也是 DynamicClient 能够处理 CRD ⾃定义资源的关键

注意: DynamicClient 不是类型安全的,因此在访问 CRD ⾃定义资源时需要特别注意。例如,在操作指针不当的情况下可能会导致程序崩溃

DynamicClient 的处理过程将 Resource (例如 PodList )转换成 Unstructured 结构类型, Kubernetes 的所有 Resource 都可以转换为该结构类型。处理完成后,再将 Unstructured 转换成 PodList 。整个过程类似于 Go 语⾔的 interface{} 断⾔转换过程。另外, Unstructured 结构类型是通过 map[string]interface{} 转换的。

通过 DynamicClient 创建 deployment ,并使⽤ list 列出当前 pod 的名称和数量,类似 kubectl 命令 ,DynamicClient Example 代码⽰例:

package main

import (
"bufio"
"context"
"flag"
"fmt"
"os"
"path/filepath"

apiv1 "k8s.io/api/core/v1"
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"
"k8s.io/client-go/util/homedir"
)

func main() {
var kubeconfig *string
// home是家目录,如果能取得家目录的值,就可以用来做默认值
// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}
flag.Parse()

//定义函数内的变量
namespace := "default"
replicas := 2
deployname := "ku"
image := "nginx:1.17"

// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
// kubeconfig加载失败就直接退出了
if err != nil {
panic(err)
}
// dynamic.NewForConfig实例化clientset对象
client, err := dynamic.NewForConfig(config)
if err != nil {
panic(err)
}
//使用schema的包带入gvr
deploymentRes := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
//定义结构化数据结构
deployment := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": deployname,
},
"spec": map[string]interface{}{
"replicas": replicas,
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{
"app": "demo",
},
},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app": "demo",
},
},

"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{
"name": "web",
"image": image,
"ports": []map[string]interface{}{
{
"name": "http",
"protocol": "TCP",
"containerPort": 80,
},
},
},
},
},
},
},
},
}

// 创建 Deployment
fmt.Println("创建 deployment...")
result, err := client.Resource(deploymentRes).Namespace(namespace).Create(context.TODO(), deployment, metav1.CreateOptions{})
if err != nil {
panic(err)
}

fmt.Printf("创建 deployment %q.\n", result.GetName())

// 列出 Deployments
prompt()
fmt.Printf("在命名空间中列出deployment %q:\n", apiv1.NamespaceDefault)
list, err := client.Resource(deploymentRes).Namespace(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err)
}
for _, d := range list.Items {
replicas, found, err := unstructured.NestedInt64(d.Object, "spec", "replicas")
if err != nil || !found {
fmt.Printf("Replicas not found for deployment %s: error=%s", d.GetName(), err)
continue
}
fmt.Printf(" * %s (%d replicas)\n", d.GetName(), replicas)
}

}
func prompt() {
fmt.Printf("--------------> 按回车键继续 <--------------.")
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
break
}
if err := scanner.Err(); err != nil {
panic(err)
}
fmt.Println()
}

⾸先加载 kubeconfig 配置信息, dynamic.NewForConfig 通过 kubeconfig 配置信息实例化 dynamicClient 对象,该对象⽤于管理 Kubernetes 的所有 Resource 的客户端,例如对 Resource 执⾏ CreateUpdateDeleteGetListWatchPatch 等操作

DiscoveryClient客户端对象

DiscoveryClient 是发现客户端,它主要⽤于发现 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息。 Kubernetes API Server ⽀持很多资源组、资源版本、资源信息,开发者在开发过程中很难记住所有信息,此时可以通过 DiscoveryClient 查看所⽀持的资源组、资源版本、资源信息。

kubectlapi-versionsapi-resources 命令输出也是通过 DiscoveryClient 实现的。另外, DiscoveryClient 同样在 RESTClient 的基础上进⾏了封装

DiscoveryClient 除了可以发现 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息,还可以将这些信息存储到本地,⽤于本地缓存( Cache ),以减轻对 Kubernetes API Server 访问的压⼒。在运⾏ Kubernetes 组件的机器上,缓存信息默认存储于 〜/.kube/cache〜/.kube/http-cache

类似于 kubectl 命令,通过 DiscoveryClient 列出 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息, DiscoveryClient Example 代码⽰例如下:

package main

import (
"flag"
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
"path/filepath"
)

func main() {

var kubeconfig *string

// home是家目录,如果能取得家目录的值,就可以用来做默认值
if home:=homedir.HomeDir(); home != "" {
// 如果输入了kubeconfig参数,该参数的值就是kubeconfig文件的绝对路径,
// 如果没有输入kubeconfig参数,就用默认路径~/.kube/config
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
// 如果取不到当前用户的家目录,就没办法设置kubeconfig的默认目录了,只能从入参中取
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}

flag.Parse()

// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)

// kubeconfig加载失败就直接退出了
if err != nil {
panic(err.Error())
}

// 新建discoveryClient实例
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)

if err != nil {
panic(err.Error())
}

// 获取所有分组和资源数据
APIGroup, APIResourceListSlice, err := discoveryClient.ServerGroupsAndResources()

if err != nil {
panic(err.Error())
}

// 先看Group信息
fmt.Printf("APIGroup :\n\n %v\n\n\n\n",APIGroup)

// APIResourceListSlice是个切片,里面的每个元素代表一个GroupVersion及其资源
for _, singleAPIResourceList := range APIResourceListSlice {

// GroupVersion是个字符串,例如"apps/v1"
groupVerionStr := singleAPIResourceList.GroupVersion

// ParseGroupVersion方法将字符串转成数据结构
gv, err := schema.ParseGroupVersion(groupVerionStr)

if err != nil {
panic(err.Error())
}

fmt.Println("*****************************************************************")
fmt.Printf("GV string [%v]\nGV struct [%#v]\nresources :\n\n", groupVerionStr, gv)

// APIResources字段是个切片,里面是当前GroupVersion下的所有资源
for _, singleAPIResource := range singleAPIResourceList.APIResources {
fmt.Printf("%v\n", singleAPIResource.Name)
}
}
}

运⾏以上代码,列出 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息。⾸先加载 kubeconfig 配置信息, discovery.NewDiscoveryClientForConfig 通过 kubeconfig 配置信息实例化 discoveryClient 对象,该对象是⽤于发现 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息的客户端

discoveryClient.ServerGroupsAndResources 函数会返回 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息(即 APIResourceList ),通过遍历 APIResourceList 输出信息。

获取 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息 Kubernetes API Server 暴露出 /api/apis 接⼝。 DiscoveryClient 通过 RESTClient 分别请求 /api/apis 接⼝,从⽽获取 Kubernetes API Server 所⽀持的资源组、资源版本、资源信息。其核⼼实现位于 ServerGroupsAndResources→ServerGroups 中,代码⽰例如下:

代码路径: vendor/k8s.io/client-go/discovery/discovery_client.go

// ServerGroups返回⽀持的组,以及⽀持的版本和⾸选版本等信息
func (d *DiscoveryClient) ServerGroups() (apiGroupList *metav1.APIGroupList, err error) {
// 获取在/api公开的groupVersions
v := &metav1.APIVersions{}
err = d.restClient.Get().AbsPath(d.LegacyPrefix).Do(context.TODO()).Into(v)
apiGroup := metav1.APIGroup{}
if err == nil && len(v.Versions) != 0 {
apiGroup = apiVersionsToAPIGroup(v)
}
if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
return nil, err
}

// 获取在/api公开的groupVersions
apiGroupList = &metav1.APIGroupList{}
err = d.restClient.Get().AbsPath("/apis").Do(context.TODO()).Into(apiGroupList)
if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
return nil, err
}
// 为了与v1.0服务器兼容,如果它是403或404,忽略并返回我们从/api得到的内容
if err != nil && (errors.IsNotFound(err) || errors.IsForbidden(err)) {
apiGroupList = &metav1.APIGroupList{}
}

// 如果不是空的,将从/api中检索到的组前置到列表中
if len(v.Versions) != 0 {
apiGroupList.Groups = append([]metav1.APIGroup{apiGroup}, apiGroupList.Groups...)
}
return apiGroupList, nil
}

⾸先, DiscoveryClient 通过 RESTClient 请求 /api 接⼝,将请求结果存放于 metav1.APIVersions 结构体中。然后,再次通过 RESTClient 请求 /apis 接⼝,将请求结果存放于 metav1.APIGroupList 结构体中。最后,将 /api 接⼝中检索到的资源组信息合并到 apiGroupList 列表中并返回

本地缓存的 DiscoveryClient

DiscoveryClient 可以将资源相关信息存储于本地,默认存储位置为 〜/.kube/cache〜/.kube/http-cache 。缓存可以减轻 client-goKubernetes API Server 的访问压⼒。默认每 10 分钟Kubernetes API Server 同步⼀次,同步周期较⻓,因为资源组、源版本、资源信息⼀般很少变动。本地缓存的 DiscoveryClient

DiscoveryClient 第⼀次获取资源组、资源版本、资源信息时,⾸先会查询本地缓存,如果数据不存在(没有命中)则请求 Kubernetes API Server 接⼝(回源), CacheKubernetes API Server 响应的数据存储在本地⼀份并返回给 DiscoveryClient 。当下⼀次 DiscoveryClient 再次获取资源信息时,会将数据直接从本地缓存返回(命中)给 DiscoveryClient 。本地缓存的默认存储周期为 10 分钟。代码⽰例如下

![DiscoveryClient.png](/img/Client-go 四种客户端交互对象/DiscoveryClient.png)

代码路径: vendor/k8s.io/client-go/discovery/cached/disk/cached_discovery.go

Istio 多集群管理

· 阅读需 7 分钟

多主架构安装

https://istio.io/v1.10/zh/docs/setup/install/multicluster/multi-primary/ ![istio跨网络多主集群架构](/img/Istio 多集群管理/istio跨网络多主集群架构.png)

第二个集群操作

kubeadm config view
#podSubnet: 10.244.0.0/16
#serviceSubnet: 10.96.0.0/12

# 修改一下网段,让两个k8s集群的相对应网段不唯一
kubeamd config view > config.yaml
kubectl get nodes
#k8s-master
#k8s-slave1
kubectl drain k8s-slave1 --ignore-daemonsets
kubectl delete nodes k8s-slave1
kubectl drain k8s-master --ignore-daemonsets
kubectl delete nodes k8s-master
rm -rf /etc/kubernetes/pki/*

# 修改配置文件
vim config.yaml
podSubnet: 10.250.0.0/16
serviceSubnet: 10.120.0.0/12

ssh root@192.168.26.92 #slave1
kubeadm reset
rm -rf /etc/kubernetes/pki/*

ssh root@192.168.26.91 #master
kubeadm reset
kubeadm --config=config.yaml init
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# slave1 加入集群:
kubeadm join 192.168.26.91:6443 xxxxxxxxxxxxxxxx

# 安装网络插件
kubectl apply -f calico.yaml

第一套集群

cd ./kube && cp config config.bak
vim config

apiVersion: v1
clusters:
- cluster:
certificate-authority-data: xxxxxxxx 第一套集群copy过来
server: https://192.168.26.81:6443
name: cluster1
- cluster:
certificate-authority-data: xxxxxxxx 第二套集群copy过来
server: https://192.168.26.91:6443
name: cluster2
contexts:
- context:
cluster: cluster1
namespace: ns1
user: admin1
name: cluster1
- context:
cluster: cluster2
namespace: default
user: admin2
name: cluster2
current-context: cluster1
kind: Config
preferences: {}
users:
- name: admin1
user:
client-certificate-data:
client-key-data:
- name: admin2
user:
client-certificate-data:
client-key-data:


cat ~/.kube/config.bak #分别两个集群的对应信息拿来
#certificate-authority-data
#client-certificate-data
#client-key-data

kubectl config get-contexts
* cluster1 cluster1 admin1 ns1
cluster2 cluster2 admin2 default

#查看切换集群
kubectl config use-context cluster2
kubectl get nodes
kubectl config use-context cluster1

#设置环境变量
export CTX_CLUSTER1=cluster1
export CTX_CLUSTER2=cluster2

# 在 Istio 安装包的顶层目录下,创建一个目录来存放证书和密钥:
# https://istio.io/v1.10/zh/docs/tasks/security/cert-management/plugin-ca-cert/
cd istio-1.10.3
mkdir -p certs
pushd certs

#生成根证书和密钥:
make -f ../tools/certs/Makefile.selfsigned.mk root-ca
# 将会生成以下文件:
root-cert.pem:生成的根证书
root-key.pem:生成的根密钥
root-ca.conf:生成根证书的 openssl 配置
root-cert.csr:为根证书生成的 CSR

#对于每个集群,为 Istio CA 生成一个中间证书和密钥
make -f ../tools/certs/Makefile.selfsigned.mk cluster1-cacerts #给cluster1创建
make -f ../tools/certs/Makefile.selfsigned.mk cluster2-cacerts #给cluster2创建

# 在每个集群中,创建一个私密 cacerts,包括所有输入文件 ca-cert.pem,ca-key.pem,root-cert.pem 和 cert-chain.pem。例如,在 cluster1 集群上:
kubectl create namespace istio-system #创建istio命名空间
kubectl create secret generic cacerts -n istio-system \
--from-file=cluster1/ca-cert.pem \
--from-file=cluster1/ca-key.pem \
--from-file=cluster1/root-cert.pem \
--from-file=cluster1/cert-chain.pem

#给cluster2也创建上
kubectl create --context=${CTX_CLUSTER2=cluster2} namespace istio-system
kubectl create --context=${CTX_CLUSTER2=cluster2} secret generic cacerts -n istio-system \
--from-file=cluster2/ca-cert.pem \
--from-file=cluster2/ca-key.pem \
--from-file=cluster2/root-cert.pem \
--from-file=cluster2/cert-chain.pem

跨网络多主架构安装

https://istio.io/v1.10/zh/docs/setup/install/multicluster/multi-primary_multi-network/

# 为 cluster1 设置缺省网络
kubectl --context="${CTX_CLUSTER1}" get namespace istio-system && \
kubectl --context="${CTX_CLUSTER1}" label namespace istio-system topology.istio.io/network=network1

#为 cluster2 设置缺省网络
kubectl --context="${CTX_CLUSTER2}" get namespace istio-system && \
kubectl --context="${CTX_CLUSTER2}" label namespace istio-system topology.istio.io/network=network2

# 为 cluster1 创建 Istio 配置文件:
cat <<EOF > cluster1.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
values:
global:
meshID: mesh1
multiCluster:
clusterName: cluster1
network: network1
EOF

vim cluster1.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
profile: demo
values:
global:
meshID: mesh1
multiCluster:
clusterName: cluster1
network: network1

#将配置文件应用到 cluster1
istioctl install --context="${CTX_CLUSTER1}" -f cluster1.yaml

# 为 cluster2 创建 Istio 配置文件:
cat <<EOF > cluster2.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
values:
global:
meshID: mesh1
multiCluster:
clusterName: cluster2
network: network2
EOF

vim cluster2.yaml
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
profile: demo
values:
global:
meshID: mesh1
multiCluster:
clusterName: cluster2
network: network2

#将配置文件应用到 cluster2
istioctl install --context="${CTX_CLUSTER2}" -f cluster2.yaml

## 启用端点发现
# 在 cluster2 中安装一个提供 cluster1 API Server 访问权限的远程 Secret。
istioctl x create-remote-secret \
--context="${CTX_CLUSTER1}" \
--name=cluster1 | \
kubectl apply -f - --context="${CTX_CLUSTER2}"
kubectl get secrets -n istio-system

# 在 cluster1 中安装一个提供 cluster2 API Server 访问权限的远程 Secret。
istioctl x create-remote-secret \
--context="${CTX_CLUSTER2}" \
--name=cluster2 | \
kubectl apply -f - --context="${CTX_CLUSTER1}"

验证安装结果

https://istio.io/v1.10/zh/docs/setup/install/multicluster/verify/

# 首先,在每个集群中创建命名空间 sample:
kubectl create --context="${CTX_CLUSTER1}" namespace sample
kubectl create --context="${CTX_CLUSTER2}" namespace sample

# 为命名空间 sample 开启 sidecar 自动注入:
kubectl label --context="${CTX_CLUSTER1}" namespace sample \
istio-injection=enabled
kubectl label --context="${CTX_CLUSTER2}" namespace sample \
istio-injection=enabled

# 在每个集群中创建 HelloWorld 服务:
kubectl apply --context="${CTX_CLUSTER1}" \
-f samples/helloworld/helloworld.yaml \
-l service=helloworld -n sample
kubectl apply --context="${CTX_CLUSTER2}" \
-f samples/helloworld/helloworld.yaml \
-l service=helloworld -n sample

部署 V1 版的 HelloWorld

# 把应用 helloworld-v1 部署到 cluster1:
kubectl apply --context="${CTX_CLUSTER1}" \
-f samples/helloworld/helloworld.yaml \
-l version=v1 -n sample

# 确认 helloworld-v1 pod 的状态:
kubectl get pod --context="${CTX_CLUSTER1}" -n sample -l app=helloworld
NAME READY STATUS RESTARTS AGE
helloworld-v1-86f77cd7bd-cpxhv 2/2 Running 0 40s

#等待 helloworld-v1 的状态最终变为 Running 状态:

等待 helloworld-v1 的状态最终变为 Running 状态:

#把应用 helloworld-v2 部署到 cluster2:
kubectl apply --context="${CTX_CLUSTER2}" \
-f samples/helloworld/helloworld.yaml \
-l version=v2 -n sample

# 确认 helloworld-v2 pod 的状态:
kubectl get pod --context="${CTX_CLUSTER2}" -n sample -l app=helloworld
NAME READY STATUS RESTARTS AGE
helloworld-v2-758dd55874-6x4t8 2/2 Running 0 40s

# 等待 helloworld-v2 的状态最终变为 Running 状态:

部署 Sleep

# 把应用 Sleep 部署到每个集群:
kubectl apply --context="${CTX_CLUSTER1}" \
-f samples/sleep/sleep.yaml -n sample
kubectl apply --context="${CTX_CLUSTER2}" \
-f samples/sleep/sleep.yaml -n sample

# 确认 cluster1 上 Sleep 的状态:
kubectl get pod --context="${CTX_CLUSTER1}" -n sample -l app=sleep
NAME READY STATUS RESTARTS AGE
sleep-754684654f-n6bzf 2/2 Running 0 5s

# 等待 Sleep 的状态最终变为 Running 状态:
# 确认 cluster2 上 Sleep 的状态:
kubectl get pod --context="${CTX_CLUSTER2}" -n sample -l app=sleep
NAME READY STATUS RESTARTS AGE
sleep-754684654f-dzl9j 2/2 Running 0 5s

# 等待 Sleep 的状态最终变为 Running 状态:

验证跨集群流量

要验证跨集群负载均衡是否按预期工作,需要用 Sleep pod 重复调用服务 HelloWorld。 为了确认负载均衡按预期工作,需要从所有集群调用服务 HelloWorld

# 从 cluster1 中的 Sleep pod 发送请求给服务 HelloWorld:
kubectl exec --context="${CTX_CLUSTER1}" -n sample -c sleep \
"$(kubectl get pod --context="${CTX_CLUSTER1}" -n sample -l \
app=sleep -o jsonpath='{.items[0].metadata.name}')" \
-- curl helloworld.sample:5000/hello

# 重复几次这个请求,验证 HelloWorld 的版本在 v1 和 v2 之间切换:
Hello version: v2, instance: helloworld-v2-758dd55874-6x4t8
Hello version: v1, instance: helloworld-v1-86f77cd7bd-cpxhv
...

# 现在,用 cluster2 中的 Sleep pod 重复此过程:
kubectl exec --context="${CTX_CLUSTER2}" -n sample -c sleep \
"$(kubectl get pod --context="${CTX_CLUSTER2}" -n sample -l \
app=sleep -o jsonpath='{.items[0].metadata.name}')" \
-- curl helloworld.sample:5000/hello

# 重复几次这个请求,验证 HelloWorld 的版本在 v1 和 v2 之间切换:
Hello version: v2, instance: helloworld-v2-758dd55874-6x4t8
Hello version: v1, instance: helloworld-v1-86f77cd7bd-cpxhv
...

# 恭喜! 你已成功的在多集群环境中安装、并验证了 Istio!。。。