Browse Source

feat(skill): add Skill api

Thomas Zhang 2 months ago
parent
commit
8c00f7f4cd

+ 8 - 0
PROJECT

@@ -81,4 +81,12 @@ resources:
   kind: Document
   path: github.com/LocoStack/loco-operator/api/v1alpha1
   version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: locostack.com
+  kind: Skill
+  path: github.com/LocoStack/loco-operator/api/v1alpha1
+  version: v1alpha1
 version: "3"

+ 123 - 0
api/v1alpha1/skill_types.go

@@ -0,0 +1,123 @@
+/*
+Copyright 2026 LocoStack.
+
+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 (
+	"strings"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+type SkillFileEntry struct {
+	// path is the relative path of the file within the skill.
+	Path string `json:"path"`
+
+	// content is the content of the file.
+	Content string `json:"content"`
+}
+
+// SkillSpec defines the desired state of Skill
+type SkillSpec struct {
+	// stackRef associates this tool with a stack.
+	// +optional
+	StackRef *corev1.LocalObjectReference `json:"stackRef,omitempty"`
+
+	// skillName is the name of the skill, part of the "SKILL.md" file.
+	// +kubebuilder:validation:Required
+	SkillName string `json:"skillName"`
+
+	// description provides a brief summary of the skill, part of the "SKILL.md" file.
+	// +kubebuilder:validation:Required
+	Description string `json:"description"`
+
+	// body contains the main content of the skill, part of the "SKILL.md" file.
+	// +kubebuilder:validation:Required
+	Body string `json:"body"`
+
+	// files is the list of files that will be used by the skill.
+	// scripts, references, assets, ...
+	// +optional
+	Files []SkillFileEntry `json:"files,omitempty"`
+}
+
+// SkillConfigMapKey maps a skill file's relative path (e.g. "scripts/foo.py") to a
+// value that satisfies the Kubernetes ConfigMap key charset ([-._a-zA-Z0-9]+), since
+// paths may contain "/" which the API rejects as a key. "-" is escaped to "--" first
+// so the "/" -> "-" substitution stays reversible, meaning distinct paths never collide.
+func SkillConfigMapKey(path string) string {
+	escaped := strings.ReplaceAll(path, "-", "--")
+	return strings.ReplaceAll(escaped, "/", "-")
+}
+
+// SkillFilePathsAnnotation is the annotation on a Skill's ConfigMap holding a
+// JSON-encoded map of SkillConfigMapKey(path) -> original relative path. Consumers
+// mounting the ConfigMap read this to lay the files back out as a real directory tree,
+// without needing to re-fetch the Skill or recompute SkillConfigMapKey themselves.
+const SkillFilePathsAnnotation = "locostack.com/skill-file-paths"
+
+// SkillStatus defines the observed state of Skill.
+type SkillStatus struct {
+	// files is the number of files contained in the skill.
+	// +optional
+	Files int32 `json:"files,omitempty"`
+
+	// observedGeneration is the .metadata.generation last reconciled.
+	// +optional
+	ObservedGeneration int64 `json:"observedGeneration,omitempty"`
+
+	// The status of each condition is one of True, False, or Unknown.
+	// +listType=map
+	// +listMapKey=type
+	// +optional
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:shortName=skl
+// +kubebuilder:printcolumn:name="Files",type=integer,JSONPath=".status.files"
+
+// Skill is the Schema for the skills API
+type Skill struct {
+	metav1.TypeMeta `json:",inline"`
+
+	// metadata is a standard object metadata
+	// +optional
+	metav1.ObjectMeta `json:"metadata,omitzero"`
+
+	// spec defines the desired state of Skill
+	// +required
+	Spec SkillSpec `json:"spec"`
+
+	// status defines the observed state of Skill
+	// +optional
+	Status SkillStatus `json:"status,omitzero"`
+}
+
+// +kubebuilder:object:root=true
+
+// SkillList contains a list of Skill
+type SkillList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitzero"`
+	Items           []Skill `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&Skill{}, &SkillList{})
+}

+ 121 - 0
api/v1alpha1/zz_generated.deepcopy.go

@@ -1249,6 +1249,127 @@ func (in *RuntimeSpec) DeepCopy() *RuntimeSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Skill) DeepCopyInto(out *Skill) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Skill.
+func (in *Skill) DeepCopy() *Skill {
+	if in == nil {
+		return nil
+	}
+	out := new(Skill)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Skill) 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 *SkillFileEntry) DeepCopyInto(out *SkillFileEntry) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillFileEntry.
+func (in *SkillFileEntry) DeepCopy() *SkillFileEntry {
+	if in == nil {
+		return nil
+	}
+	out := new(SkillFileEntry)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SkillList) DeepCopyInto(out *SkillList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Skill, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillList.
+func (in *SkillList) DeepCopy() *SkillList {
+	if in == nil {
+		return nil
+	}
+	out := new(SkillList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *SkillList) 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 *SkillSpec) DeepCopyInto(out *SkillSpec) {
+	*out = *in
+	if in.StackRef != nil {
+		in, out := &in.StackRef, &out.StackRef
+		*out = new(v1.LocalObjectReference)
+		**out = **in
+	}
+	if in.Files != nil {
+		in, out := &in.Files, &out.Files
+		*out = make([]SkillFileEntry, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillSpec.
+func (in *SkillSpec) DeepCopy() *SkillSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(SkillSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SkillStatus) DeepCopyInto(out *SkillStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]metav1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillStatus.
+func (in *SkillStatus) DeepCopy() *SkillStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(SkillStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Stack) DeepCopyInto(out *Stack) {
 	*out = *in

+ 7 - 0
cmd/main.go

@@ -241,6 +241,13 @@ func main() {
 		setupLog.Error(err, "Failed to create controller", "controller", "Document")
 		os.Exit(1)
 	}
+	if err := (&controller.SkillReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "Failed to create controller", "controller", "Skill")
+		os.Exit(1)
+	}
 	// +kubebuilder:scaffold:builder
 
 	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

+ 176 - 0
config/crd/bases/locostack.com_skills.yaml

@@ -0,0 +1,176 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.20.1
+  name: skills.locostack.com
+spec:
+  group: locostack.com
+  names:
+    kind: Skill
+    listKind: SkillList
+    plural: skills
+    shortNames:
+    - skl
+    singular: skill
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .status.files
+      name: Files
+      type: integer
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: Skill is the Schema for the skills 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: spec defines the desired state of Skill
+            properties:
+              body:
+                description: body contains the main content of the skill, part of
+                  the "SKILL.md" file.
+                type: string
+              description:
+                description: description provides a brief summary of the skill, part
+                  of the "SKILL.md" file.
+                type: string
+              files:
+                description: |-
+                  files is the list of files that will be used by the skill.
+                  scripts, references, assets, ...
+                items:
+                  properties:
+                    content:
+                      description: content is the content of the file.
+                      type: string
+                    path:
+                      description: path is the relative path of the file within the
+                        skill.
+                      type: string
+                  required:
+                  - content
+                  - path
+                  type: object
+                type: array
+              skillName:
+                description: skillName is the name of the skill, part of the "SKILL.md"
+                  file.
+                type: string
+              stackRef:
+                description: stackRef associates this tool with a stack.
+                properties:
+                  name:
+                    default: ""
+                    description: |-
+                      Name of the referent.
+                      This field is effectively required, but due to backwards compatibility is
+                      allowed to be empty. Instances of this type with an empty value here are
+                      almost certainly wrong.
+                      More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                    type: string
+                type: object
+                x-kubernetes-map-type: atomic
+            required:
+            - body
+            - description
+            - skillName
+            type: object
+          status:
+            description: status defines the observed state of Skill
+            properties:
+              conditions:
+                description: The status of each condition is one of True, False, or
+                  Unknown.
+                items:
+                  description: Condition contains details for one aspect of the current
+                    state of this API Resource.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+                x-kubernetes-list-map-keys:
+                - type
+                x-kubernetes-list-type: map
+              files:
+                description: files is the number of files contained in the skill.
+                format: int32
+                type: integer
+              observedGeneration:
+                description: observedGeneration is the .metadata.generation last reconciled.
+                format: int64
+                type: integer
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}

+ 1 - 0
config/crd/kustomization.yaml

@@ -11,6 +11,7 @@ resources:
 - bases/locostack.com_externalknowledgebases.yaml
 - bases/locostack.com_managedknowledgebases.yaml
 - bases/locostack.com_documents.yaml
+- bases/locostack.com_skills.yaml
 # +kubebuilder:scaffold:crdkustomizeresource
 
 patches:

+ 3 - 0
config/rbac/kustomization.yaml

@@ -22,6 +22,9 @@ resources:
 # default, aiding admins in cluster management. Those roles are
 # not used by the loco-operator itself. You can comment the following lines
 # if you do not want those helpers be installed with your Project.
+- skill_admin_role.yaml
+- skill_editor_role.yaml
+- skill_viewer_role.yaml
 - document_admin_role.yaml
 - document_editor_role.yaml
 - document_viewer_role.yaml

+ 3 - 0
config/rbac/role.yaml

@@ -53,6 +53,7 @@ rules:
   - managedknowledgebases
   - managedmodels
   - managedtools
+  - skills
   - stacks
   verbs:
   - create
@@ -73,6 +74,7 @@ rules:
   - managedknowledgebases/finalizers
   - managedmodels/finalizers
   - managedtools/finalizers
+  - skills/finalizers
   - stacks/finalizers
   verbs:
   - update
@@ -87,6 +89,7 @@ rules:
   - managedknowledgebases/status
   - managedmodels/status
   - managedtools/status
+  - skills/status
   - stacks/status
   verbs:
   - get

+ 27 - 0
config/rbac/skill_admin_role.yaml

@@ -0,0 +1,27 @@
+# This rule is not used by the project loco-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants full permissions ('*') over locostack.com.
+# 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: loco-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: skill-admin-role
+rules:
+- apiGroups:
+  - locostack.com
+  resources:
+  - skills
+  verbs:
+  - '*'
+- apiGroups:
+  - locostack.com
+  resources:
+  - skills/status
+  verbs:
+  - get

+ 33 - 0
config/rbac/skill_editor_role.yaml

@@ -0,0 +1,33 @@
+# This rule is not used by the project loco-operator 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 locostack.com.
+# 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: loco-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: skill-editor-role
+rules:
+- apiGroups:
+  - locostack.com
+  resources:
+  - skills
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - locostack.com
+  resources:
+  - skills/status
+  verbs:
+  - get

+ 29 - 0
config/rbac/skill_viewer_role.yaml

@@ -0,0 +1,29 @@
+# This rule is not used by the project loco-operator itself.
+# It is provided to allow the cluster admin to help manage permissions for users.
+#
+# Grants read-only access to locostack.com 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: loco-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: skill-viewer-role
+rules:
+- apiGroups:
+  - locostack.com
+  resources:
+  - skills
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - locostack.com
+  resources:
+  - skills/status
+  verbs:
+  - get

+ 1 - 0
config/samples/kustomization.yaml

@@ -9,4 +9,5 @@ resources:
 - v1alpha1_externalknowledgebase.yaml
 - v1alpha1_managedknowledgebase.yaml
 - v1alpha1_document.yaml
+- v1alpha1_skill.yaml
 # +kubebuilder:scaffold:manifestskustomizesamples

+ 9 - 0
config/samples/v1alpha1_skill.yaml

@@ -0,0 +1,9 @@
+apiVersion: locostack.com/v1alpha1
+kind: Skill
+metadata:
+  labels:
+    app.kubernetes.io/name: loco-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: skill-sample
+spec:
+  # TODO(user): Add fields here

+ 63 - 0
internal/controller/skill_controller.go

@@ -0,0 +1,63 @@
+/*
+Copyright 2026 LocoStack.
+
+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"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/LocoStack/loco-operator/api/v1alpha1"
+)
+
+// SkillReconciler reconciles a Skill object
+type SkillReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+// +kubebuilder:rbac:groups=locostack.com,resources=skills,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=locostack.com,resources=skills/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=locostack.com,resources=skills/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 Skill 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.23.3/pkg/reconcile
+func (r *SkillReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	_ = logf.FromContext(ctx)
+
+	// TODO(user): your logic here
+
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *SkillReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&v1alpha1.Skill{}).
+		Named("skill").
+		Complete(r)
+}

+ 84 - 0
internal/controller/skill_controller_test.go

@@ -0,0 +1,84 @@
+/*
+Copyright 2026 LocoStack.
+
+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"
+
+	locostackcomv1alpha1 "github.com/LocoStack/loco-operator/api/v1alpha1"
+)
+
+var _ = Describe("Skill 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
+		}
+		skill := &locostackcomv1alpha1.Skill{}
+
+		BeforeEach(func() {
+			By("creating the custom resource for the Kind Skill")
+			err := k8sClient.Get(ctx, typeNamespacedName, skill)
+			if err != nil && errors.IsNotFound(err) {
+				resource := &locostackcomv1alpha1.Skill{
+					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 := &locostackcomv1alpha1.Skill{}
+			err := k8sClient.Get(ctx, typeNamespacedName, resource)
+			Expect(err).NotTo(HaveOccurred())
+
+			By("Cleanup the specific resource instance Skill")
+			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
+		})
+		It("should successfully reconcile the resource", func() {
+			By("Reconciling the created resource")
+			controllerReconciler := &SkillReconciler{
+				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.
+		})
+	})
+})