前言
上篇文章中介绍了code-generator
生成clientsets,informers等,但在测试时由于没有创建crd,导致执行无法获取到crd。这篇文章就介绍下如何把kubebuilder
和code-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
2.创建API
kubebuilder create api --group webapp --version v1 --kind Guestbook
Create Resource [y/n]
y
Create Controller [y/n]
n
3.设计API
修改guestbook_types.go
修改GuestbookSpec
和GuestbookStatus
的字段。
// 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
:
4.生成crd
我这里不启用webhook,所以不需要修改相关patch文件。执行命令生成manifests:
make manifests
生成的crd manifests位于:config/crd/bases/webapp.example.com_guestbooks.yaml
生成后的role.yaml
:
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
中已经声明了SchemeBuilder
,AddToScheme
变量,所以这里不用再次声明。
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
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。
更新crd的状态为Online,更新crd的name为wghdrr。
查看crd的manifest确认更新成功。