diff --git a/kubechain/PROJECT b/kubechain/PROJECT index 6fab5c8..da52ee7 100644 --- a/kubechain/PROJECT +++ b/kubechain/PROJECT @@ -17,4 +17,13 @@ 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: ContactChannel + path: github.com/humanlayer/smallchain/kubechain/api/v1alpha1 + version: v1alpha1 version: "3" 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..b7de8e0 100644 --- a/kubechain/config/rbac/role.yaml +++ b/kubechain/config/rbac/role.yaml @@ -7,7 +7,7 @@ rules: - apiGroups: - kubechain.humanlayer.dev resources: - - agents + - contactchannels verbs: - create - delete @@ -19,135 +19,13 @@ rules: - apiGroups: - kubechain.humanlayer.dev resources: - - agents/status + - contactchannels/finalizers verbs: - - get - - patch - - update -- apiGroups: - - kubechain.humanlayer.dev - resources: - - 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: - - 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 - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kubechain.humanlayer.dev - resources: - - taskruntoolcalls/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/status verbs: - get - patch 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/internal/controller/contactchannel/contactchannel_controller.go b/kubechain/internal/controller/contactchannel/contactchannel_controller.go new file mode 100644 index 0000000..62b2277 --- /dev/null +++ b/kubechain/internal/controller/contactchannel/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.0/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/contactchannel_controller_test.go b/kubechain/internal/controller/contactchannel/contactchannel_controller_test.go new file mode 100644 index 0000000..8408c8a --- /dev/null +++ b/kubechain/internal/controller/contactchannel/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/contactchannel/suite_test.go b/kubechain/internal/controller/contactchannel/suite_test.go new file mode 100644 index 0000000..50d656b --- /dev/null +++ b/kubechain/internal/controller/contactchannel/suite_test.go @@ -0,0 +1,117 @@ +/* +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, + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s"), + } + + // 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 "" +}