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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require (
github.com/google/jsonschema-go v0.3.0
github.com/mark3labs/mcp-go v0.43.1
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/pkg/errors v0.9.1
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
Expand Down Expand Up @@ -104,6 +103,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
Expand Down
137 changes: 137 additions & 0 deletions pkg/mcp/common_crd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package mcp

import (
"context"
"fmt"
"time"

"golang.org/x/sync/errgroup"
apiextensionsv1spec "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/discovery"
"k8s.io/utils/ptr"
)

func CRD(group, version, resource, kind, singular string, namespaced bool) *apiextensionsv1spec.CustomResourceDefinition {
scope := "Cluster"
if namespaced {
scope = "Namespaced"
}
crd := &apiextensionsv1spec.CustomResourceDefinition{
TypeMeta: metav1.TypeMeta{
APIVersion: apiextensionsv1spec.SchemeGroupVersion.String(),
Kind: "CustomResourceDefinition",
},
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s.%s", resource, group)},
Spec: apiextensionsv1spec.CustomResourceDefinitionSpec{
Group: group,
Versions: []apiextensionsv1spec.CustomResourceDefinitionVersion{
{
Name: version,
Served: false,
Storage: true,
Schema: &apiextensionsv1spec.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1spec.JSONSchemaProps{
Type: "object",
XPreserveUnknownFields: ptr.To(true),
},
},
},
},
Scope: apiextensionsv1spec.ResourceScope(scope),
Names: apiextensionsv1spec.CustomResourceDefinitionNames{
Plural: resource,
Singular: singular,
Kind: kind,
ShortNames: []string{singular},
},
},
}
return crd
}

func EnvTestEnableCRD(ctx context.Context, group, version, resource string) error {
apiExtensionsV1Client := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
_, err := apiExtensionsV1Client.CustomResourceDefinitions().Patch(
ctx,
fmt.Sprintf("%s.%s", resource, group),
types.JSONPatchType,
[]byte(`[{"op": "replace", "path": "/spec/versions/0/served", "value": true}]`),
metav1.PatchOptions{})
if err != nil {
return err
}
return EnvTestWaitForAPIResourceCondition(ctx, group, version, resource, true)
}

func EnvTestDisableCRD(ctx context.Context, group, version, resource string) error {
apiExtensionsV1Client := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
_, err := apiExtensionsV1Client.CustomResourceDefinitions().Patch(
ctx,
fmt.Sprintf("%s.%s", resource, group),
types.JSONPatchType,
[]byte(`[{"op": "replace", "path": "/spec/versions/0/served", "value": false}]`),
metav1.PatchOptions{})
if err != nil {
return err
}
return EnvTestWaitForAPIResourceCondition(ctx, group, version, resource, false)
}

func EnvTestWaitForAPIResourceCondition(ctx context.Context, group, version, resource string, shouldBeAvailable bool) error {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(envTestRestConfig)
if err != nil {
return fmt.Errorf("failed to create discovery client: %w", err)
}

groupVersion := fmt.Sprintf("%s/%s", group, version)
if group == "" {
groupVersion = version
}

return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 10*time.Second, true, func(ctx context.Context) (bool, error) {
resourceList, err := discoveryClient.ServerResourcesForGroupVersion(groupVersion)
if err != nil {
// If we're waiting for the resource to be unavailable and we get an error, it might be gone
if !shouldBeAvailable {
return true, nil
}
// Otherwise, keep polling
return false, nil
}

// Check if the resource exists in the list
found := false
for _, apiResource := range resourceList.APIResources {
if apiResource.Name == resource {
found = true
break
}
}

// Return true if the condition is met
if shouldBeAvailable {
return found, nil
}
return !found, nil
})
}

// EnvTestInOpenShift sets up the kubernetes environment to seem to be running OpenShift
func EnvTestInOpenShift(ctx context.Context) error {
tasks, _ := errgroup.WithContext(ctx)
tasks.Go(func() error { return EnvTestEnableCRD(ctx, "project.openshift.io", "v1", "projects") })
tasks.Go(func() error { return EnvTestEnableCRD(ctx, "route.openshift.io", "v1", "routes") })
return tasks.Wait()
}

// EnvTestInOpenShiftClear clears the kubernetes environment so it no longer seems to be running OpenShift
func EnvTestInOpenShiftClear(ctx context.Context) error {
tasks, _ := errgroup.WithContext(ctx)
tasks.Go(func() error { return EnvTestDisableCRD(ctx, "project.openshift.io", "v1", "projects") })
tasks.Go(func() error { return EnvTestDisableCRD(ctx, "route.openshift.io", "v1", "routes") })
return tasks.Wait()
}
120 changes: 16 additions & 104 deletions pkg/mcp/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,20 @@ package mcp

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"time"

"github.com/mark3labs/mcp-go/client/transport"
"github.com/pkg/errors"
"github.com/spf13/afero"
"github.com/stretchr/testify/suite"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1spec "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
toolswatch "k8s.io/client-go/tools/watch"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/env"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/remote"
Expand All @@ -49,6 +40,8 @@ func TestMain(m *testing.M) {
// Set high rate limits to avoid client-side throttling in tests
_ = os.Setenv("KUBE_CLIENT_QPS", "1000")
_ = os.Setenv("KUBE_CLIENT_BURST", "2000")
//// Enable control plane output to see API server logs
//_ = os.Setenv("KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT", "true")
envTestDir, err := store.DefaultStoreDir()
if err != nil {
panic(err)
Expand All @@ -73,7 +66,21 @@ func TestMain(m *testing.M) {
versionDir := envTestEnv.Platform.BaseName(*envTestEnv.Version.AsConcrete())
envTest = &envtest.Environment{
BinaryAssetsDirectory: filepath.Join(envTestDir, "k8s", versionDir),
CRDs: []*apiextensionsv1spec.CustomResourceDefinition{
CRD("project.openshift.io", "v1", "projects", "Project", "project", false),
CRD("route.openshift.io", "v1", "routes", "Route", "route", true),
},
}
// Configure API server for faster CRD establishment and test performance
envTest.ControlPlane.GetAPIServer().Configure().
// Increase concurrent request limits for faster parallel operations
Set("max-requests-inflight", "1000").
Set("max-mutating-requests-inflight", "500").
// Speed up namespace cleanup with more workers
Set("delete-collection-workers", "10") //.
// Enable verbose logging for debugging
//Set("v", "9")

adminSystemMasterBaseConfig, _ := envTest.Start()
au := test.Must(envTest.AddUser(envTestUser, adminSystemMasterBaseConfig))
envTestRestConfig = au.Config()
Expand Down Expand Up @@ -195,98 +202,3 @@ func (s *BaseMcpSuite) InitMcpClient(options ...transport.StreamableHTTPCOption)
s.Require().NoError(err, "Expected no error creating MCP server")
s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(), options...)
}

// EnvTestInOpenShift sets up the kubernetes environment to seem to be running OpenShift
func EnvTestInOpenShift(ctx context.Context) error {
crdTemplate := `
{
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": {"name": "%s"},
"spec": {
"group": "%s",
"versions": [{
"name": "v1","served": true,"storage": true,
"schema": {"openAPIV3Schema": {"type": "object","x-kubernetes-preserve-unknown-fields": true}}
}],
"scope": "%s",
"names": {"plural": "%s","singular": "%s","kind": "%s"}
}
}`
tasks, _ := errgroup.WithContext(ctx)
tasks.Go(func() error {
return EnvTestCrdApply(ctx, fmt.Sprintf(crdTemplate, "projects.project.openshift.io", "project.openshift.io",
"Cluster", "projects", "project", "Project"))
})
tasks.Go(func() error {
return EnvTestCrdApply(ctx, fmt.Sprintf(crdTemplate, "routes.route.openshift.io", "route.openshift.io",
"Namespaced", "routes", "route", "Route"))
})
return tasks.Wait()
}

// EnvTestInOpenShiftClear clears the kubernetes environment so it no longer seems to be running OpenShift
func EnvTestInOpenShiftClear(ctx context.Context) error {
tasks, _ := errgroup.WithContext(ctx)
tasks.Go(func() error { return EnvTestCrdDelete(ctx, "projects.project.openshift.io") })
tasks.Go(func() error { return EnvTestCrdDelete(ctx, "routes.route.openshift.io") })
return tasks.Wait()
}

// EnvTestCrdWaitUntilReady waits for a CRD to be established
func EnvTestCrdWaitUntilReady(ctx context.Context, name string) error {
apiExtensionClient := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
watcher, err := apiExtensionClient.CustomResourceDefinitions().Watch(ctx, metav1.ListOptions{
FieldSelector: "metadata.name=" + name,
})
if err != nil {
return fmt.Errorf("unable to watch CRDs: %w", err)
}
_, err = toolswatch.UntilWithoutRetry(ctx, watcher, func(event watch.Event) (bool, error) {
for _, c := range event.Object.(*apiextensionsv1spec.CustomResourceDefinition).Status.Conditions {
if c.Type == apiextensionsv1spec.Established && c.Status == apiextensionsv1spec.ConditionTrue {
return true, nil
}
}
return false, nil
})
if err != nil {
return fmt.Errorf("failed to wait for CRD: %w", err)
}
return nil
}

// EnvTestCrdApply creates a CRD from the provided resource string and waits for it to be established
func EnvTestCrdApply(ctx context.Context, resource string) error {
apiExtensionsV1Client := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
var crd = &apiextensionsv1spec.CustomResourceDefinition{}
err := json.Unmarshal([]byte(resource), crd)
if err != nil {
return fmt.Errorf("failed to create CRD %v", err)
}
_, err = apiExtensionsV1Client.CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create CRD %v", err)
}
return EnvTestCrdWaitUntilReady(ctx, crd.Name)
}

// crdDelete deletes a CRD by name and waits for it to be removed
func EnvTestCrdDelete(ctx context.Context, name string) error {
apiExtensionsV1Client := apiextensionsv1.NewForConfigOrDie(envTestRestConfig)
err := apiExtensionsV1Client.CustomResourceDefinitions().Delete(ctx, name, metav1.DeleteOptions{
GracePeriodSeconds: ptr.To(int64(0)),
})
iteration := 0
for iteration < 100 {
if _, derr := apiExtensionsV1Client.CustomResourceDefinitions().Get(ctx, name, metav1.GetOptions{}); derr != nil {
break
}
time.Sleep(5 * time.Millisecond)
iteration++
}
if err != nil {
return errors.Wrap(err, "failed to delete CRD")
}
return nil
}
2 changes: 1 addition & 1 deletion pkg/mcp/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ func (s *ResourcesSuite) TestResourcesCreateOrUpdate() {
_, err = apiExtensionsV1Client.CustomResourceDefinitions().Get(s.T().Context(), "customs.example.com", metav1.GetOptions{})
s.Nilf(err, "custom resource definition not found")
})
s.Require().NoError(EnvTestCrdWaitUntilReady(s.T().Context(), "customs.example.com"))
s.Require().NoError(EnvTestWaitForAPIResourceCondition(s.T().Context(), "example.com", "v1", "customs", true))
})

s.Run("resources_create_or_update creates custom resource", func() {
Expand Down