My company have won an open source project that involves creation of a Kubernetes operator to manage user workspaces for a web platform. We have an existing operator for another project that does the job, but it contains some project IP. I have been tasked to refactor out the generic functionality into a core package, and structure the module so that it is extendable. We are using Kubebuilder framework to create the operator. It’s also worth mentioning that, although I am an experienced software engineer, I am new to Go.
I have scaffolded the core project using Kubebuilder. I added the workspace API and everything works as expected. The functionality just now is minimal, when a workspace resource is created the API will create a namespace with the same name as the workspace resource. Key excerpts below:
# steps to scaffold project
kubebuilder init --domain my.domain --owner "My Org"
kubebuilder create api --group core --kind Workspace --version v1alpha1
// (core) api/v1alpha1/workspace_types.go
...
type WorkspaceSpec struct {} // no functionality required yet
type WorkspaceStatus struct {
// The name of the namespace for the workspace
Namespace string `json:"namespace,omitempty"`
} // added namespace field to status
type Workspace struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WorkspaceSpec `json:"spec,omitempty"`
Status WorkspaceStatus `json:"status,omitempty"`
} // boilerplate, unchanged from scaffolding
type WorkspaceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Workspace `json:"items"`
} // boilerplate, unchanged from scaffolding
// (core) controller/workspace_controller.go
...
type WorkspaceReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Get a reference to the workspace that has been updated
workspace := &corev1alpha1.Workspace{}
if err := r.Get(ctx, req.NamespacedName, workspace); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// my reconciliation logic
return ctrl.Result{}, nil
}
Note that I removed the workspace_controller module from the internal
directory created during the scaffolding as I want to make it importable in isolation by the extending project.
Now, my design is to allow a user to scaffold their own Kubebuilder project and reuse and extend the functionality provided by the core module.
Project is scaffolded similarly to above, but with different domain and group. I embedded the structs that define the API in the newly scaffolded API.
// (extension) api/v1alpha1/workspace_types.go
...
import (
...
corev1alpha1 "github.com/MyOrg/workspace-controller/api/v1alpha1"
)
...
type WorkspaceSpec struct {
corev1alpha1.WorkspaceSpec `json:",inline"`
} // embedded core spec
type WorkspaceStatus struct {
corev1alpha1.WorkspaceStatus `json:",inline"`
} // embedded core status
type Workspace struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec WorkspaceSpec `json:"spec,omitempty"`
Status WorkspaceStatus `json:"status,omitempty"`
} // unmodified, so there is no link between core Workspace struct and ext Workspace struct
type WorkspaceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Workspace `json:"items"`
} // unmodified
So far so good, I can generate the CRDs with make manifests
and the code runs. The issue I have is reusing the reconciliation logic in the core. In the controller/workspace_controller.go
above, the type in the Reconcile method is hardcoded, and will differ from the extension type. When I try and call the core Reconcile from the extension Reconcile then it does not find any resources, which I understand since I hardcoded the core Workspace struct type.
// (extension) controller/workspace_controller.go
import (
...
corecontroller "github.com/MyOrg/workspace-controller/controller"
)
...
type WorkspaceReconciler struct {
corecontroller.WorkspaceReconciler
}
func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Get a reference to the workspace that has been updated
workspace := &corev1alpha1.Workspace{}
if err := r.Get(ctx, req.NamespacedName, workspace); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
} // this call finds the extension resource
// call embedded core Reconciler
r.WorkspaceReconciler.Reconcile(ctx, req) // becomes a no-op because workspace resource not found, which is understandable
return ctrl.Result{}, nil
}
So, my question is, how do other extendable Kubebuilder APIs deal with this issue? Is there something I have missed? Or can I use any features of Go that I haven’t thought of, such as reflection, to infer the type of resource I am looking for from the Reconciler or any other available variables so that when using the core module I get core.Workspace and when using the extension module I get extension.Workspace?
Is anyone aware of any extendable Kubebuilder API open source projects that I can reference to see how they do it?