这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
package kubernetesservice
import (
"context"
"flag"
"log"
"path/filepath"
"sync"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
)
var CLIENT_SET kubernetes.Interface
var ONCE sync.Once
// DoInit Indexer相关的初始化操作,这里确保只执行一次
func DoInit() {
ONCE.Do(initInKubernetesEnv)
}
// GetClient 调用此方法返回clientSet对象
func GetClient() kubernetes.Interface {
return CLIENT_SET
}
// SetClient 可以通过initInKubernetesEnv在kubernetes初始化,如果有准备好的clientSet,也可以调用SetClient直接设置,而无需初始化
func SetClient(clientSet kubernetes.Interface) {
CLIENT_SET = clientSet
}
// initInKubernetesEnv 这里是真正的初始化逻辑
func initInKubernetesEnv() {
log.Println("开始初始化Indexer")
var kubeconfig *string
// 试图取到当前账号的家目录
if home := homedir.HomeDir(); home != "" {
// 如果能取到,就把家目录下的.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")
}
// 加载配置文件
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err.Error())
}
// 用clientset类来执行后续的查询操作
CLIENT_SET, err = kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
log.Println("kubernetes服务初始化成功")
}
// QueryPodNameByLabelApp 根据指定的namespace和label值搜索
func QueryPodNameByLabelApp(context context.Context, namespace, app string) ([]string, error) {
log.Printf("QueryPodNameByLabelApp, namespace [%s], app [%s]", namespace, app)
equalRequirement, err := labels.NewRequirement("app", selection.Equals, []string{app})
if err != nil {
return nil, err
}
selector := labels.NewSelector().Add(*equalRequirement)
// 查询pod列表
pods, err := CLIENT_SET.
CoreV1().
Pods(namespace).
List(context, metav1.ListOptions{
// 传入的selector在这里用到
LabelSelector: selector.String(),
})
if err != nil {
return nil, err
}
names := make([]string, 0)
for _, v := range pods.Items {
names = append(names, v.GetName())
}
return names, nil
}
// CreateNamespace 单元测试的辅助工具,用于创建namespace
func CreateNamespace(context context.Context, client kubernetes.Interface, name string) error {
namespaceObj := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
_, err := client.CoreV1().Namespaces().Create(context, namespaceObj, metav1.CreateOptions{})
return err
}
// DeleteeNamespace 单元测试的辅助工具,用于创建namespace
func DeleteNamespace(context context.Context, client kubernetes.Interface, name string) error {
err := client.CoreV1().Namespaces().Delete(context, name, metav1.DeleteOptions{})
return err
}
/*
// CreateDeployment 单元测试的辅助工具,用于创建namespace
func CreateDeployment(context context.Context, client kubernetes.Interface, namespace, name, image, app string, replicas int32) error {
_, err := client.AppsV1().Deployments(namespace).Create(context, &apps.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: map[string]string{
"app": app,
},
},
Spec: apps.DeploymentSpec{
Replicas: &replicas,
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Image: image,
},
},
},
},
},
}, metav1.CreateOptions{})
return err
}
*/
package handler
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
kubernetesservice "client-go-unit-tutorials/kubernetes_service"
)
const (
PARAM_NAMESPACE = "namespace"
PARAM_APP = "label_app"
)
func QueryPodsByLabelApp(context *gin.Context) {
rlt := make(map[string]interface{})
namespace := context.DefaultQuery(PARAM_NAMESPACE, "")
app := context.DefaultQuery(PARAM_APP, "")
log.Printf("query param, namespace [%s], app [%s]", namespace, app)
names, err := kubernetesservice.QueryPodNameByLabelApp(context, namespace, app)
if err != nil {
rlt["message"] = err.Error()
context.JSON(http.StatusInternalServerError, rlt)
return
}
rlt["message"] = "success"
rlt["names"] = names
context.JSON(http.StatusOK, rlt)
}
package initor
import (
"github.com/gin-gonic/gin"
"client-go-unit-tutorials/handler"
)
const (
PATH_QUERY_PODS_BY_LABEL_APP = "/query_pods_by_label_app"
)
func InitRouter() *gin.Engine {
r := gin.Default()
// 绑定path的handler
r.GET(PATH_QUERY_PODS_BY_LABEL_APP, handler.QueryPodsByLabelApp)
return r
}
package main
import (
"client-go-unit-tutorials/initor"
kubernetesservice "client-go-unit-tutorials/kubernetes_service"
)
func main() {
// 初始化kubernetes相关配置
kubernetesservice.DoInit()
router := initor.InitRouter()
_ = router.Run(":18080")
}
kubectl create namespace client-go-tutorials
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: client-go-tutorials
name: nginx-deployment
labels:
app: nginx-app
type: front-end
spec:
replicas: 3
selector:
matchLabels:
app: nginx-app
type: front-end
template:
metadata:
labels:
app: nginx-app
type: front-end
# 这是第一个业务自定义label,指定了mysql的语言类型是c语言
language: c
# 这是第二个业务自定义label,指定了这个pod属于哪一类服务,nginx属于web类
business-service-type: web
spec:
containers:
- name: nginx-container
image: nginx:latest
resources:
limits:
cpu: "0.5"
memory: 128Mi
requests:
cpu: "0.1"
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
namespace: client-go-tutorials
name: nginx-service
spec:
type: NodePort
selector:
app: nginx-app
type: front-end
ports:
- port: 80
targetPort: 80
nodePort: 30011
kubectl apply -f nginx-deployment-service.yaml
kubectl get pods -n client-go-tutorials
NAME READY STATUS RESTARTS AGE
nginx-deployment-78f6b696d9-j98xj 1/1 Running 0 19h
nginx-deployment-78f6b696d9-wp4qf 1/1 Running 0 7d17h
nginx-deployment-78f6b696d9-wpnt7 1/1 Running 0 20h
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}"
}
]
}
### 变量
@namespace=client-go-tutorials
@label_app=nginx-app
### 测试用例,指定namespace和label查询所有的pod名称
GET http://192.168.50.76:18080/query_pods_by_label_app?namespace={{namespace}}&label_app={{label_app}}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 02 Jul 2023 04:58:03 GMT
Content-Length: 139
Connection: close
{
"message": "success",
"names": [
"nginx-deployment-78f6b696d9-j98xj",
"nginx-deployment-78f6b696d9-wp4qf",
"nginx-deployment-78f6b696d9-wpnt7"
]
}
package unittesthelper
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
TEST_NAMESPACE = "client-go-tutorials"
TEST_POD_NAME_PREFIX = "nginx-pod-"
TEST_IMAGE = "nginx:latest"
TEST_LABEL_APP = "nginx-app"
TEST_POD_NUM = 3
)
// 数据结构,用于保存web响应的body
type ResponseNames struct {
Message string `json:"message"`
Names []string `json:"names"`
}
// SingleTest 辅助方法,发请求,返回响应
func SingleTest(router *gin.Engine, url string) (int, string, error) {
log.Printf("start SingleTest, request url : %s", url)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, url, nil)
router.ServeHTTP(w, req)
return w.Code, w.Body.String(), nil
}
// 9. 辅助方法,解析web响应,检查结果是否符合预期
func Check(suite *suite.Suite, body string, expectNum int) {
suite.NotNil(body)
response := &ResponseNames{}
err := json.Unmarshal([]byte(body), response)
if err != nil {
log.Fatalf("unmarshal response error, %s", err.Error())
}
suite.EqualValues(expectNum, len(response.Names))
}
// CreatePodObj 辅助方法,用于创建pod对象
func CreatePodObj(namespace, name, app, image string) *v1.Pod {
return &v1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: map[string]string{
"app": app,
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Image: image,
},
},
},
}
}
// CreateDeployment 单元测试的辅助工具,用于创建namespace
func CreatePods(context context.Context, client kubernetes.Interface, namespace, name, image, app string) error {
_, err := client.CoreV1().Pods(namespace).Create(context, CreatePodObj(namespace, name, app, image), metav1.CreateOptions{})
return err
}
// CreatePod 辅助方法,用于创建多个pod
func CreatePod(context context.Context, client kubernetes.Interface, num int) {
for i := 0; i < num; i++ {
if err := CreatePods(context,
client,
TEST_NAMESPACE,
fmt.Sprintf("%s%d", TEST_POD_NAME_PREFIX, i),
TEST_IMAGE,
TEST_LABEL_APP); err != nil {
log.Fatalf("create pod [%d] error, %s", i, err.Error())
}
}
}
package handler_test
import (
"client-go-unit-tutorials/handler"
"client-go-unit-tutorials/initor"
kubernetesservice "client-go-unit-tutorials/kubernetes_service"
"client-go-unit-tutorials/unittesthelper"
"context"
"fmt"
"log"
"net/http"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
)
// 1. 定义suite数据结构
type MySuite struct {
suite.Suite
ctx context.Context
cancel context.CancelFunc
clientSet kubernetes.Interface
router *gin.Engine
}
// 2. 单元测试的初始化操作
func (mySuite *MySuite) SetupTest() {
client := fake.NewSimpleClientset()
kubernetesservice.SetClient(client)
mySuite.ctx, mySuite.cancel = context.WithCancel(context.Background())
mySuite.clientSet = client
mySuite.router = initor.InitRouter()
// 初始化数据,创建namespace
if err := kubernetesservice.CreateNamespace(mySuite.ctx, client, unittesthelper.TEST_NAMESPACE); err != nil {
log.Fatalf("create namespace error, %s", err.Error())
}
// 初始化数据,创建pod
unittesthelper.CreatePod(mySuite.ctx, client, 3)
}
// 3. 定义测试完成后的收尾工作,例如清理一些资源
func (mySuite *MySuite) TearDownTest() {
// 删除namespace
if err := kubernetesservice.DeleteNamespace(mySuite.ctx, kubernetesservice.GetClient(), unittesthelper.TEST_NAMESPACE); err != nil {
log.Fatalf("delete namespace error, %s", err.Error())
}
mySuite.cancel()
}
// 4. 启动测试集
func TestBasicCrud(t *testing.T) {
suite.Run(t, new(MySuite))
}
// 5. 定义测试集
func (mySuite *MySuite) TestBasicCrud() {
// 5.1 若有需要,执行monkey.Patch
// 5.2 若执行了monkey.Patch,需要执行defer monkey.UnpatchAll()
// 5.3 执行单个测试
// 参考 client-go/examples/fake-client/main_test.go/main_test.go
mySuite.Run("常规查询", func() {
url := fmt.Sprintf("%s?%s=%s&%s=%s",
initor.PATH_QUERY_PODS_BY_LABEL_APP,
handler.PARAM_NAMESPACE,
unittesthelper.TEST_NAMESPACE,
handler.PARAM_APP,
unittesthelper.TEST_LABEL_APP)
code, body, error := unittesthelper.SingleTest(mySuite.router, url)
if error != nil {
mySuite.Fail("SingleTest error, %v", error)
return
}
// 检查返回码
mySuite.EqualValues(http.StatusOK, code)
// 检查结果
unittesthelper.Check(&mySuite.Suite, body, unittesthelper.TEST_POD_NUM)
})
}
=== RUN TestBasicCrud
=== RUN TestBasicCrud/TestBasicCrud
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /query_pods_by_label_app --> client-go-unit-tutorials/handler.QueryPodsByLabelApp (3 handlers)
=== RUN TestBasicCrud/TestBasicCrud/常规查询
2023/07/02 05:17:27 start SingleTest, request url : /query_pods_by_label_app?namespace=client-go-tutorials&label_app=nginx-app
2023/07/02 05:17:27 query param, namespace [client-go-tutorials], app [nginx-app]
2023/07/02 05:17:27 QueryPodNameByLabelApp, namespace [client-go-tutorials], app [nginx-app]
[GIN] 2023/07/02 - 05:17:27 | 200 | 205.281µs | | GET "/query_pods_by_label_app?namespace=client-go-tutorials&label_app=nginx-app"
--- PASS: TestBasicCrud/TestBasicCrud/常规查询 (0.00s)
--- PASS: TestBasicCrud/TestBasicCrud (0.00s)
--- PASS: TestBasicCrud (0.00s)
PASS
ok client-go-unit-tutorials/handler 0.034s
> Test run finished at 7/2/2023, 1:17:26 PM <
名称 | 链接 | 备注 |
---|---|---|
项目主页 | 该项目在GitHub上的主页 | |
git仓库地址(https) | 该项目源码的仓库地址,https协议 | |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |