这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"go.toolsEnvVars": {
"GO111MODULE": "on"
},
"go.useLanguageServer": true,
"go.workspaceFolder": "${workspaceFolder}/kubechain",
"go.coverShowCounts": true,
"gopls": {
"formatting.gofumpt": true,
"ui.semanticTokens": true
}
}
7 changes: 7 additions & 0 deletions kubechain/api/v1alpha1/contactchannel_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ type ContactChannelSpec struct {
type ContactChannelStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Ready bool `json:"ready,omitempty"`

// +kubebuilder:validation:Enum=Ready;Error;Pending
Status string `json:"status,omitempty"`

// StatusDetail provides additional details about the current status
StatusDetail string `json:"statusDetail,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
4 changes: 4 additions & 0 deletions kubechain/api/v1alpha1/mcpserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type MCPServerSpec struct {
// ResourceRequirements defines CPU/Memory resources requests/limits
// +optional
Resources ResourceRequirements `json:"resources,omitempty"`

// ApprovalContactChannel is the contact channel for approval
// +optional
ApprovalContactChannel *LocalObjectReference `json:"approvalContactChannel,omitempty"`
}

// EnvVar represents an environment variable
Expand Down
5 changes: 5 additions & 0 deletions kubechain/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ spec:
type: object
status:
description: ContactChannelStatus defines the observed state of ContactChannel.
properties:
ready:
description: |-
INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
Important: Run "make" to regenerate code after modifying this file
type: boolean
status:
enum:
- Ready
- Error
- Pending
type: string
statusDetail:
description: StatusDetail provides additional details about the current
status
type: string
type: object
type: object
served: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ spec:
spec:
description: MCPServerSpec defines the desired state of MCPServer
properties:
approvalContactChannel:
description: ApprovalContactChannel is the contact channel for approval
properties:
name:
description: Name of the referent
minLength: 1
type: string
required:
- name
type: object
args:
description: Args are the arguments to pass to the command for stdio
MCP servers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -17,7 +18,9 @@ import (
)

const (
StatusError = "Error"
StatusError = "Error"
StatusPending = "Pending"
StatusReady = "Ready"
)

// MCPServerManagerInterface defines the interface for MCP server management
Expand Down Expand Up @@ -84,6 +87,34 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
// Create a status update copy
statusUpdate := mcpServer.DeepCopy()

if statusUpdate.Spec.ApprovalContactChannel != nil {
// validate the approval contact channel
approvalContactChannel := &kubechainv1alpha1.ContactChannel{}
err := r.Get(ctx, types.NamespacedName{Name: statusUpdate.Spec.ApprovalContactChannel.Name, Namespace: statusUpdate.Namespace}, approvalContactChannel)
if err != nil {
statusUpdate.Status.Connected = false
statusUpdate.Status.Status = StatusError
// todo handle other types of error, not just "not found"
statusUpdate.Status.StatusDetail = fmt.Sprintf("ContactChannel %q not found", statusUpdate.Spec.ApprovalContactChannel.Name)
r.recorder.Event(&mcpServer, corev1.EventTypeWarning, "ContactChannelNotFound", fmt.Sprintf("ContactChannel %q not found", statusUpdate.Spec.ApprovalContactChannel.Name))
if err := r.updateStatus(ctx, req, statusUpdate); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, err
}

if !approvalContactChannel.Status.Ready {
statusUpdate.Status.Connected = false
statusUpdate.Status.Status = StatusPending
statusUpdate.Status.StatusDetail = fmt.Sprintf("ContactChannel %q is not ready", statusUpdate.Spec.ApprovalContactChannel.Name)
r.recorder.Event(&mcpServer, corev1.EventTypeWarning, "ContactChannelNotReady", fmt.Sprintf("ContactChannel %q is not ready", statusUpdate.Spec.ApprovalContactChannel.Name))
if err := r.updateStatus(ctx, req, statusUpdate); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: time.Second * 5}, nil
}
}

// Basic validation
if err := r.validateMCPServer(&mcpServer); err != nil {
statusUpdate.Status.Connected = false
Expand Down
112 changes: 112 additions & 0 deletions kubechain/internal/controller/mcpserver/mcpserver_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

kubechainv1alpha1 "github.com/humanlayer/smallchain/kubechain/api/v1alpha1"
"github.com/humanlayer/smallchain/kubechain/internal/mcpmanager"
"github.com/humanlayer/smallchain/kubechain/test/utils"
)

// MockMCPServerManager is a mock implementation of the MCPServerManager for testing
Expand Down Expand Up @@ -196,5 +197,116 @@ var _ = Describe("MCPServer Controller", func() {
By("Cleaning up the invalid MCPServer")
Expect(k8sClient.Delete(ctx, invalidMCPServer)).To(Succeed())
})

It("Should error if the approval contact channel is non-existent", func() {
ctx := context.Background()

By("Creating a new MCPServer with non-existent approval contact channel reference")
mcpServer := &kubechainv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "mcpserver-missing-channel",
Namespace: MCPServerNamespace,
},
Spec: kubechainv1alpha1.MCPServerSpec{
Transport: "stdio",
Command: "test-command",
ApprovalContactChannel: &kubechainv1alpha1.LocalObjectReference{
Name: "non-existent-channel",
},
},
}

Expect(k8sClient.Create(ctx, mcpServer)).To(Succeed())

By("Creating a controller with a mock manager")
recorder := record.NewFakeRecorder(10)
reconciler := &MCPServerReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
recorder: recorder,
MCPManager: &MockMCPServerManager{},
}

By("Reconciling the MCPServer with non-existent contact channel")
_, err := reconciler.Reconcile(ctx, ctrl.Request{
NamespacedName: types.NamespacedName{Name: "mcpserver-missing-channel", Namespace: MCPServerNamespace},
})
Expect(err).To(HaveOccurred()) // Should fail because contact channel doesn't exist

By("Checking that the status was updated correctly to reflect the error")
createdMCPServer := &kubechainv1alpha1.MCPServer{}
err = k8sClient.Get(ctx, types.NamespacedName{Name: "mcpserver-missing-channel", Namespace: MCPServerNamespace}, createdMCPServer)
Expect(err).NotTo(HaveOccurred())
Expect(createdMCPServer.Status.Connected).To(BeFalse())
Expect(createdMCPServer.Status.Status).To(Equal("Error"))
Expect(createdMCPServer.Status.StatusDetail).To(ContainSubstring("ContactChannel \"non-existent-channel\" not found"))
By("Checking that the event was emitted")
utils.ExpectRecorder(recorder).ToEmitEventContaining("ContactChannelNotFound")

By("Cleaning up the MCPServer")
Expect(k8sClient.Delete(ctx, mcpServer)).To(Succeed())
})

It("Should stay in pending if the approval contact channel is not ready", func() {
ctx := context.Background()

By("Creating a new MCPServer with approval contact channel reference")
mcpServer := &kubechainv1alpha1.MCPServer{
ObjectMeta: metav1.ObjectMeta{
Name: "mcpserver-channel-ready",
Namespace: MCPServerNamespace,
},
Spec: kubechainv1alpha1.MCPServerSpec{
Transport: "stdio",
Command: "test-command",
ApprovalContactChannel: &kubechainv1alpha1.LocalObjectReference{
Name: "test-channel",
},
},
}

Expect(k8sClient.Create(ctx, mcpServer)).To(Succeed())

By("Creating the contact channel in not-ready state")
contactChannel := &kubechainv1alpha1.ContactChannel{
ObjectMeta: metav1.ObjectMeta{
Name: "test-channel",
Namespace: MCPServerNamespace,
},
Status: kubechainv1alpha1.ContactChannelStatus{
Ready: false,
Status: "Pending",
StatusDetail: "Initializing",
},
}
Expect(k8sClient.Create(ctx, contactChannel)).To(Succeed())

By("Reconciling the MCPServer with not-ready contact channel")
recorder := record.NewFakeRecorder(10)
reconciler := &MCPServerReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
recorder: recorder,
MCPManager: &MockMCPServerManager{},
}

result, err := reconciler.Reconcile(ctx, ctrl.Request{
NamespacedName: types.NamespacedName{Name: "mcpserver-channel-ready", Namespace: MCPServerNamespace},
})
Expect(err).NotTo(HaveOccurred()) // Should stay in pending because contact channel is not ready
Expect(result.RequeueAfter).To(Equal(time.Second * 5))

By("Checking that the status was updated to Pending")
createdMCPServer := &kubechainv1alpha1.MCPServer{}
err = k8sClient.Get(ctx, types.NamespacedName{Name: "mcpserver-channel-ready", Namespace: MCPServerNamespace}, createdMCPServer)
Expect(err).NotTo(HaveOccurred())
Expect(createdMCPServer.Status.Status).To(Equal("Pending"))
Expect(createdMCPServer.Status.StatusDetail).To(ContainSubstring("ContactChannel \"test-channel\" is not ready"))
utils.ExpectRecorder(recorder).ToEmitEventContaining("ContactChannelNotReady")

By("Cleaning up the MCPServer and ContactChannel")
Expect(k8sClient.Delete(ctx, mcpServer)).To(Succeed())
Expect(k8sClient.Delete(ctx, contactChannel)).To(Succeed())
})
})
})
14 changes: 8 additions & 6 deletions kubechain/internal/controller/mcpserver/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import (
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var cancel context.CancelFunc
var ctx context.Context
var (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
cancel context.CancelFunc
ctx context.Context
)

func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
Expand All @@ -46,6 +48,7 @@ var _ = BeforeSuite(func() {
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", "1.32.0-darwin-arm64"),
}

var err error
Expand Down Expand Up @@ -85,7 +88,6 @@ var _ = BeforeSuite(func() {
err = k8sManager.Start(ctx)
Expect(err).ToNot(HaveOccurred(), "Failed to run manager")
}()

})

var _ = AfterSuite(func() {
Expand Down