基于 client-go 实现 ServiceAccount 创建并实现 rolebind

基于 client-go 实现 ServiceAccount 创建并实现 rolebind

前言:

在公司中由于采用的并非原生后台操作 K8S ,而是通过使用开源的 PAAS 平台向用户提供使用,但是有些业务部门需要通过 ServiceAccout(下面统称为 SA) 创建之后生成的 Secret 中的 ca.crt 以及 token 来实现对自身业务操作 K8S 。

并且在研发该工具之前一直都是通过手动认为创建的方式来实现该功能,但是有几次在同事创建好 SA 之后就给到了业务部门,结果发现创建出来的 SA 并不能正常使用,而且我们都知道通过命令的方式来进行 SA 的验证相对是麻烦的,比如需要将 ca.crt 以及 token 信息进行 base64 编码之后在调用 ApiServer 的地址才能够进行访问

并且该工具还需要将一个 SA 对多个 NS 下进行绑定:

内部需要对一个 K8S cluster 对外接入的 Token 进行精细化管控,就出现了不少需求侧要求对 K8S 内的多个 Namespace 使用各自独立 Token 管理和访问权限。

  • 业务组 A:使用 TokenA 要对 Namespace A, B, C 控制
  • 业务组 B:使用 TokenB 要对 Namespace E, F, G 控制

需求拆分

  • 每一个业务组都有自己独立且唯一的 Token 访问 K8S
  • 每一个业务组都有自己独立的需要管理和维护的多个 Namespace
  • 每一个业务组都需要自己的 Namespace 与 自己的 Token 关联与绑定
  • 每一个业务组的自有访问策略组内共享,业务组之间独立

概念解析

那么在 RBAC 的世界里面有下面几个重要的概念:

  • Role: 针对特定 Namespace 内 apiGroup 设定访问控制规则
  • RoleBinding: 针对特定 Namespace 内,将 Role 或 ClusterRole 与某一个 ServiceAccount 或者其他账号绑定
  • ClusterRole: K8S 范围内 apiGroup 设定访问控制规则
  • ClusterRoleBinding: K8S 范围内,将 ClusterRole 与某一个 ServiceAccount 或者其他账号绑定

这里有几个关键知识点要提及

  • Role RoleBinding: 是针对某一个特定的 Namespace 做约束
  • ClusterRole ClusterRoleBinding: 是 cluster 范围内做约束,不受某一个 Namespace 影响
  • K8S Api Group: 分为 Namespace 级和非 Namespace 级。(这里很重要)
  • RoleBinding 可以绑定 Role 和 ClusterRole,而 ClusterRoleBinding 只能绑定 ClusterRole

完整项目地址:https://github.com/As9530272755/Tool-set/tree/master/sactl

工具设计:

对于该工具我的设计功能有以下几点

  1. 能够验证 SA 是否创建成功
    • 命令使用格式如下
      sactl check -n NS SaName 从而通过 sa 获取到对应的资源信息,这里我暂定为获取 Pod 信息
  2. 通过子命令自动创建 SA
    • 命令使用格式如下:
      sactl create -n ns SaName
      同时调用 kubectl create rolebinding 命令,默认传参实现 SA 的创建同时也将该 sa 与 对应的 admin role 进行 bind,admin(该 role 为 PAAS 平台在页面创建 NS 的时候自动创建的一个 role,拥有该 NS 下所有权限)
  3. 需要链接 K8S 配置、日志系统、以及命令行 ctl

功能开发:

1 目录结构和 cobra 使用

[16:08:45 root@go sactl]#tree 
.
├── cmd
│   ├── check.go
│   ├── create.go
│   └── root.go
├── config
│   └── config.go
├── controller
│   ├── check.go
│   └── create.go
├── etc
│   ├── config
│   └── saConfig.yaml
├── go.mod
├── go.sum
├── LICENSE
├── linK8S
│   └── linkK8s.go
├── log
├── logs
│   └── logs.go
├── main.go
└── model
    └── SA_Role.go

可以看到有一下几个目录

  • cmd 主要实现命令行工具的调用代码
  • config 主要实现配置文件代码
  • controller 主要处理核心逻辑代码
  • etc 主要存放配置文件
  • linK8S 链接K8S
  • log 存放日志
  • logs 编写日志处理代码
  • model 模块代码

1.通过 cobra 命令创建 sactl 命令行工具, cobra init 命令来初始化 CLI 应用的脚手架:

$ cobra init --pkg-name sactl

2.初始化之后就会生成下面的目录结构

$ tree .
.
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go

1 directory, 5 files

cobra 对应的用法参考我以前写的文章 http://39.105.137.222:8089/?p=1974#header-id-3

2 model 模块开发

该模块主要是用于在创建 SA 以及 RoleBind 的时候使用

package model

type SA struct {
    NameSpace string
    Name      string
    API       string
}

func NewSa(namespace, name, api string) *SA {
    return &SA{
        NameSpace: namespace,
        Name:      name,
        API:       api,
    }
}

type RoleBind struct {
    NameSpace string
    Name      string
}

func NewRoleBind(namespace, name string) *RoleBind {
    return &RoleBind{
        namespace,
        name,
    }
}

type Secrets struct {
    Token     string
    Namespace string
    API       string
}

func NewSecrets(namespace, api, token string) *Secrets {
    return &Secrets{
        Token:     token,
        Namespace: namespace,
        API:       api,
    }
}

type AddRole struct {
    Role      string
    RoleBind  string
    Namespace []string
}

func NewAddRole(roleBind, role string, namespace []string) *AddRole {
    return &AddRole{
        role,
        roleBind,
        namespace,
    }
}

3 config 模块开发

我们先开发 config 模块然后就可以通过配置对应的功能获取到 K8S config 之后在链接 K8S 进行操作

在该模块中使用到了 viper 包,该包的使用我也写过对应的文章开一点击该链接,viper 使用

1.编写代码

// 该模块主要功能就是定义日志并放回 K8S 的 config 路径

package config

import (
    "github.com/spf13/viper"
)

// 用于获取 K8S config
type KubeConfig struct {
    ConfigPath string `mapstructure:"path"`
}

// 用于定义日志详细信息
type Log struct {
    FileName string `mapstructure:"filename"`
    Max_age  int    `mapstructure:"max_age"`
    Max_size int    `mapstructure:"max_size"`
    Compress bool   `mapstructure:"compress"`
    Level    string `mapstructure:"level"`
}

// 结构体嵌套从而直接返回 *Optins 
type Optins struct {
    KubeConfig KubeConfig `mapstructure:"kubeconfig"`
    Log        Log        `mapstructure:"log"`
}

// 指定配置文件路径,并返回 optins 结构体
func ParseConfig() (*Optins, error) {
    conf := viper.New()
    // 指定我们的配置文件路径
    conf.SetConfigFile("./etc/saConfig.yaml")

    if err := conf.ReadInConfig(); err != nil {
        return nil, err
    }

    // 解析并返回
    optins := &Optins{}
    if err := conf.Unmarshal(&optins); err != nil {
        return nil, err
    }
    return optins, nil
}

2.编写配置文件

kubeconfig:
# K8S config 路径
  path: ./etc/config

log:
  filename: log/sactl.log
  max_age: 30
  max_size: 10
  max_backups: 14
  compress: false
  level: info

4 编写 logs 模块

这里编写 logs 功能,因为在该功能中有对应的 error 处理

该包的使用我也写过对应的文章开一点击该链接:logrus 日志处理工具

package logs

import (
    "github.com/sirupsen/logrus"
    "gopkg.in/natefinch/lumberjack.v2"
    "sactl/config"
)

// 记录日志
func Logs() {

    // 调用 config.ParseConfig() ,返回 optins
    optins, err := config.ParseConfig()
    if err != nil {
        ErrInfo("Logs optins", "fail", err)
    }

    logger := lumberjack.Logger{
        // 基于 optins 来填写日志的信息
        Filename: optins.Log.FileName,
        MaxSize:  optins.Log.Max_size,
        MaxAge:   optins.Log.Max_age,
        Compress: optins.Log.Compress,
    }

    defer logger.Close()
    // 放入我们通过配置文件中获取的设置,来生成日志信息
    logrus.SetOutput(&logger)

      // 转换日志级别
    level, err := logrus.ParseLevel(optins.Log.Level)
    if err != nil {
        ErrInfo("log Level", "fail", err)
    }

     // 设置日志级别
    logrus.SetLevel(logrus.Level(level))
    logrus.SetReportCaller(true)
    //logrus.SetFormatter(&logrus.JSONFormatter{})
}

// error 和 info 处理函数
func ErrInfo(action string, sample interface{}, err error) {
    if err != nil {
        logrus.WithFields(
            logrus.Fields{
                "Action":     action,
                "Sample":     sample,
                "Error_Info": err,
            }).Error("SaCtl")
    } else {
        logrus.WithFields(
            logrus.Fields{
                "action": action,
                "sample": sample,
                "Info":   err,
            }).Info("SaCtl")
    }
}

5 link_K8S 模块开发

在 config 中我们已经获取了对应的 K8S config 路径,该文件是链接 K8S 的核心文件,也就是说当我们有了该文件之后才能够链接 K8S

package linK8S

import (
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

// 获取配置文件中 K8S config 信息,返回 ClientSet
func Link_K8s(configPath string) (*kubernetes.Clientset, error) {
    config, err := clientcmd.BuildConfigFromFlags("", configPath)
    if err != nil {
        return nil, err
    }

    clientSet, err := kubernetes.NewForConfig(config)
    if err != nil {
        return nil, err
    }

    // 返回
    return clientSet, nil
}

6 编写 controller 模块

该模块有一下两个功能一个是 check SA 是否正常,另外一个是 create SA 并实现 rolebind

6.1 check.go 模块

我们都知道在一个 SA 之后就会指定生成一个 secrets ,如下:

# 创建 sa
16:01:42 root@master test]#kubectl create sa test
serviceaccount/test created

# 生成 secrets
[17:03:36 root@master test]#kubectl get secrets 
NAME                  TYPE                                  DATA   AGE
default-token-rd7kc   kubernetes.io/service-account-token   3      2d1h
test-token-s7mt4      kubernetes.io/service-account-token   3      9s       # 创建 SA 自动生成的 secrets

# 也就是我们需要获取他的 ca 和 token 才能够调用 K8S api 进行操作
[17:03:45 root@master test]#kubectl get secrets test-token-s7mt4 -o yaml
apiVersion: v1
data:
  ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJek1ESXdOakEzTXprME5Gb1hEVE16TURJd016QTNNemswTkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBT1kvCk9qa0VFMTFra3I4TzJrdlFrd3ZUUTlsVjZ0aFRzSDBOcnU2Sk9YUVBKWXpOS0U3T1RDd0dUNnNUbVZLOUJveG4KNmREUTBEd29rYWxVdTVjZC9LV09lUHVHTlNpK3J5aHVXRWxGdlR4VndIQkk4akFua2JkWFdRZWFMRXZISFVVNApiYUlxSjlrTldXcWFMdXFWWmZuUWovL2pUSlVVNGFKYnpzcUhYU3M1K0RqQTcxOUJWY09FdFVqQXNKellQVHVqCnE4SzFyeXJkYitJdExXMkhYSkdPZEVxMTN4VkhVSHRnMVQveVNSeGFYbEFFN1NpTnh1MzluYUZKY3dLNmllc1QKQ2gzb1ZGejJLaFc0VUVWRVlnQlpXSUZMMWh3VkVaV1cxeXJiblQyNjVWU25OeWtKcjAxY1NNaHNyNVpyTWJmVworTkd2aXNWcldvZTJVZTVQdWtNQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJTnFBWXA1TU42bUFza2hyWW9ldjM3Y25CRXJNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQTJQa3E5STFhMVZORmx2R3JsRQorNXVxUHExYnJFL0cweXEzMVpXSHRvdHJ3Q1poZmY5U1E0YzQ0cHYyVmZnMzBKQkN5N1FLS0dZV0ZvS29xeG8rCmhobFAzd05HMmp2WWFTem82TGV2N01RMEFTdktmcklsZUJxbnFWdmNON1VpajFpK1ZrMC9nM2hIbXV1akdmckUKS0pwMFVaSTk5VjZZUUJCUUtDTjcvVjNXQ3ptbkIxOWtuT2ZnNGdtSXJTRVBqa29XbUd6d3VzTUc2UERmTnNVVwppU2hpb1BHa2FqNVZaam9TL0FtTFpzb1lwQlNKVFZQcnY3MGdzUXErQ3U5K2ZBczJ3WkNjZG81b3Jwa3RxQU5SCmtSNk9zQS9tNmd6c1lQczZLc3dCc0llUml6L3dzWnNXWXNkWjJQcWViY2lkQjl0bm80Znd0d3pXTFNMRU5YNmsKcitFPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
  namespace: ZGVmYXVsdA==
  token: ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklreGxYMkpSV0VGRE56UkNObUZvZEhwMlZ6WkJhbkJ3TUZsRFlXeFlZazFqTVMxQ1ZqTXlWM2xmT1hNaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUprWldaaGRXeDBJaXdpYTNWaVpYSnVaWFJsY3k1cGJ5OXpaWEoyYVdObFlXTmpiM1Z1ZEM5elpXTnlaWFF1Ym1GdFpTSTZJblJsYzNRdGRHOXJaVzR0Y3pkdGREUWlMQ0pyZFdKbGNtNWxkR1Z6TG1sdkwzTmxjblpwWTJWaFkyTnZkVzUwTDNObGNuWnBZMlV0WVdOamIzVnVkQzV1WVcxbElqb2lkR1Z6ZENJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZ5ZG1salpTMWhZMk52ZFc1MExuVnBaQ0k2SWpJMVltRTBZMkZqTFRjME9UWXRORGcxT1MwNE1ESmpMVEUzT0ROaE1XVXlaRGN3TUNJc0luTjFZaUk2SW5ONWMzUmxiVHB6WlhKMmFXTmxZV05qYjNWdWREcGtaV1poZFd4ME9uUmxjM1FpZlEuRXhjcVdBUXVxYkswS1dhME5zN2I4TVBKT08xVzFaWUtEdTFjcFZmRWtlb21mRUY4dXVTNHFuR2R5T2t5MG5NN0NnQTM1X29sS0szVUdlWkVYckltMjNsNC1sQUxneW5WY0FHeng4LXhZMTRueHp1NUlfa3ZOVHdGWHRvSTl1VGpWalBqb1dzYmJlMmtaOHhRQXdsQkY1QXVFWTdWbS1hUV95aTRHMTd0cWVnMlNiMHNtaUQxRWtuM0p3MTNpalpGODI1UmJIajJ2M0FHNEJrN0duWENpdlljdFYtek9XaTFuSFk4MllCVks4ZlZqOUppOTJaME1xdUNmcEVmbThfT1Q4bUtCWWhtdU90U2pBdmZlS0o1OERCUENva3Q0RlNIb0l0UDA2Z1F1cGJ0NU5OTk5td2piZWI2MTBQVlZZUktGdEVDTUhKbE5KMTg1V0VMaHdSUnlR
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: test
    kubernetes.io/service-account.uid: 25ba4cac-7496-4859-802c-1783a1e2d700
  creationTimestamp: "2023-02-08T09:03:36Z"
  name: test-token-s7mt4
  namespace: default
  resourceVersion: "106017"
  uid: 31b50b40-fe68-416f-a1a8-c3674f1449f2
type: kubernetes.io/service-account-token

编写代码

package controller

import (
    "context"
    "fmt"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "sactl/config"
    "sactl/linK8S"
    "sactl/logs"
    "sactl/model"
    "time"
)

// 定义下面需要用到的变量,CA Token Secrets
var (
    CA        []byte
    Token     []byte
    ClientSet *kubernetes.Clientset
    Secrets   string
)

// 该函数用于获取 secrets , newSA 是通过 cobra 的 cmd 中进行传参得到
func Get_Secrets(newSA *model.SA) {
    // 基于 config 获取到对应的 K8S config 路径
    config, err := config.ParseConfig()
    if err != nil {
        fmt.Println("Error from server Config:", err)
        logs.ErrInfo("GetSecrets", "Config Fail", err)
    }

    // 并得到 clientSet
    ClientSet, err = linK8S.Link_K8s(config.KubeConfig.ConfigPath)
    if err != nil {
        fmt.Println("Error from server ClientSet:", err)
        logs.ErrInfo("GetSecrets", "ClientSet", err)
    }

    // 获取对应的 SA 信息,并且返回一个 []
    saSet := ClientSet.CoreV1().ServiceAccounts(newSA.NameSpace)
    saList, err := saSet.List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        fmt.Println("Error from server SAList:", err)
        logs.ErrInfo("GetSecrets 函数", "saList 报错", err)
    }

    // for 判断 sa.Name 是否和 nweSA.NAME 相同,如果相同那么就是我们在 cmd 中需要查找的 SA 
    for _, sa := range saList.Items {
        if sa.Name == newSA.Name {
            // 索引[0] 就是对应 secrets,并赋值给 Secrets
            Secrets = sa.Secrets[0].Name
        }
    }

}

// 获取到了 Secrets 之后就需要获取 token 和 CA
func Get_Token(newSA *model.SA) {
    // 获取指定的 Namespace 下的 SecretsSet
    secretsSet := ClientSet.CoreV1().Secrets(newSA.NameSpace)

    // 获取 Secrets
    se, err := secretsSet.Get(context.TODO(), Secrets, metav1.GetOptions{})
    if err != nil {
        fmt.Println("Error from server Get_Token:", err)
        logs.ErrInfo("Get_Token", "Secrets", err)
    }

    // se.Data 是一个 map 类型,所以这里通过 key-value 的方式来获取对应的 CA 和 token
    CA = se.Data["ca.crt"]
    Token = se.Data["token"]
}

// 通过 token 和 CA 获取资源用于验证
func Get_RS(newSA *model.SA) {
    // 通过CA 来获取 client
    tlsClientConfig := rest.TLSClientConfig{
        CAData: CA,
    }

    // 通过 SA 的方式来创建 config
    config := rest.Config{
        Host:            newSA.API,
        BearerToken:     string(Token),
        TLSClientConfig: tlsClientConfig,
        Timeout:         20 * time.Second,
    }

    // 检查是否链接 K8S 
    RSclientSet, err := kubernetes.NewForConfig(&config)
    if err != nil {
        fmt.Println("Error from server (NotFound) 基于 token CA 检查失败:", err)
        logs.ErrInfo("GetRS", "基于 token CA 检查失败", err)
    }

    // 如果链接成功获取对应的 NS 下的 POD
    pods := RSclientSet.CoreV1().Pods(newSA.NameSpace)
    podsList, err := pods.List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        fmt.Println("Error from server (NotFound):", err)
        logs.ErrInfo("GetRS podsList", "基于 token CA 获取 POD 失败", err)
    }

    // 将其全部输出
    for _, pod := range podsList.Items {
        fmt.Printf("NameSpace:%s\tPod:%s\n", pod.Namespace, pod.Name)
    }
}

以上就是 check 功能的开发

6.2 create.go 模块

现在我们开始编写创建 sa 并实现 rolebind 功能

package controller

import (
    "context"
    "fmt"
    v1 "k8s.io/api/core/v1"
    rbacv1 "k8s.io/api/rbac/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    config2 "sactl/config"
    "sactl/linK8S"
    "sactl/logs"
    "sactl/model"
)

// create ServiceAccount
// 基于通过 cobra 的 cmd 中进行传参得到 newSa
func Create_SA(newSa *model.SA) {

    // 获取 K8S client SET
    config, err := config2.ParseConfig()
    if err != nil {
        logs.ErrInfo("Create_SA", "create SA config error!", err)
    }
    clientSet, err := linK8S.Link_K8s(config.KubeConfig.ConfigPath)
    if err != nil {
        logs.ErrInfo("Create_SA", "create SA clientSet error!", err)
    }

    // 创建 SA
    sa, err := clientSet.CoreV1().ServiceAccounts(newSa.NameSpace).Create(context.TODO(), &v1.ServiceAccount{
        ObjectMeta: metav1.ObjectMeta{
            Name:      newSa.Name,
            Namespace: newSa.NameSpace,
        },
    }, metav1.CreateOptions{})
    if err != nil {
        logs.ErrInfo("Create_SA", "创建 SA Create 时操作失败", err)
        fmt.Println("Failed to create ServiceAccounts!\n", err)
    } else {
        fmt.Printf("%v ServiceAccounts Create Success!\n", sa.Name)
    }
}

// kubectl create sa and rolebind admin role
func Role_Bind(newRoleBind *model.RoleBind) {
    config, err := config2.ParseConfig()
    if err != nil {
        logs.ErrInfo("Create_SA", "创建 SA config 位置失败", err)
    }
    clientSet, err := linK8S.Link_K8s(config.KubeConfig.ConfigPath)
    if err != nil {
        logs.ErrInfo("Create_SA", "创建 SA clientSet 位置失败", err)
    }

    // 创建 rolebind
    roleBind, err := clientSet.RbacV1().RoleBindings(newRoleBind.NameSpace).Create(context.TODO(), &rbacv1.RoleBinding{
        ObjectMeta: metav1.ObjectMeta{
            Name:      newRoleBind.Name,
            Namespace: newRoleBind.NameSpace,
        },
        Subjects: []rbacv1.Subject{
            {Kind: "ServiceAccount",
                Name:      newRoleBind.Name,
                Namespace: newRoleBind.NameSpace},
        },
        RoleRef: rbacv1.RoleRef{
            APIGroup: "rbac.authorization.k8s.io",
            Kind:     "Role",
            Name:     "admin",    // 这里我写死为 admin role 后期如果需要自定义即可自行更改
        },
    }, metav1.CreateOptions{})
    if err != nil {
        logs.ErrInfo("roleBind", "create roleBind error!", err)
        fmt.Println("Failed to create roleBind!\n", err)
    } else {
        fmt.Printf("%v Rolebinding Create Success!\n", roleBind.Name)
    }
}

6.3 token_get.go 模块开发

该功能不再需要 client 通过 K8S config 链接,而是可以通过 SA 所生成的 secrets 中的 token 进行链接即可

package controller

import (
    "context"
    "fmt"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "sactl/logs"
    "sactl/model"
)

// 通过 cmd 模块中的 get.go 获取到用户输入的 secrets 结构体
func TokenGet(newSecrets *model.Secrets) {
    // 通过用户输入 token 获取
    token := newSecrets.Token

    // 基于 rest 链接,得到 rc
    rc := &rest.Config{
        // API 用户输入
        Host: newSecrets.API,
        // 开启 TLS 认证
        TLSClientConfig: rest.TLSClientConfig{
            Insecure: true,
        },
        // BearerToken 用户输入
        BearerToken: token,
    }

    // 基于 rc 链接并得到,cs 就是一个 clientSet
    cs, err := kubernetes.NewForConfig(rc)
    if err != nil {
        logs.ErrInfo("cs 连接失败", "kubernetes.NewForConfig(rc)", err)
    }

    // 基于不同资源选择
    //if newSecrets.Rs == "pod" {
    //  pod, err := cs.CoreV1().Pods(newSecrets.Namespace).List(context.TODO(), metav1.ListOptions{})
    //  if err != nil {
    //      fmt.Println("token 验证失败!")
    //  } else {
    //      fmt.Println("token 验证成功!")
    //  }
    //  for _, p := range pod.Items {
    //      fmt.Println(p.Namespace, p.Name)
    //  }
    //} else if newSecrets.Rs == "deployment" {
    //  dels, err := cs.AppsV1().Deployments(newSecrets.Namespace).List(context.TODO(), metav1.ListOptions{})
    //  if err != nil {
    //      logs.ErrInfo("del", "errinfo ", err)
    //  }
    //  for _, del := range dels.Items {
    //      fmt.Println(del.Name)
    //  }
    //} else {
    //  fmt.Println("输入资源错误")
    //}

    // 获取 POD
    pod, err := cs.CoreV1().Pods(newSecrets.Namespace).List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        logs.ErrInfo("TokenGet token pod 验证", "cs.corev1.pod", err)
        fmt.Println("token 验证失败!")
        return
    } else {
        fmt.Println("token 验证成功!")
    }
    for _, p := range pod.Items {
        fmt.Printf("namespace:%s\tpod:%s\n", p.Namespace, p.Name)
    }
}

6.4 add_rolebind.go 模块

该模块主要想实现的功能就是在现有的 SA 上将其添加到别的 NS 中进行 RoleBindg 操作,从而实现单个 SA 对多个 NS 进行纳管

package controller

import (
    "context"
    "fmt"
    rbacv1 "k8s.io/api/rbac/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    config2 "sactl/config"
    "sactl/linK8S"
    "sactl/logs"
    "sactl/model"
)

// kubectl create sa and rolebind role
func Add_RoleBind(new_RoleBindg *model.AddRole) {
    config, err := config2.ParseConfig()
    if err != nil {
        logs.ErrInfo("Create_SA", "创建 SA config 位置失败", err)
    }
    clientSet, err := linK8S.Link_K8s(config.KubeConfig.ConfigPath)
    if err != nil {
        logs.ErrInfo("Add_RoleBind", "创建 Add_RoleBind clientSet 位置失败", err)
    }

    // 遍历用户输入的多个 NS
    for i := 0; i < len(new_RoleBindg.Namespace); i++ {
        // 判断是否输入的 role 已经存在,如果为空就表示 ns 下不存在对应的 role,并且进入下一个循环
        _, err := clientSet.RbacV1().Roles(new_RoleBindg.Namespace[i]).Get(context.TODO(), new_RoleBindg.Role, metav1.GetOptions{})
        if err != nil {
            logs.ErrInfo(new_RoleBindg.Role, new_RoleBindg.Namespace[i], err)
            fmt.Printf("错误! namespace:%s 下不存在 role:%s!\n", new_RoleBindg.Namespace[i], new_RoleBindg.Role)
            continue

            // 如果用户输入的 role 存在那么就判断 rolebind 是否存在,如果 rolebind 不存在那么就执行并创建 rolebind
        } else if _, err := clientSet.RbacV1().RoleBindings(new_RoleBindg.Namespace[i]).Get(context.TODO(), new_RoleBindg.RoleBind, metav1.GetOptions{}); err != nil {
            if err != nil {
                rolebind, err := clientSet.RbacV1().RoleBindings(new_RoleBindg.Namespace[i]).Create(context.TODO(), &rbacv1.RoleBinding{
                    ObjectMeta: metav1.ObjectMeta{
                        Name:      new_RoleBindg.RoleBind,
                        Namespace: new_RoleBindg.Namespace[i],
                    },
                    Subjects: []rbacv1.Subject{
                        {Kind: "ServiceAccount",
                            Name:      new_RoleBindg.RoleBind,
                            Namespace: "kube-system"},        // 指定写死为 kube-system NS 后期手动更改源码
                    },
                    RoleRef: rbacv1.RoleRef{
                        APIGroup: "rbac.authorization.k8s.io",
                        Kind:     "Role",
                        Name:     new_RoleBindg.Role,
                    },
                }, metav1.CreateOptions{})

                if err != nil {
                    logs.ErrInfo("roleBind", "create roleBind error!", err)
                    fmt.Println("Failed to create roleBind!\n", err)
                    return
                } else {
                    fmt.Printf("%v Rolebinding Create Success!\n", rolebind.Name)
                }
            }
            // 否则提示用户 rolebind 已经存在
        } else {
            logs.ErrInfo(new_RoleBindg.Role, new_RoleBindg.Namespace[i], err)
            fmt.Printf("错误! namespace:%s 下已存在 RoleBindg:%s!\n", new_RoleBindg.Namespace[i], new_RoleBindg.RoleBind)
            continue
        }
    }
}

7 编写 cmd 模块

该模块主要是用到 cobra 包

cobra 对应的用法参考我以前写的文章 http://39.105.137.222:8089/?p=1974#header-id-3

7.1 check.go 模块

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "sactl/controller"
    "sactl/model"
)

// checkCmd represents the check command
var checkCmd = &cobra.Command{
    Use:   `check`,
    Short: "该子命令用于验证 SA",
    Long: `验证示例:
sactl check -n namespace sa api`,
    Run: func(cmd *cobra.Command, args []string) {
        if len(args) == 0 || len(args) != 3 {
            fmt.Println(`语法错误:
查看帮助: sactl check -h `)
            return
        }
        // 判断是否需要 -n 指定 NS
        fstatus, _ := cmd.Flags().GetBool("namespace")
        if fstatus {
            appoint_NS(args)
        } else {
            check_SA(args)
        }
    },
}

func init() {
    rootCmd.AddCommand(checkCmd)
    checkCmd.Flags().BoolP("namespace", "n", false, "指定 namespace 验证 SA,默认情况下访问 default NS")
    // Here you will define your flags and configuration settings.

    // Cobra supports Persistent Flags which will work for this command
    // and all subcommands, e.g.:
    // checkCmd.PersistentFlags().String("foo", "", "A help for foo")

    // Cobra supports local flags which will only run when this command
    // is called directly, e.g.:
    // checkCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// 接收用户输入的 args 
func check_SA(args []string) {
    // 如果不输 -n ns 就默认在 default 下,并通过工厂函数初始化 NewSA 结构体
    newSA := model.NewSa("default", args[0], args[1])

    // 传入 newSA
    controller.Get_Secrets(newSA)
    controller.Get_Token(newSA)
    controller.Get_RS(newSA)
}

// 接收用户输入的 args ,该函数用于 -n 指定 NS 时使用
func appoint_NS(args []string) {
    newSA := model.NewSa(args[0], args[1], args[2])
    controller.Get_Secrets(newSA)
    controller.Get_Token(newSA)
    controller.Get_RS(newSA)
}

7.2 create.go 模块

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
    "fmt"
    "sactl/controller"
    "sactl/model"

    "github.com/spf13/cobra"
)

// createCmd represents the create command
var createCmd = &cobra.Command{
    Use:   "create",
    Short: "该子命令用于创建 SA",
    Long: `验证示例:
sactl create -n namespace sa`,
    Run: func(cmd *cobra.Command, args []string) {
        if len(args) == 0 || len(args) != 2 {
            fmt.Println(`语法错误:
查看帮助: sactl create -h `)
            return
        }
        fstatus, _ := cmd.Flags().GetBool("namespace")
        if fstatus {
            Create_Sa_Rolebind(args)
        } else {
            fmt.Println(`语法错误:
查看帮助: sactl create -h `)
            return
        }
    },
}

func init() {
    rootCmd.AddCommand(createCmd)
    createCmd.Flags().BoolP("namespace", "n", false, "指定 namespace 创建 SA,实现 rolebind")
    // Here you will define your flags and configuration settings.

    // Cobra supports Persistent Flags which will work for this command
    // and all subcommands, e.g.:
    // createCmd.PersistentFlags().String("foo", "", "A help for foo")

    // Cobra supports local flags which will only run when this command
    // is called directly, e.g.:
    // createCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

func Create_Sa_Rolebind(args []string) {
    newSA := model.NewSa(args[0], args[1], "")
    newRoleBind := model.NewRoleBind(args[0], args[1])

    controller.Create_SA(newSA)
    controller.Role_Bind(newRoleBind)
}

7.3 get.go 模块

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
    "fmt"
    "sactl/controller"
    "sactl/model"

    "github.com/spf13/cobra"
)

// getCmd represents the get command
var getCmd = &cobra.Command{
    Use:   "get",
    Short: "该子命令用于验证 Token 可用性!",
    Long: `验证示例:
sactl get -n namespace api token`,
    Run: func(cmd *cobra.Command, args []string) {
        if len(args) == 0 || len(args) != 3 {
            fmt.Println(`语法错误:
查看帮助: sactl get -h `)
            return
        }
        fstatus, _ := cmd.Flags().GetBool("namespace")
        if fstatus {
            get_Secrets(args)
        } else {
            fmt.Println(`语法错误:
查看帮助: sactl get -h `)
            return
        }
    },
}

func init() {
    rootCmd.AddCommand(getCmd)
    getCmd.Flags().BoolP("namespace", "n", false, "指定 namespace 实现基于 token 认证")
    // Here you will define your flags and configuration settings.

    // Cobra supports Persistent Flags which will work for this command
    // and all subcommands, e.g.:
    // getCmd.PersistentFlags().String("foo", "", "A help for foo")

    // Cobra supports local flags which will only run when this command
    // is called directly, e.g.:
    // getCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

func get_Secrets(args []string) {
    namespace := args[0]
    api := args[1]
    token := args[2]
    secrets := model.NewSecrets(namespace, api, token)
    controller.TokenGet(secrets)
}

7.4 add.go 模块

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "sactl/controller"
    "sactl/logs"
    "sactl/model"
)

var (
    role string
)

// addCmd represents the add command
var addCmd = &cobra.Command{
    Use:   "add",
    Short: "该子命令用于 SA 绑定不同 namespace 下的 RoleBind!",
    Long: `验证示例:
sactl add ServiceAccount -r RoleBind -n NameSpace1 NameSpace2 NameSpace3 ... `,
    Run: func(cmd *cobra.Command, args []string) {
        //if len(args) == 0 {
        //  fmt.Println(`语法错误:
        //查看帮助: sactl add -h `)
        //  return
        //}
        //

        fstatus, err := cmd.Flags().GetBool("namespace")
        if fstatus {
            AddSa(args)
        } else {
            fmt.Printf("Error:namesapce 未指定\nsactl add -h 查看帮助!\n")
            logs.ErrInfo("cmd error", "AddSa", err)
            return
        }
    },
}

func init() {
    rootCmd.AddCommand(addCmd)

    // 这里由于需要传入多个参数所以通过 StringVarP() 函数,接收 namespace 和 role
    addCmd.Flags().StringVarP(&role, "role", "r", "", "指定 ServiceAccount 绑定的 Role")
    addCmd.Flags().BoolP("namespace", "n", false, "指定 Role 创建的 NameSpace")

    // Here you will define your flags and configuration settings.

    // Cobra supports Persistent Flags which will work for this command
    // and all subcommands, e.g.:
    // addCmd.PersistentFlags().String("foo", "", "A help for foo")

    // Cobra supports local flags which will only run when this command
    // is called directly, e.g.:
    // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

func AddSa(args []string) {
    roleBind := args[0]
    namespaces := args[1:]
    addSa := model.NewAddRole(roleBind, role, namespaces)

    controller.Add_RoleBind(addSa)
}

8 main.go

/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main

import (
    "sactl/cmd"
    "sactl/logs"
)

func main() {
    // 先实现日志以及 config 功能的配置
    logs.Logs()
    cmd.Execute()
}

9 验证

1.创建 ns 和 pod

[17:30:06 root@master test]#kubectl create ns dev
namespace/dev created

[17:30:25 root@master test]#kubectl run nginx --image=nginx:1.16 --namespace=dev
pod/nginx created

[17:32:53 root@master test]#kubectl get pod -n dev 
NAME    READY   STATUS              RESTARTS   AGE
nginx   0/1     ContainerCreating   0          2m20s

9.1 验证 role 绑定 admin

1.创建一个 admin 的 role ,因为刚才在代码中写死了需要绑定 role 为 admin

[17:30:35 root@master test]#cat test_role.yaml 
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: dev
  name: admin      # 指定 role 为 admin
rules:
- apiGroups: ["*"] # "" 标明 core API 组以及所有资源和操作的权限, * 表示所有
  resources: ["*"]
  verbs: ["*"]

[17:32:55 root@master test]#kubectl apply -f test_role.yaml 
role.rbac.authorization.k8s.io/admin created

2 通过程序创建 sa

[16:30:02 root@go sactl]#go run main.go create -n dev sa1
sa1 ServiceAccounts Create Success!
sa1 Rolebinding Create Success!

9.2 验证基于 SA 获取 POD

1 通过程序来获取 pod

[17:34:07 root@go sactl]#go run main.go check -n dev sa1 "https://10.0.0.131:6443"
NameSpace:dev  Pod:nginx

# 获取成功

9.3 验证基于 Token 获取 Pod 信息

通过 token 获取到对应的 Pod 信息

1.获取 token

# 获取 sa
[15:57:47 root@master ~]#kubectl get sa -n dev 
NAME      SECRETS   AGE
default   1         22h
sa1       1         22h

# 获取 role
[15:58:00 root@master ~]#kubectl get role -n dev 
NAME    CREATED AT
admin   2023-02-08T09:34:22Z

# 查看 secrets
[15:58:13 root@master ~]#kubectl get secrets -n dev 
NAME                  TYPE                                  DATA   AGE
default-token-9bmqc   kubernetes.io/service-account-token   3      22h
sa1-token-c5frn       kubernetes.io/service-account-token   3      22h

# 查看 rolebind
[15:58:22 root@master ~]#kubectl get rolebindings.rbac.authorization.k8s.io -n dev 
NAME   ROLE         AGE
sa1    Role/admin   22h

# 查看 POD
[15:58:31 root@master ~]#kubectl get pod -n dev 
No resources found in dev namespace.

# 创建 POD
[15:58:39 root@master ~]#kubectl run nginx --image=nginx:1.16 -n dev
pod/nginx created

# 获取 pod
[15:58:56 root@master ~]#kubectl get pod -n dev 
NAME    READY   STATUS              RESTARTS   AGE
nginx   0/1     ContainerCreating   0          4s

# 查看 token    
[15:59:00 root@master ~]#kubectl describe secrets -n dev sa1-token-c5frn 
Name:         sa1-token-c5frn
Namespace:    dev
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: sa1
              kubernetes.io/service-account.uid: f12b5cbd-3444-4fd7-ac86-7fa877aa0d87

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1099 bytes
namespace:  3 bytes
# 下面就是获取到的 token 信息
token:              eyJhbGciOiJSUzI1NiIsImtpZCI6IkxlX2JRWEFDNzRCNmFodHp2VzZBanBwMFlDYWxYYk1jMS1CVjMyV3lfOXMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXYiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoic2ExLXRva2VuLWM1ZnJuIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6InNhMSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImYxMmI1Y2JkLTM0NDQtNGZkNy1hYzg2LTdmYTg3N2FhMGQ4NyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXY6c2ExIn0.W0iIh7gn3ajuUGN15TvdwDMA3NhhDw-OflrFqZmw6Cvj3_V1kB1o-o1bvgPOdpf758rvSITg96zHSmHguZaRpmUnG7NsEvBHO78hpmzThcWLY8-jcP69nfyeRwzF2M7V3oZ9KyACi1pF5DvYhoUFgpBQ9bIgSGPRlwtM-0YbPOldwPCbImEomHtQhuTZ_tAqm43-xc_jnfnm1kXjP-zhWoq_a_SD28hL_G3CunrMoYajc0VLVcLPIAu53qAHDGp4XQX0Zwo2NQYhU2EGLT4mN7dBZKq36VdS8rTjUegke9dH9zLa6chZUZPX_q6LsW_QfQBK4s84AFdVjnBeAIL7Bg

2.工具基于 token 访问 pod

# token 可以看到是相同的
[17:31:57 root@go sactl]#go run main.go get -n dev  "https://10.0.0.131:6443" "eyJhbGciOiJSUzI1NiIsImtpZCI6IkxlX2JRWEFDNzRCNmFodHp2VzZBanBwMFlDYWxYYk1jMS1CVjMyV3lfOXMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXYiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoic2ExLXRva2VuLWM1ZnJuIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6InNhMSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImYxMmI1Y2JkLTM0NDQtNGZkNy1hYzg2LTdmYTg3N2FhMGQ4NyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXY6c2ExIn0.W0iIh7gn3ajuUGN15TvdwDMA3NhhDw-OflrFqZmw6Cvj3_V1kB1o-o1bvgPOdpf758rvSITg96zHSmHguZaRpmUnG7NsEvBHO78hpmzThcWLY8-jcP69nfyeRwzF2M7V3oZ9KyACi1pF5DvYhoUFgpBQ9bIgSGPRlwtM-0YbPOldwPCbImEomHtQhuTZ_tAqm43-xc_jnfnm1kXjP-zhWoq_a_SD28hL_G3CunrMoYajc0VLVcLPIAu53qAHDGp4XQX0Zwo2NQYhU2EGLT4mN7dBZKq36VdS8rTjUegke9dH9zLa6chZUZPX_q6LsW_QfQBK4s84AFdVjnBeAIL7Bg"

token 验证成功!
namespace:dev   pod:nginx

9.4 追加 RoleBind

验证将 kube-system 绑定到多个 NS 下

# 先在 kube-systeml 中创建一个 sa1
[16:54:17 root@master test]#kubectl get sa -n kube-system sa1 
NAME   SECRETS   AGE
sa1    1         30m

# 所有集群下没有 admin1 role
[17:00:07 root@master test]#kubectl get role -A| grep admin1

# 所以程序未能找到对应的 role 无法做后续的 rolebind 动作
[16:52:19 root@go sactl]#go run main.go add sa1 -r admin1 -n test dev temp
错误! namespace:test 下不存在 role:admi1n!
错误! namespace:dev 下不存在 role:admi1n!
错误! namespace:temp 下不存在 role:admi1n!

# 可以看到 dev temp test 这三个 NS 有 role admin
[17:01:43 root@master test]#kubectl get role -A| grep admin
dev           admin                                            2023-02-08T09:34:22Z
temp          admin                                            2023-02-10T06:17:01Z
test          admin                                            2023-02-10T02:03:26Z

# 这时候我将 kube-system ns 下的 sa1 绑定到这三个 NS 下的 admin role 上
[16:58:17 root@go sactl]#go run main.go add sa1 -r admin -n test dev temp
sa1 Rolebinding Create Success!
sa1 Rolebinding Create Success!
sa1 Rolebinding Create Success!

# 验证已经绑定成功
[17:03:12 root@master test]#kubectl get rolebindings.rbac.authorization.k8s.io -A | grep sa1
dev           sa1                                                 Role/admin                                            20s
temp          sa1                                                 Role/admin                                            20s
test          sa1                                                 Role/admin                                            20s

# 再次创建 sa1 rolebind 发现程序识别错误并退出
[16:58:03 root@go sactl]#go run main.go add sa1 -r admin -n test dev temp
错误! namespace:test 下已存在 RoleBindg:sa1!
错误! namespace:dev 下已存在 RoleBindg:sa1!
错误! namespace:temp 下已存在 RoleBindg:sa1!

总结:

该工具需要我们了解 K8S 中的 sa secrets 的关联关系,以及 role rolebind 的关系,和 client-go sdk 的使用

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇