go, k8s

kubebuilder+code-generator创建crd

前言

上篇文章中介绍了code-generator生成clientsets,informers等,但在测试时由于没有创建crd,导致执行无法获取到crd。这篇文章就介绍下如何把kubebuildercode-generator二者相结合。整体思路就是先用kubebuilder生成crd的yaml文件,再使用code-generator生成clientsets,informers等。

步骤

1.创建并初始化项目

mkdir example && cd example
go mod init example
kubebuilder init --domain example.com
kubebuilder edit --multigroup=true

file

2.创建API

kubebuilder create api --group webapp --version v1 --kind Guestbook
Create Resource [y/n]
y
Create Controller [y/n]
n

file

3.设计API

修改guestbook_types.go

修改GuestbookSpecGuestbookStatus的字段。

// GuestbookSpec defines the desired state of Guestbook
type GuestbookSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Foo is an example field of Guestbook. Edit guestbook_types.go to remove/update
    Name string `json:"name,omitempty"`
}

// GuestbookStatus defines the observed state of Guestbook
type GuestbookStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file
    Online bool `json:"online,omitempty"`
}

创建rbac.go

由于这里没有创建controller,所以这里通过创建api/webapp/v1/rbac.go文件来生成rbac的yaml,文件位置:config/rbac/role.yaml

// +kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=webapp.example.com,resources=guestbooks/status,verbs=get;update;patch

package v1

这是生成前的role.yaml

file

4.生成crd

我这里不启用webhook,所以不需要修改相关patch文件。执行命令生成manifests:

make manifests

file

生成的crd manifests位于:config/crd/bases/webapp.example.com_guestbooks.yaml
生成后的role.yaml

file

5.使用code-generator生成代码

  • 创建以下三个对外API文件:

doc.go

// +k8s:deepcopy-gen=package
// +groupName=webapp.example.com

package v1

types.go

由于上一步中已经使用kubebuilder生成了guestbook_types.go文件,所以这里直接修改该文件添加tag即可。

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Guestbook is the Schema for the guestbooks API
type Guestbook struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty"`

        Spec   GuestbookSpec   `json:"spec,omitempty"`
        Status GuestbookStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// GuestbookList contains a list of Guestbook
type GuestbookList struct {
        metav1.TypeMeta `json:",inline"`
        metav1.ListMeta `json:"metadata,omitempty"`
        Items           []Guestbook `json:"items"`
}

register.go

api/webapp/v1/groupversion_info.go中已经声明了SchemeBuilderAddToScheme变量,所以这里不用再次声明。

var (
        // GroupVersion is group version used to register these objects
        GroupVersion = schema.GroupVersion{Group: "webapp.example.com", Version: "v1"}

        // SchemeBuilder is used to add go types to the GroupVersionKind scheme
        SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

        // AddToScheme adds the types in this group-version to the given scheme.
        AddToScheme = SchemeBuilder.AddToScheme
)

所以register.go代码如下:

package v1

import (
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/apimachinery/pkg/runtime/schema"
)

// SchemeGroupVersion is group version used to register these objects.
var SchemeGroupVersion = GroupVersion

func Resource(resource string) schema.GroupResource {
        return SchemeGroupVersion.WithResource(resource).GroupResource()
}

func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &Guestbook{},
        &GuestbookList{},
    )
    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
    return nil
}
  • 进入hack目录,创建以下三个文件:

tools.go

导入k8s.io/code-generator包。

// +build tools

package tools

import _ "k8s.io/code-generator"

update-codegen.sh

根据实际修改相关变量。

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

# corresponding to go mod init <module>
MODULE=example
# api package
APIS_PKG=api
# generated output package
OUTPUT_PKG=generated/webapp
# group-version such as foo:v1alpha1
GROUP_VERSION=webapp:v1

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

# generate the code with:
# --output-base    because this script should also be able to run inside the vendor dir of
#                  k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
#                  instead of the $GOPATH directly. For normal projects this can be dropped.
bash "${CODEGEN_PKG}"/generate-groups.sh "client,lister,informer" \
  ${MODULE}/${OUTPUT_PKG} ${MODULE}/${APIS_PKG} \
  ${GROUP_VERSION} \
  --go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt \
  --output-base "${SCRIPT_ROOT}"
#  --output-base "${SCRIPT_ROOT}/../../.." \

verify-codegen.sh

根据实际修改变量。

#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

OUTPUT_PKG=generated/webapp
MODULE=example

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..

DIFFROOT="${SCRIPT_ROOT}/${OUTPUT_PKG}"
TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/${OUTPUT_PKG}"
_tmp="${SCRIPT_ROOT}/_tmp"

cleanup() {
  rm -rf "${_tmp}"
}
trap "cleanup" EXIT SIGINT

cleanup

mkdir -p "${TMP_DIFFROOT}"
cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}"

"${SCRIPT_ROOT}/hack/update-codegen.sh"
echo "copying generated ${SCRIPT_ROOT}/${MODULE}/${OUTPUT_PKG} to ${DIFFROOT}"
cp -r "${SCRIPT_ROOT}/${MODULE}/${OUTPUT_PKG}"/* "${DIFFROOT}"

echo "diffing ${DIFFROOT} against freshly generated codegen"
ret=0
diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$?
cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}"
if [[ $ret -eq 0 ]]
then
  echo "${DIFFROOT} up to date."
else
  echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh"
  exit 1
fi
  • 获取依赖:

修改包的版本为k8s集群版本。

go get k8s.io/apimachinery@v0.24.16
go get k8s.io/code-generator@v0.24.16
go mod tidy
go mod vendor
  • 执行脚本
chmod +x hack/update-codegen.sh
chmod +x vendor/k8s.io/code-generator/generate-groups.sh
chmod +x vendor/k8s.io/code-generator/generate-internal-groups.sh
./hack/update-codegen.sh

Generating clientset for webapp:v1 at example/generated/webapp/clientset
Generating listers for webapp:v1 at example/generated/webapp/listers
Generating informers for webapp:v1 at example/generated/webapp/informers

mv example/generated/ .

生成了clientset,listers,informers目录。

6.测试

修改cr webapp_v1_guestbook.yaml文件,添加spec。

apiVersion: webapp.example.com/v1
kind: Guestbook
metadata:
  labels:
    app.kubernetes.io/name: guestbook
    app.kubernetes.io/instance: guestbook-sample
    app.kubernetes.io/part-of: example
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: example
  name: guestbook-sample
spec:
  # TODO(user): Add fields here
  name: wgh

创建crd和cr。

k apply -f config/crd/bases/webapp.example.com_guestbooks.yaml
k apply -f config/samples/webapp_v1_guestbook.yaml
k get crd | grep guestbook
k get Guestbook
k get Guestbook -o yaml

file

file

7.代码访问

package main

import (
    "context"
    webappv1 "example/api/webapp/v1"
    guestbookclientset "example/generated/webapp/clientset/versioned"
    guestbookinformers "example/generated/webapp/informers/externalversions"
    guestbooklisters "example/generated/webapp/listers/webapp/v1"
    "fmt"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/labels"
    "k8s.io/client-go/tools/cache"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/util/homedir"
    "k8s.io/klog/v2"
    "time"
)

func main() {
    // 获取kubeconfig
    config, _ := clientcmd.BuildConfigFromFlags("", homedir.HomeDir()+"/.kube/config")
    // 创建clientset
    clientset, err := guestbookclientset.NewForConfig(config)
    if err != nil {
        klog.Fatalln(err)
    }
    // 创建SharedInformer,监听Guestbook资源的变化
    factory := guestbookinformers.NewSharedInformerFactory(clientset, time.Second*10)
    informers := factory.Webapp().V1().Guestbooks()
    informers.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            klog.Infof("add: %v", obj)
        },
        UpdateFunc: func(oldObj, newObj interface{}) {
            klog.Infof("update: %v -> %v", oldObj, newObj)
        },
        DeleteFunc: func(obj interface{}) {
            klog.Infof("delete: %v", obj)
        },
    })
    // 启动SharedInformer
    stopCh := make(chan struct{})
    defer close(stopCh)
    factory.Start(stopCh)
    // 创建Lister
    lister := informers.Lister()
    fmt.Println("---------------")
    listBooks(lister)
    // 创建一个定时器,超时后退出
    timeout := time.NewTimer(time.Second * 30)
    timeoutCh := make(chan struct{})
    go func() {
        <-timeout.C
        timeoutCh <- struct{}{}
    }()
    // 缓存同步,如果超时就退出
    if ok := cache.WaitForCacheSync(timeoutCh, informers.Informer().HasSynced); !ok {
        klog.Fatalln("timeout waiting for cache sync")
    }
    fmt.Println("---------------")
    fmt.Println("sync done")
    fmt.Println("---------------")
    guestbooks := listBooks(lister)

    fmt.Println("---------------")
    for _, guestbook := range guestbooks {
        setStatus(clientset, guestbook)
    }

    for _, guestbook := range guestbooks {
        setName(clientset, guestbook)
    }
}

// 打印Guestbook
func listBooks(lister guestbooklisters.GuestbookLister) []*webappv1.Guestbook {
    guestBooks, err := lister.List(labels.Everything())
    if err != nil {
        return nil
    }
    fmt.Println("list Guestbooks:")
    for _, guestBook := range guestBooks {
        fmt.Println(guestBook)
    }
    return guestBooks
}

// 设置Guestbook状态
func setStatus(clientset *guestbookclientset.Clientset, guestbook *webappv1.Guestbook) {
    // cache是只读的本地存储,所以不能直接修改,需要深拷贝
    copy := guestbook.DeepCopy()
    copy.Status.Online = true
    _, err := clientset.WebappV1().Guestbooks(copy.Namespace).UpdateStatus(context.TODO(), copy, metav1.UpdateOptions{})
    if err != nil {
        klog.Fatalln(err)
    }
    fmt.Println("update Guestbook status to online", guestbook)
}

// 设置Guestbook名字
func setName(clientset *guestbookclientset.Clientset, guestbook *webappv1.Guestbook) {
    copy := guestbook.DeepCopy()
    // 由于上面更新了status,所以要重新获取,否则会报错the object has been modified; please apply your changes to the latest version and try again
    crd, err := clientset.WebappV1().Guestbooks(copy.Namespace).Get(context.TODO(), copy.Name, metav1.GetOptions{})
    if err != nil {
        klog.Fatalln(err)
    }
    crd.Spec.Name = "wghdrr"
    _, err = clientset.WebappV1().Guestbooks(crd.Namespace).Update(context.TODO(), crd, metav1.UpdateOptions{})
    if err != nil {
        klog.Fatalln(err)
    }
    fmt.Println("update Guestbook name to wghdrr", guestbook)
}

代码输出如下:

先获取crd,再同步,再次获取crd。

file

更新crd的状态为Online,更新crd的name为wghdrr。

file

查看crd的manifest确认更新成功。

file

0 0 投票数
文章评分
订阅评论
提醒
guest

0 评论
最旧
最新 最多投票
内联反馈
查看所有评论

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部
0
希望看到您的想法,请您发表评论x