diff --git a/CLAUDE.md b/CLAUDE.md index da65696..6d13f39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,8 +76,8 @@ You can run these commands directly in the ts directory or use the pattern-match - `make lint-fix`: Run golangci-lint and fix issues - `make test`: Run unit tests - `make test-e2e`: Run end-to-end tests (requires a running Kind cluster) -- `make manifests`: Generate Kubernetes manifests (CRDs, RBAC) -- `make generate`: Generate Go code (DeepCopy methods) +- `make manifests`: Generate Kubernetes manifests (CRDs, RBAC) - **Important:** Run this after modifying CRD types or controller RBAC annotations +- `make generate`: Generate Go code (DeepCopy methods) - **Important:** Run this after adding new struct fields #### Build Commands - `make build`: Build the manager binary @@ -122,6 +122,14 @@ Individual components can be managed separately: - `make tempo-up/down` - `make loki-up/down` +## Documentation + +The project includes detailed documentation in the `/kubechain/docs/` directory: + +- [MCP Server Guide](/kubechain/docs/mcp-server.md) - Working with Model Control Protocol servers +- [CRD Reference](/kubechain/docs/crd-reference.md) - Complete reference for all Custom Resource Definitions +- [Kubebuilder Guide](/kubechain/docs/kubebuilder-guide.md) - How to develop with Kubebuilder in this project + ## Typical Workflow ### Local Development with Kind Cluster @@ -168,6 +176,16 @@ Alternatively, clean up components individually: - Test with Ginkgo/Gomega framework - Document public functions with godoc +### Kubebuilder and CRD Development +- All resources should be in the `kubechain.humanlayer.dev` API group +- Use proper kubebuilder annotations for validation and RBAC +- Add RBAC annotations to all controllers to generate proper permissions +- Run `make manifests` after modifying CRD types or controller annotations +- Run `make generate` after adding new struct fields to generate DeepCopy methods +- When creating new resources, use `kubebuilder create api --group kubechain --version v1alpha1 --kind YourResource --namespaced true --resource true --controller true` +- Ensure the PROJECT file contains entries for all resources before running `make manifests` +- Follow the detailed guidance in the [Kubebuilder Guide](/kubechain/docs/kubebuilder-guide.md) + ### TypeScript - Use 2-space indentation - No semicolons (per prettier config) diff --git a/kubechain/PROJECT b/kubechain/PROJECT index 6fab5c8..a2a3774 100644 --- a/kubechain/PROJECT +++ b/kubechain/PROJECT @@ -17,4 +17,67 @@ resources: kind: LLM path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: Tool + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: Agent + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: Task + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: TaskRun + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: TaskRunToolCall + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: MCPServer + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: humanlayer.dev + group: kubechain + kind: ContactChannel + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/kubechain/README.md b/kubechain/README.md index 887c102..259a306 100644 --- a/kubechain/README.md +++ b/kubechain/README.md @@ -113,16 +113,38 @@ previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml is manually re-applied afterwards. ## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project -**NOTE:** Run `make help` for more information on all potential `make` targets +### Development Workflow -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) +The project uses [Kubebuilder](https://book.kubebuilder.io/) for scaffolding Kubernetes resources and controllers. If you're extending the API or adding new resource types, please refer to our [Kubebuilder Guide](./docs/kubebuilder-guide.md) for detailed instructions on: + +- Adding new custom resources +- Updating existing resources +- Working with controllers +- Generating RBAC permissions +- Following best practices + +### Make Targets + +Run `make help` for more information on all potential `make` targets. Common targets include: + +- `make build` - Build the manager binary +- `make manifests` - Generate WebhookConfiguration, ClusterRole, and CustomResourceDefinition objects +- `make generate` - Generate code (DeepCopy methods) +- `make test` - Run tests +- `make docker-build` - Build the Docker image + +### Resources + +- [Kubebuilder Book](https://book.kubebuilder.io/introduction.html) - Official Kubebuilder documentation +- [Controller Runtime](https://github.com/kubernetes-sigs/controller-runtime) - Library for building controllers +- [Kubernetes API Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md) - Standards for Kubernetes API design ## Documentation - [MCP Server Guide](./docs/mcp-server.md) - Detailed guide for working with MCP servers - [CRD Reference](./docs/crd-reference.md) - Complete reference for all Custom Resource Definitions +- [Kubebuilder Guide](./docs/kubebuilder-guide.md) - How to develop with Kubebuilder in this project ## Resource Types diff --git a/kubechain/api/v1alpha1/contactchannel_types.go b/kubechain/api/v1alpha1/contactchannel_types.go new file mode 100644 index 0000000..1a4be8a --- /dev/null +++ b/kubechain/api/v1alpha1/contactchannel_types.go @@ -0,0 +1,64 @@ +/* +Copyright 2025 the Kubechain Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ContactChannelSpec defines the desired state of ContactChannel. +type ContactChannelSpec 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 ContactChannel. Edit contactchannel_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// ContactChannelStatus defines the observed state of ContactChannel. +type ContactChannelStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ContactChannel is the Schema for the contactchannels API. +type ContactChannel struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ContactChannelSpec `json:"spec,omitempty"` + Status ContactChannelStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ContactChannelList contains a list of ContactChannel. +type ContactChannelList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ContactChannel `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ContactChannel{}, &ContactChannelList{}) +} diff --git a/kubechain/api/v1alpha1/zz_generated.deepcopy.go b/kubechain/api/v1alpha1/zz_generated.deepcopy.go index cba932a..fc18de1 100644 --- a/kubechain/api/v1alpha1/zz_generated.deepcopy.go +++ b/kubechain/api/v1alpha1/zz_generated.deepcopy.go @@ -182,6 +182,95 @@ func (in *BuiltinToolSpec) DeepCopy() *BuiltinToolSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContactChannel) DeepCopyInto(out *ContactChannel) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContactChannel. +func (in *ContactChannel) DeepCopy() *ContactChannel { + if in == nil { + return nil + } + out := new(ContactChannel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ContactChannel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContactChannelList) DeepCopyInto(out *ContactChannelList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ContactChannel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContactChannelList. +func (in *ContactChannelList) DeepCopy() *ContactChannelList { + if in == nil { + return nil + } + out := new(ContactChannelList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ContactChannelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContactChannelSpec) DeepCopyInto(out *ContactChannelSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContactChannelSpec. +func (in *ContactChannelSpec) DeepCopy() *ContactChannelSpec { + if in == nil { + return nil + } + out := new(ContactChannelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContactChannelStatus) DeepCopyInto(out *ContactChannelStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContactChannelStatus. +func (in *ContactChannelStatus) DeepCopy() *ContactChannelStatus { + if in == nil { + return nil + } + out := new(ContactChannelStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EnvVar) DeepCopyInto(out *EnvVar) { *out = *in diff --git a/kubechain/config/crd/bases/kubechain.humanlayer.dev_contactchannels.yaml b/kubechain/config/crd/bases/kubechain.humanlayer.dev_contactchannels.yaml new file mode 100644 index 0000000..dec0987 --- /dev/null +++ b/kubechain/config/crd/bases/kubechain.humanlayer.dev_contactchannels.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 + name: contactchannels.kubechain.humanlayer.dev +spec: + group: kubechain.humanlayer.dev + names: + kind: ContactChannel + listKind: ContactChannelList + plural: contactchannels + singular: contactchannel + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ContactChannel is the Schema for the contactchannels API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ContactChannelSpec defines the desired state of ContactChannel. + properties: + foo: + description: Foo is an example field of ContactChannel. Edit contactchannel_types.go + to remove/update + type: string + type: object + status: + description: ContactChannelStatus defines the observed state of ContactChannel. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/kubechain/config/crd/kustomization.yaml b/kubechain/config/crd/kustomization.yaml index 1e6c284..a0f7bf9 100644 --- a/kubechain/config/crd/kustomization.yaml +++ b/kubechain/config/crd/kustomization.yaml @@ -9,6 +9,7 @@ resources: - bases/kubechain.humanlayer.dev_taskruns.yaml - bases/kubechain.humanlayer.dev_taskruntoolcalls.yaml - bases/kubechain.humanlayer.dev_mcpservers.yaml +- bases/kubechain.humanlayer.dev_contactchannels.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/kubechain/config/rbac/contactchannel_admin_role.yaml b/kubechain/config/rbac/contactchannel_admin_role.yaml new file mode 100644 index 0000000..80a0373 --- /dev/null +++ b/kubechain/config/rbac/contactchannel_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project kubechain itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over kubechain.humanlayer.dev. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubechain + app.kubernetes.io/managed-by: kustomize + name: contactchannel-admin-role +rules: +- apiGroups: + - kubechain.humanlayer.dev + resources: + - contactchannels + verbs: + - '*' +- apiGroups: + - kubechain.humanlayer.dev + resources: + - contactchannels/status + verbs: + - get diff --git a/kubechain/config/rbac/contactchannel_editor_role.yaml b/kubechain/config/rbac/contactchannel_editor_role.yaml new file mode 100644 index 0000000..75b6bf0 --- /dev/null +++ b/kubechain/config/rbac/contactchannel_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project kubechain itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the kubechain.humanlayer.dev. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubechain + app.kubernetes.io/managed-by: kustomize + name: contactchannel-editor-role +rules: +- apiGroups: + - kubechain.humanlayer.dev + resources: + - contactchannels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kubechain.humanlayer.dev + resources: + - contactchannels/status + verbs: + - get diff --git a/kubechain/config/rbac/contactchannel_viewer_role.yaml b/kubechain/config/rbac/contactchannel_viewer_role.yaml new file mode 100644 index 0000000..96a19b9 --- /dev/null +++ b/kubechain/config/rbac/contactchannel_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project kubechain itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to kubechain.humanlayer.dev resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: kubechain + app.kubernetes.io/managed-by: kustomize + name: contactchannel-viewer-role +rules: +- apiGroups: + - kubechain.humanlayer.dev + resources: + - contactchannels + verbs: + - get + - list + - watch +- apiGroups: + - kubechain.humanlayer.dev + resources: + - contactchannels/status + verbs: + - get diff --git a/kubechain/config/rbac/kustomization.yaml b/kubechain/config/rbac/kustomization.yaml index 7815c72..8188fb9 100644 --- a/kubechain/config/rbac/kustomization.yaml +++ b/kubechain/config/rbac/kustomization.yaml @@ -22,6 +22,11 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the {{ .ProjectName }} itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- contactchannel_admin_role.yaml +- contactchannel_editor_role.yaml +- contactchannel_viewer_role.yaml - llm_admin_role.yaml - llm_editor_role.yaml - llm_viewer_role.yaml + + diff --git a/kubechain/config/rbac/role.yaml b/kubechain/config/rbac/role.yaml index b08e900..a004d85 100644 --- a/kubechain/config/rbac/role.yaml +++ b/kubechain/config/rbac/role.yaml @@ -5,109 +5,24 @@ metadata: name: manager-role rules: - apiGroups: - - kubechain.humanlayer.dev + - "" resources: - - agents + - secrets verbs: - - create - - delete - get - list - - patch - - update - watch - apiGroups: - kubechain.humanlayer.dev resources: - - agents/status - verbs: - - get - - patch - - update -- apiGroups: - - kubechain.humanlayer.dev - resources: + - agents + - contactchannels - llms - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kubechain.humanlayer.dev - resources: - - llms/status - verbs: - - get - - patch - - update -- apiGroups: - - kubechain.humanlayer.dev - resources: - - tools - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kubechain.humanlayer.dev - resources: - - tools/status - verbs: - - get - - patch - - update -- apiGroups: - - kubechain.humanlayer.dev - resources: - - tasks - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kubechain.humanlayer.dev - resources: - - tasks/status - verbs: - - get - - patch - - update -- apiGroups: - - kubechain.humanlayer.dev - resources: + - mcpservers - taskruns - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kubechain.humanlayer.dev - resources: - - taskruns/status - verbs: - - get - - patch - - update -- apiGroups: - - kubechain.humanlayer.dev - resources: - taskruntoolcalls + - tasks + - tools verbs: - create - delete @@ -119,36 +34,21 @@ rules: - apiGroups: - kubechain.humanlayer.dev resources: + - agents/status + - contactchannels/status + - llms/status + - mcpservers/status + - taskruns/status - taskruntoolcalls/status + - tasks/status + - tools/status verbs: - get - patch - update -- apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch -- apiGroups: - - kubechain.humanlayer.dev - resources: - - mcpservers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - kubechain.humanlayer.dev resources: - - mcpservers/status + - contactchannels/finalizers verbs: - - get - - patch - update diff --git a/kubechain/config/samples/kubechain_v1alpha1_contactchannel.yaml b/kubechain/config/samples/kubechain_v1alpha1_contactchannel.yaml new file mode 100644 index 0000000..2317364 --- /dev/null +++ b/kubechain/config/samples/kubechain_v1alpha1_contactchannel.yaml @@ -0,0 +1,9 @@ +apiVersion: kubechain.humanlayer.dev/v1alpha1 +kind: ContactChannel +metadata: + labels: + app.kubernetes.io/name: kubechain + app.kubernetes.io/managed-by: kustomize + name: contactchannel-sample +spec: + # TODO(user): Add fields here diff --git a/kubechain/config/samples/kustomization.yaml b/kubechain/config/samples/kustomization.yaml index 87971df..90e6bd8 100644 --- a/kubechain/config/samples/kustomization.yaml +++ b/kubechain/config/samples/kustomization.yaml @@ -6,4 +6,5 @@ resources: - kubechain_v1alpha1_agent.yaml - kubechain_v1alpha1_task.yaml - kubechain_v1alpha1_mcpserver.yaml +- kubechain_v1alpha1_contactchannel.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/kubechain/docs/README.md b/kubechain/docs/README.md index b6dd2b2..4994c94 100644 --- a/kubechain/docs/README.md +++ b/kubechain/docs/README.md @@ -14,6 +14,7 @@ Kubechain is a Kubernetes operator for managing Large Language Model (LLM) workf - [MCP Server Guide](./mcp-server.md) - Working with Model Control Protocol servers - [CRD Reference](./crd-reference.md) - Complete reference for all Custom Resource Definitions +- [Kubebuilder Guide](./kubebuilder-guide.md) - How to develop with Kubebuilder in this project ## Example Resources @@ -32,4 +33,6 @@ For concrete examples, check the sample YAML files in the [`config/samples/`](.. ## Development -For development documentation, see the [CONTRIBUTING](../CONTRIBUTING.md) guide. \ No newline at end of file +For general development documentation, see the [CONTRIBUTING](../CONTRIBUTING.md) guide. + +For instructions on working with Kubebuilder to extend the Kubernetes API (adding new CRDs, controllers, etc.), refer to the [Kubebuilder Guide](./kubebuilder-guide.md). \ No newline at end of file diff --git a/kubechain/docs/kubebuilder-guide.md b/kubechain/docs/kubebuilder-guide.md new file mode 100644 index 0000000..22db44a --- /dev/null +++ b/kubechain/docs/kubebuilder-guide.md @@ -0,0 +1,232 @@ +# Kubebuilder Development Guide + +## Overview + +Kubechain is built using [Kubebuilder](https://book.kubebuilder.io/), a framework for building Kubernetes APIs using custom resource definitions (CRDs). This guide explains how to use kubebuilder in this project, particularly for adding new resources and maintaining existing ones. + +## Current Project Structure + +The project uses Kubebuilder v4 with a domain of `humanlayer.dev` and an API group of `kubechain`. All resources are in the `v1alpha1` version and are namespaced. + +Current resources include: +- `LLM` - Configuration for large language models +- `Agent` - Defines an agent using an LLM and tools +- `Tool` - Defines tools that can be used by agents +- `Task` - Defines a task to be executed +- `TaskRun` - Represents a run of a task +- `TaskRunToolCall` - Represents a tool call during a task run +- `MCPServer` - Defines a Model Control Protocol server for tool integration + +## Adding a New Resource + +To add a new resource to the project, follow these steps: + +1. Create the new resource using kubebuilder: + +```bash +kubebuilder create api --group kubechain --version v1alpha1 --kind YourNewResource --namespaced true --resource true --controller true +``` + +This will: +- Create a new file in `api/v1alpha1/yournewresource_types.go` +- Create a new controller in `internal/controller/yournewresource/` +- Update the PROJECT file with the new resource + +2. Define your resource fields in the `*Spec` and `*Status` structs in the generated `_types.go` file. + +3. Add RBAC annotations to the controller in `internal/controller/yournewresource/yournewresource_controller.go`: + +```go +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=yournewresources,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=yournewresources/status,verbs=get;update;patch +``` + +Add additional RBAC annotations for any other resources your controller needs to access: + +```go +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=someotherresource,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch +``` + +4. Add kubebuilder printing column annotations to your resource's struct: + +```go +// +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.status" +``` + +5. Generate manifests to create the CRD and update RBAC: + +```bash +make manifests +``` + +### Example: Adding a ContactChannel Resource + +Here's an example of creating a ContactChannel resource: + +```bash +kubebuilder create api --group kubechain --version v1alpha1 --kind ContactChannel --namespaced true --resource true --controller true +``` + +Then, edit the generated `api/v1alpha1/contactchannel_types.go` file: + +```go +type ContactChannelSpec struct { + // Type specifies the type of contact channel (e.g., "email", "slack", "teams") + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=email;slack;teams + Type string `json:"type"` + + // Address is the destination for notifications (email address, channel ID, etc.) + // +kubebuilder:validation:Required + Address string `json:"address"` + + // SecretRef contains credentials needed to access the channel + // +optional + SecretRef *SecretKeyRef `json:"secretRef,omitempty"` +} + +type ContactChannelStatus struct { + // Ready indicates if the channel is ready to receive notifications + Ready bool `json:"ready,omitempty"` + + // Status indicates the current status of the channel + // +kubebuilder:validation:Enum=Ready;Error;Pending + Status string `json:"status,omitempty"` + + // StatusDetail provides additional details about the current status + StatusDetail string `json:"statusDetail,omitempty"` +} +``` + +Edit the controller to add RBAC annotations: + +```go +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=contactchannels,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=contactchannels/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + +// ContactChannelReconciler reconciles a ContactChannel object +type ContactChannelReconciler struct { + // ... +} +``` + +Add printing columns: + +```go +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type" +// +kubebuilder:printcolumn:name="Address",type="string",JSONPath=".spec.address" +// +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.status" +// +kubebuilder:resource:scope=Namespaced +``` + +After making these changes, run: + +```bash +make manifests +``` + +## Common Pitfalls and Solutions + +### 1. Missing or Incorrect RBAC Annotations + +**Problem**: The controller can't access resources it needs because RBAC permissions are missing. + +**Solution**: Make sure to add proper RBAC annotations to your controller before running `make manifests`. Remember to include permissions for any resources your controller accesses, not just the one it's primarily responsible for. + +### 2. PROJECT File Out of Sync + +**Problem**: The PROJECT file doesn't contain all resources or has incorrect information. + +**Solution**: If kubebuilder doesn't update the PROJECT file correctly, you can manually edit it to ensure all resources are properly listed. After manual edits, run `make manifests` to regenerate manifests. + +### 3. DeepCopy Methods Missing + +**Problem**: After adding new fields, you get compilation errors about missing DeepCopy methods. + +**Solution**: Run `make generate` to regenerate DeepCopy methods: + +```bash +make generate +``` + +### 4. CRD Validation Issues + +**Problem**: The CRD validation schema doesn't match your expectations. + +**Solution**: Check the kubebuilder validation annotations in your type definitions. Most validation is done using annotations like: + +```go +// +kubebuilder:validation:Minimum=0 +// +kubebuilder:validation:Maximum=100 +// +kubebuilder:validation:Enum=option1;option2;option3 +``` + +### 5. Forgetting to Update the Controller Implementation + +**Problem**: The new resource is created but doesn't do anything. + +**Solution**: Remember to implement the Reconcile function in your controller to handle the resource's logic. + +## Kubebuilder Commands Reference + +### Initialize a New Project + +```bash +kubebuilder init --domain example.com --repo github.com/example/project +``` + +### Create a New API/Resource + +```bash +kubebuilder create api --group groupname --version v1 --kind KindName +``` + +### Generate Code and Manifests + +```bash +# Update generated code +make generate + +# Generate CRDs and RBAC +make manifests +``` + +### Install CRDs into a Cluster + +```bash +make install +``` + +### Package for Release + +```bash +make release-local tag=v0.1.0 +``` + +## Best Practices + +1. **Use proper validation annotations**: Add validation to your resource fields with kubebuilder annotations to ensure users provide valid data. + +2. **Keep RBAC minimal**: Only request the permissions your controller actually needs. This follows the principle of least privilege. + +3. **Include status conditions**: Status conditions provide a standardized way to report resource state. + +4. **Add printer columns**: Printer columns make the resource more user-friendly when viewed with `kubectl get`. + +5. **Document your resource fields**: Use comments to document what each field does and any constraints. + +6. **Write unit tests**: Test your controller's reconciliation logic with proper unit tests. + +7. **Follow Kubernetes API conventions**: Follow the [Kubernetes API conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md) for naming and field organization. + +## Additional Resources + +- [Kubebuilder Book](https://book.kubebuilder.io/) +- [Kubernetes API Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md) +- [Controller Runtime](https://github.com/kubernetes-sigs/controller-runtime) \ No newline at end of file diff --git a/kubechain/internal/controller/agent/agent_controller.go b/kubechain/internal/controller/agent/agent_controller.go index 219e0de..8b11376 100644 --- a/kubechain/internal/controller/agent/agent_controller.go +++ b/kubechain/internal/controller/agent/agent_controller.go @@ -20,6 +20,12 @@ const ( StatusError = "Error" ) +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=agents,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=agents/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=llms,verbs=get;list;watch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tools,verbs=get;list;watch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=mcpservers,verbs=get;list;watch + // AgentReconciler reconciles a Agent object type AgentReconciler struct { client.Client diff --git a/kubechain/internal/controller/contactchannel_controller.go b/kubechain/internal/controller/contactchannel_controller.go new file mode 100644 index 0000000..63ed048 --- /dev/null +++ b/kubechain/internal/controller/contactchannel_controller.go @@ -0,0 +1,63 @@ +/* +Copyright 2025 the Kubechain Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + kubechainv1alpha1 "github.com/humanlayer/smallchain/kubechain/api/v1alpha1" +) + +// ContactChannelReconciler reconciles a ContactChannel object +type ContactChannelReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=contactchannels,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=contactchannels/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=contactchannels/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the ContactChannel object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile +func (r *ContactChannelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ContactChannelReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&kubechainv1alpha1.ContactChannel{}). + Named("contactchannel"). + Complete(r) +} diff --git a/kubechain/internal/controller/contactchannel_controller_test.go b/kubechain/internal/controller/contactchannel_controller_test.go new file mode 100644 index 0000000..8408c8a --- /dev/null +++ b/kubechain/internal/controller/contactchannel_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2025 the Kubechain Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kubechainv1alpha1 "github.com/humanlayer/smallchain/kubechain/api/v1alpha1" +) + +var _ = Describe("ContactChannel Controller", func() { + Context("When reconciling a resource", func() { + const resourceName = "test-resource" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed + } + contactchannel := &kubechainv1alpha1.ContactChannel{} + + BeforeEach(func() { + By("creating the custom resource for the Kind ContactChannel") + err := k8sClient.Get(ctx, typeNamespacedName, contactchannel) + if err != nil && errors.IsNotFound(err) { + resource := &kubechainv1alpha1.ContactChannel{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &kubechainv1alpha1.ContactChannel{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance ContactChannel") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &ContactChannelReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/kubechain/internal/controller/llm/llm_controller.go b/kubechain/internal/controller/llm/llm_controller.go index e27893b..744a77e 100644 --- a/kubechain/internal/controller/llm/llm_controller.go +++ b/kubechain/internal/controller/llm/llm_controller.go @@ -32,6 +32,10 @@ import ( kubechainv1alpha1 "github.com/humanlayer/smallchain/kubechain/api/v1alpha1" ) +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=llms,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=llms/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + // LLMReconciler reconciles a LLM object type LLMReconciler struct { client.Client diff --git a/kubechain/internal/controller/mcpserver/mcpserver_controller.go b/kubechain/internal/controller/mcpserver/mcpserver_controller.go index aa614b3..1a741e2 100644 --- a/kubechain/internal/controller/mcpserver/mcpserver_controller.go +++ b/kubechain/internal/controller/mcpserver/mcpserver_controller.go @@ -32,6 +32,9 @@ type MCPServerManagerInterface interface { Close() } +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=mcpservers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=mcpservers/status,verbs=get;update;patch + // MCPServerReconciler reconciles a MCPServer object type MCPServerReconciler struct { client.Client diff --git a/kubechain/internal/controller/suite_test.go b/kubechain/internal/controller/suite_test.go new file mode 100644 index 0000000..bfd5e57 --- /dev/null +++ b/kubechain/internal/controller/suite_test.go @@ -0,0 +1,116 @@ +/* +Copyright 2025 the Kubechain Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + kubechainv1alpha1 "github.com/humanlayer/smallchain/kubechain/api/v1alpha1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + ctx context.Context + cancel context.CancelFunc + testEnv *envtest.Environment + cfg *rest.Config + k8sClient client.Client +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + var err error + err = kubechainv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + // Retrieve the first found binary directory to allow running tests from IDEs + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. +// ENVTEST-based tests depend on specific binaries, usually located in paths set by +// controller-runtime. When running tests directly (e.g., via an IDE) without using +// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. +// +// This function streamlines the process by finding the required binaries, similar to +// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are +// properly set up, run 'make setup-envtest' beforehand. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + logf.Log.Error(err, "Failed to read directory", "path", basePath) + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} diff --git a/kubechain/internal/controller/task/task_controller.go b/kubechain/internal/controller/task/task_controller.go index 799cb9c..319d750 100644 --- a/kubechain/internal/controller/task/task_controller.go +++ b/kubechain/internal/controller/task/task_controller.go @@ -16,6 +16,11 @@ import ( kubechainv1alpha1 "github.com/humanlayer/smallchain/kubechain/api/v1alpha1" ) +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tasks,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tasks/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=agents,verbs=get;list;watch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=taskruns,verbs=get;list;create;watch + // TaskReconciler reconciles a Task object type TaskReconciler struct { client.Client diff --git a/kubechain/internal/controller/taskrun/taskrun_controller.go b/kubechain/internal/controller/taskrun/taskrun_controller.go index 6c5708c..2dfb735 100644 --- a/kubechain/internal/controller/taskrun/taskrun_controller.go +++ b/kubechain/internal/controller/taskrun/taskrun_controller.go @@ -27,6 +27,13 @@ const ( StatusPending = "Pending" ) +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=taskruns,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=taskruns/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tasks,verbs=get;list;watch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=agents,verbs=get;list;watch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=llms,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + // TaskRunReconciler reconciles a TaskRun object type TaskRunReconciler struct { client.Client diff --git a/kubechain/internal/controller/taskruntoolcall/taskruntoolcall_controller.go b/kubechain/internal/controller/taskruntoolcall/taskruntoolcall_controller.go index 039816b..c2603a6 100644 --- a/kubechain/internal/controller/taskruntoolcall/taskruntoolcall_controller.go +++ b/kubechain/internal/controller/taskruntoolcall/taskruntoolcall_controller.go @@ -31,6 +31,11 @@ const ( DetailInvalidArgsJSON = "Invalid arguments JSON" ) +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=taskruntoolcalls,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=taskruntoolcalls/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tools,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + // TaskRunToolCallReconciler reconciles a TaskRunToolCall object. type TaskRunToolCallReconciler struct { client.Client diff --git a/kubechain/internal/controller/tool/tool_controller.go b/kubechain/internal/controller/tool/tool_controller.go index 87e2cff..a2424e1 100644 --- a/kubechain/internal/controller/tool/tool_controller.go +++ b/kubechain/internal/controller/tool/tool_controller.go @@ -16,6 +16,9 @@ import ( "github.com/openai/openai-go" ) +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tools,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kubechain.humanlayer.dev,resources=tools/status,verbs=get;update;patch + // ToolReconciler reconciles a Tool object type ToolReconciler struct { client.Client