Kubernetes is awesome. But documentation for lesser-used parts of the ecosystem isn't as awesome. If you've dug your way through the documentation and managed to create your very own custom resource, you might want to now use it in your Go app. But the only documentation for generating Go types for the official k8s client is a blog post from 2017 linked from the 147 word README, which was last changed on May 7, 2022 to fix a broken link to the aforementioned blog post.
Let's imagine we are trying to organise our song collection with a CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: songs.music.sportshead.dev
spec:
group: music.sportshead.dev
scope: Namespaced
names:
plural: songs
singular: song
kind: Song
versions:
- name: v1
served: true
storage: true
To make sure that other developers can use our song collection, we'll write an OpenAPI schema:
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
title:
type: string
artist:
type: string
rating:
type: integer
enum: [1, 2, 3, 4, 5]
genres:
type: array
items:
type: string
required:
- title
- artist
To start generating code, we need to set some tags for the code-generator
tool. A tag is a comment in a Go file that looks like this:
// +tag-name
// +tag-name=value
By convention, our global tags are stored in pkg/apis/<group>/<version>/doc.go
. For example, our pkg/apis/music.sportshead.dev/v1/doc.go
would look like this:
// +k8s:deepcopy-gen=package
// Package v1 is the v1 version of the API.
// +groupName=music.sportshead.dev
// +groupGoName=CoolMusic
package v1
k8s:deepcopy-gen
enables registering deepcopy methods for the entire package.groupName
sets the fully qualified API group namegroupGoName
sets the prefix of the generated Go objects (e.g.CoolMusicV1Client
)
Now we create a types.go
file to declare our types. pkg/apis/music.sportshead.dev/v1/types.go
:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Song is a Song resource.
type Song struct {
metav1.TypeMeta `json:",inline"`
// Standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
metav1.ObjectMeta `json:"metadata"`
Spec SongSpec `json:"spec"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// SongList is a collection of Song resources.
type SongList struct {
metav1.TypeMeta `json:",inline"`
// Standard object's metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
metav1.ListMeta `json:"metadata"`
Items []Song `json:"items"`
}
// SongSpec is the spec of a Song resource.
type SongSpec struct {
Title string `json:"title"`
Artist string `json:"artist"`
Rating int `json:"rating,omitempty"`
Genres []string `json:"genres,omitempty"`
}
We're almost done! Now we create a register.go
file to tell code-generator
about our API:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "music.sportshead.dev"
// SchemeGroupVersion is a group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
// Kind takes an unqualified kind and returns a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// SchemeBuilder initializes a scheme builder
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that registers this API group & version to a scheme
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Song{},
&SongList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
Change the GroupName
and add all the types declared in types.go
to addKnownTypes
. At this point, your IDE might complain about the lack of DeepCopyObject()
methods on your types. Luckily, we can generate these methods using the deepcopy-gen
package. We can create a bash script to generate our clientset and deepcopy code. This script is usually in hack/update-codegen.sh
:
#!/usr/bin/env bash
# Modified from https://github.com/kubernetes-sigs/kueue/blob/065451d907fa27a0647436505b3cac38718ef136/hack/update-codegen.sh
# Apache-2.0, Copyright 2023 The Kubernetes Authors
set -o errexit
set -o nounset
set -o pipefail
GO_CMD=${1:-go}
PKG_ROOT=$(realpath "$(dirname ${BASH_SOURCE[0]})/..")
CODEGEN_PKG=$($GO_CMD list -m -f "{{.Dir}}" k8s.io/code-generator)
cd $PKG_ROOT
source "${CODEGEN_PKG}/kube_codegen.sh"
# TODO: remove the workaround when the issue is solved in code-generator
# (https://github.com/kubernetes/code-generator/issues/165).
# kube_codegen.sh expects this layout:
# .
# └── github.com
# └── sportshead
# └── codegen-demo
# We can use soft links in order to fake this layout, such that
# ./github.com/sportshead/codegen-demo resolves to ././../codegen-demo, or ./.
ln -s . github.com
ln -s .. sportshead
trap "rm github.com && rm sportshead" EXIT
kube::codegen::gen_helpers \
--input-pkg-root github.com/sportshead/codegen-demo/pkg/apis \
--boilerplate /dev/null \
--output-base "${PKG_ROOT}"
kube::codegen::gen_client \
--input-pkg-root github.com/sportshead/codegen-demo/pkg/apis \
--output-pkg-root github.com/sportshead/codegen-demo/pkg/generated \
--boilerplate /dev/null \
--output-base "${PKG_ROOT}" \
--with-watch \
--with-applyconfig
# clean up temporary libraries added in go.mod by code-generator
"${GO_CMD}" mod tidy
Note: You may need to create a hack/codegen.go
file with the following to stop go mod tidy
from removing k8s.io/code-generator
:
package hack
// Keep a reference to code-generator so it's not removed by go mod tidy
import (
_ "k8s.io/code-generator"
)
Change the package paths as necessary, and set --boilerplate
to ${PKG_ROOT}/hack/boilerplate.go.txt
if you need licence headers in generated files.
Running hack/update-codegen.sh
now generates all the code we need to start using our CRD in Go!
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err.Error())
}
musicClient, err := clientset.NewForConfig(config)
if err != nil {
panic(err.Error())
}
songs, err := musicClient.CoolMusicV1().Songs(*namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf(`Found %d songs in namespace "%s"`, len(songs.Items), namespace)
All the code in this tutorial can be found at https://github.com/sportshead/codegen-demo. Happy hacking!
References:
- https://cloud.redhat.com/blog/kubernetes-deep-dive-code-generation-customresources
- kubernetes/code-generator/examples/hack/update-codegen.sh - Apache 2.0, The Kubernetes Authors
- kubernetes-sigs/kueue/hack/update-codegen.sh - Apache 2.0, The Kubernetes Authors
- kubernetes/sample-controller - Apache 2.0, The Kubernetes Authors