diff --git a/pkg/mcp/common_test.go b/pkg/mcp/common_test.go index 892226ab..8546c4aa 100644 --- a/pkg/mcp/common_test.go +++ b/pkg/mcp/common_test.go @@ -222,120 +222,6 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *clientcmdapi.Config { return fakeConfig } -// withEnvTest sets up the environment for kubeconfig to be used with envTest -func (c *mcpContext) withEnvTest() { - c.withKubeConfig(envTestRestConfig) -} - -// inOpenShift sets up the kubernetes environment to seem to be running OpenShift -func inOpenShift(c *mcpContext) { - c.withEnvTest() - 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(c.ctx) - tasks.Go(func() error { - return c.crdApply(fmt.Sprintf(crdTemplate, "projects.project.openshift.io", "project.openshift.io", - "Cluster", "projects", "project", "Project")) - }) - tasks.Go(func() error { - return c.crdApply(fmt.Sprintf(crdTemplate, "routes.route.openshift.io", "route.openshift.io", - "Namespaced", "routes", "route", "Route")) - }) - if err := tasks.Wait(); err != nil { - panic(err) - } -} - -// inOpenShiftClear clears the kubernetes environment so it no longer seems to be running OpenShift -func inOpenShiftClear(c *mcpContext) { - tasks, _ := errgroup.WithContext(c.ctx) - tasks.Go(func() error { return c.crdDelete("projects.project.openshift.io") }) - tasks.Go(func() error { return c.crdDelete("routes.route.openshift.io") }) - if err := tasks.Wait(); err != nil { - panic(err) - } -} - -// newKubernetesClient creates a new Kubernetes client with the envTest kubeconfig -func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset { - return kubernetes.NewForConfigOrDie(envTestRestConfig) -} - -// newApiExtensionsClient creates a new ApiExtensions client with the envTest kubeconfig -func (c *mcpContext) newApiExtensionsClient() *apiextensionsv1.ApiextensionsV1Client { - return apiextensionsv1.NewForConfigOrDie(envTestRestConfig) -} - -// crdApply creates a CRD from the provided resource string and waits for it to be established -func (c *mcpContext) crdApply(resource string) error { - apiExtensionsV1Client := c.newApiExtensionsClient() - 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(c.ctx, crd, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("failed to create CRD %v", err) - } - c.crdWaitUntilReady(crd.Name) - return nil -} - -// crdDelete deletes a CRD by name and waits for it to be removed -func (c *mcpContext) crdDelete(name string) error { - apiExtensionsV1Client := c.newApiExtensionsClient() - err := apiExtensionsV1Client.CustomResourceDefinitions().Delete(c.ctx, name, metav1.DeleteOptions{ - GracePeriodSeconds: ptr.To(int64(0)), - }) - iteration := 0 - for iteration < 100 { - if _, derr := apiExtensionsV1Client.CustomResourceDefinitions().Get(c.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 -} - -// crdWaitUntilReady waits for a CRD to be established -func (c *mcpContext) crdWaitUntilReady(name string) { - watcher, err := c.newApiExtensionsClient().CustomResourceDefinitions().Watch(c.ctx, metav1.ListOptions{ - FieldSelector: "metadata.name=" + name, - }) - if err != nil { - panic(fmt.Errorf("failed to watch CRD %v", err)) - } - _, err = toolswatch.UntilWithoutRetry(c.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 { - panic(fmt.Errorf("failed to wait for CRD %v", err)) - } -} - // callTool helper function to call a tool by name with arguments func (c *mcpContext) callTool(name string, args map[string]interface{}) (*mcp.CallToolResult, error) { callToolRequest := mcp.CallToolRequest{} @@ -446,14 +332,53 @@ func (s *BaseMcpSuite) InitMcpClient(options ...transport.StreamableHTTPCOption) s.McpClient = test.NewMcpClient(s.T(), s.mcpServer.ServeHTTP(nil), options...) } -// CrdWaitUntilReady waits for a CRD to be established -func (s *BaseMcpSuite) CrdWaitUntilReady(name string) { +// 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(s.T().Context(), metav1.ListOptions{ + watcher, err := apiExtensionClient.CustomResourceDefinitions().Watch(ctx, metav1.ListOptions{ FieldSelector: "metadata.name=" + name, }) - s.Require().NoError(err, "failed to watch CRD") - _, err = toolswatch.UntilWithoutRetry(s.T().Context(), watcher, func(event watch.Event) (bool, error) { + 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 @@ -461,5 +386,43 @@ func (s *BaseMcpSuite) CrdWaitUntilReady(name string) { } return false, nil }) - s.Require().NoError(err, "failed to wait for CRD") + 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 } diff --git a/pkg/mcp/namespaces_test.go b/pkg/mcp/namespaces_test.go index a0a6ff23..25565512 100644 --- a/pkg/mcp/namespaces_test.go +++ b/pkg/mcp/namespaces_test.go @@ -13,9 +13,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "sigs.k8s.io/yaml" - - "github.com/containers/kubernetes-mcp-server/internal/test" - "github.com/containers/kubernetes-mcp-server/pkg/config" ) type NamespacesSuite struct { @@ -108,68 +105,67 @@ func (s *NamespacesSuite) TestNamespacesListAsTable() { }) } -func TestNamespaces(t *testing.T) { - suite.Run(t, new(NamespacesSuite)) -} +func (s *NamespacesSuite) TestProjectsListInOpenShift() { + s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift") + s.T().Cleanup(func() { + s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration") + }) + s.InitMcpClient() -func TestProjectsListInOpenShift(t *testing.T) { - testCaseWithContext(t, &mcpContext{before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) { + s.Run("projects_list returns project list in OpenShift", func() { dynamicClient := dynamic.NewForConfigOrDie(envTestRestConfig) _, _ = dynamicClient.Resource(schema.GroupVersionResource{Group: "project.openshift.io", Version: "v1", Resource: "projects"}). - Create(c.ctx, &unstructured.Unstructured{Object: map[string]interface{}{ + Create(s.T().Context(), &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": "project.openshift.io/v1", "kind": "Project", "metadata": map[string]interface{}{ "name": "an-openshift-project", }, }}, metav1.CreateOptions{}) - toolResult, err := c.callTool("projects_list", map[string]interface{}{}) - t.Run("projects_list returns project list", func(t *testing.T) { - if err != nil { - t.Fatalf("call tool failed %v", err) - } - if toolResult.IsError { - t.Fatalf("call tool failed") - } + toolResult, err := s.CallTool("projects_list", map[string]interface{}{}) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(toolResult.IsError, "call tool failed") }) var decoded []unstructured.Unstructured err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decoded) - t.Run("projects_list has yaml content", func(t *testing.T) { - if err != nil { - t.Fatalf("invalid tool result content %v", err) - } + s.Run("has yaml content", func() { + s.Nilf(err, "invalid tool result content %v", err) }) - t.Run("projects_list returns at least 1 items", func(t *testing.T) { - if len(decoded) < 1 { - t.Errorf("invalid project count, expected at least 1, got %v", len(decoded)) - } + s.Run("returns at least 1 item", func() { + s.GreaterOrEqualf(len(decoded), 1, "invalid project count, expected at least 1, got %v", len(decoded)) idx := slices.IndexFunc(decoded, func(ns unstructured.Unstructured) bool { return ns.GetName() == "an-openshift-project" }) - if idx == -1 { - t.Errorf("namespace %s not found in the list", "an-openshift-project") - } + s.NotEqualf(-1, idx, "namespace %s not found in the list", "an-openshift-project") }) }) } -func TestProjectsListInOpenShiftDenied(t *testing.T) { - deniedResourcesServer := test.Must(config.ReadToml([]byte(` +func (s *NamespacesSuite) TestProjectsListInOpenShiftDenied() { + s.Require().NoError(toml.Unmarshal([]byte(` denied_resources = [ { group = "project.openshift.io", version = "v1" } ] - `))) - testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer, before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) { - c.withEnvTest() - projectsList, _ := c.callTool("projects_list", map[string]interface{}{}) - t.Run("projects_list has error", func(t *testing.T) { - if !projectsList.IsError { - t.Fatalf("call tool should fail") - } + `), s.Cfg), "Expected to parse denied resources config") + s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift") + s.T().Cleanup(func() { + s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration") + }) + s.InitMcpClient() + + s.Run("projects_list (denied)", func() { + projectsList, err := s.CallTool("projects_list", map[string]interface{}{}) + s.Run("has error", func() { + s.Truef(projectsList.IsError, "call tool should fail") + s.Nilf(err, "call tool should not return error object") }) - t.Run("projects_list describes denial", func(t *testing.T) { + s.Run("describes denial", func() { expectedMessage := "failed to list projects: resource not allowed: project.openshift.io/v1, Kind=Project" - if projectsList.Content[0].(mcp.TextContent).Text != expectedMessage { - t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text) - } + s.Equalf(expectedMessage, projectsList.Content[0].(mcp.TextContent).Text, + "expected descriptive error '%s', got %v", expectedMessage, projectsList.Content[0].(mcp.TextContent).Text) }) }) } + +func TestNamespaces(t *testing.T) { + suite.Run(t, new(NamespacesSuite)) +} diff --git a/pkg/mcp/pods_run_test.go b/pkg/mcp/pods_run_test.go index 1dc751a9..4c329f3e 100644 --- a/pkg/mcp/pods_run_test.go +++ b/pkg/mcp/pods_run_test.go @@ -109,37 +109,33 @@ func (s *PodsRunSuite) TestPodsRunDenied() { }) } -func TestPodsRunInOpenShift(t *testing.T) { - testCaseWithContext(t, &mcpContext{before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) { - t.Run("pods_run with image, namespace, and port returns route with port", func(t *testing.T) { - podsRunInOpenShift, err := c.callTool("pods_run", map[string]interface{}{"image": "nginx", "port": 80}) - if err != nil { - t.Errorf("call tool failed %v", err) - return - } - if podsRunInOpenShift.IsError { - t.Errorf("call tool failed") - return - } - var decodedPodServiceRoute []unstructured.Unstructured - err = yaml.Unmarshal([]byte(podsRunInOpenShift.Content[0].(mcp.TextContent).Text), &decodedPodServiceRoute) - if err != nil { - t.Errorf("invalid tool result content %v", err) - return - } - if len(decodedPodServiceRoute) != 3 { - t.Errorf("invalid pods count, expected 3, got %v", len(decodedPodServiceRoute)) - return - } - if decodedPodServiceRoute[2].GetKind() != "Route" { - t.Errorf("invalid route kind, expected Route, got %v", decodedPodServiceRoute[2].GetKind()) - return - } +func (s *PodsRunSuite) TestPodsRunInOpenShift() { + s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift") + s.T().Cleanup(func() { + s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration") + }) + s.InitMcpClient() + + s.Run("pods_run(image=nginx, namespace=nil, port=80) returns route with port", func() { + podsRunInOpenShift, err := s.CallTool("pods_run", map[string]interface{}{"image": "nginx", "port": 80}) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(podsRunInOpenShift.IsError, "call tool failed") + }) + var decodedPodServiceRoute []unstructured.Unstructured + err = yaml.Unmarshal([]byte(podsRunInOpenShift.Content[0].(mcp.TextContent).Text), &decodedPodServiceRoute) + s.Run("has yaml content", func() { + s.Nilf(err, "invalid tool result content %v", err) + }) + s.Run("returns 3 items (Pod + Service + Route)", func() { + s.Lenf(decodedPodServiceRoute, 3, "invalid pods count, expected 3, got %v", len(decodedPodServiceRoute)) + s.Equalf("Pod", decodedPodServiceRoute[0].GetKind(), "invalid pod kind, expected Pod, got %v", decodedPodServiceRoute[0].GetKind()) + s.Equalf("Service", decodedPodServiceRoute[1].GetKind(), "invalid service kind, expected Service, got %v", decodedPodServiceRoute[1].GetKind()) + s.Equalf("Route", decodedPodServiceRoute[2].GetKind(), "invalid route kind, expected Route, got %v", decodedPodServiceRoute[2].GetKind()) + }) + s.Run("returns route with port", func() { targetPort := decodedPodServiceRoute[2].Object["spec"].(map[string]interface{})["port"].(map[string]interface{})["targetPort"].(int64) - if targetPort != 80 { - t.Errorf("invalid route target port, expected 80, got %v", targetPort) - return - } + s.Equalf(int64(80), targetPort, "invalid route target port, expected 80, got %v", targetPort) }) }) } diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index 8cff3e41..ddeec3ea 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -454,20 +454,26 @@ func (s *PodsSuite) TestPodsDeleteDenied() { }) } -func TestPodsDeleteInOpenShift(t *testing.T) { - testCaseWithContext(t, &mcpContext{before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) { +func (s *PodsSuite) TestPodsDeleteInOpenShift() { + s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift") + s.T().Cleanup(func() { + s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration") + }) + s.InitMcpClient() + + s.Run("pods_delete with managed pod in OpenShift", func() { managedLabels := map[string]string{ "app.kubernetes.io/managed-by": "kubernetes-mcp-server", "app.kubernetes.io/name": "a-manged-pod-to-delete", } - kc := c.newKubernetesClient() - _, _ = kc.CoreV1().Pods("default").Create(c.ctx, &corev1.Pod{ + kc := kubernetes.NewForConfigOrDie(envTestRestConfig) + _, _ = kc.CoreV1().Pods("default").Create(s.T().Context(), &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "a-managed-pod-to-delete-in-openshift", Labels: managedLabels}, Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}}, }, metav1.CreateOptions{}) dynamicClient := dynamic.NewForConfigOrDie(envTestRestConfig) _, _ = dynamicClient.Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}). - Namespace("default").Create(c.ctx, &unstructured.Unstructured{Object: map[string]interface{}{ + Namespace("default").Create(s.T().Context(), &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "metadata": map[string]interface{}{ @@ -475,36 +481,22 @@ func TestPodsDeleteInOpenShift(t *testing.T) { "labels": managedLabels, }, }}, metav1.CreateOptions{}) - podsDeleteManagedOpenShift, err := c.callTool("pods_delete", map[string]interface{}{ + podsDeleteManagedOpenShift, err := s.CallTool("pods_delete", map[string]interface{}{ "name": "a-managed-pod-to-delete-in-openshift", }) - t.Run("pods_delete with managed pod in OpenShift returns success", func(t *testing.T) { - if err != nil { - t.Errorf("call tool failed %v", err) - return - } - if podsDeleteManagedOpenShift.IsError { - t.Errorf("call tool failed") - return - } - if podsDeleteManagedOpenShift.Content[0].(mcp.TextContent).Text != "Pod deleted successfully" { - t.Errorf("invalid tool result content, got %v", podsDeleteManagedOpenShift.Content[0].(mcp.TextContent).Text) - return - } + s.Run("returns success", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(podsDeleteManagedOpenShift.IsError, "call tool failed") + s.Equalf("Pod deleted successfully", podsDeleteManagedOpenShift.Content[0].(mcp.TextContent).Text, + "invalid tool result content, got %v", podsDeleteManagedOpenShift.Content[0].(mcp.TextContent).Text) }) - t.Run("pods_delete with managed pod in OpenShift deletes Pod and Route", func(t *testing.T) { - p, pErr := kc.CoreV1().Pods("default").Get(c.ctx, "a-managed-pod-to-delete-in-openshift", metav1.GetOptions{}) - if pErr == nil && p != nil && p.DeletionTimestamp == nil { - t.Errorf("Pod not deleted") - return - } + s.Run("deletes Pod and Route", func() { + p, pErr := kc.CoreV1().Pods("default").Get(s.T().Context(), "a-managed-pod-to-delete-in-openshift", metav1.GetOptions{}) + s.False(pErr == nil && p != nil && p.DeletionTimestamp == nil, "Pod not deleted") r, rErr := dynamicClient. Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}). - Namespace("default").Get(c.ctx, "a-managed-route-to-delete", metav1.GetOptions{}) - if rErr == nil && r != nil && r.GetDeletionTimestamp() == nil { - t.Errorf("Route not deleted") - return - } + Namespace("default").Get(s.T().Context(), "a-managed-route-to-delete", metav1.GetOptions{}) + s.False(rErr == nil && r != nil && r.GetDeletionTimestamp() == nil, "Route not deleted") }) }) } diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index 83401377..21329d20 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -17,8 +17,6 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "sigs.k8s.io/yaml" - - "github.com/containers/kubernetes-mcp-server/pkg/output" ) type ResourcesSuite struct { @@ -141,31 +139,34 @@ func (s *ResourcesSuite) TestResourcesListDenied() { }) } -func TestResourcesListAsTable(t *testing.T) { - testCaseWithContext(t, &mcpContext{listOutput: output.Table, before: inOpenShift, after: inOpenShiftClear}, func(c *mcpContext) { - c.withEnvTest() - kc := c.newKubernetesClient() - _, _ = kc.CoreV1().ConfigMaps("default").Create(t.Context(), &corev1.ConfigMap{ +func (s *ResourcesSuite) TestResourcesListAsTable() { + s.Cfg.ListOutput = "table" + s.Require().NoError(EnvTestInOpenShift(s.T().Context()), "Expected to configure test for OpenShift") + s.T().Cleanup(func() { + s.Require().NoError(EnvTestInOpenShiftClear(s.T().Context()), "Expected to clear OpenShift test configuration") + }) + s.InitMcpClient() + + s.Run("resources_list(apiVersion=v1, kind=ConfigMap) (list_output=table)", func() { + kc := kubernetes.NewForConfigOrDie(envTestRestConfig) + _, _ = kc.CoreV1().ConfigMaps("default").Create(s.T().Context(), &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "a-configmap-to-list-as-table", Labels: map[string]string{"resource": "config-map"}}, Data: map[string]string{"key": "value"}, }, metav1.CreateOptions{}) - configMapList, err := c.callTool("resources_list", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap"}) - t.Run("resources_list returns ConfigMap list", func(t *testing.T) { - if err != nil { - t.Fatalf("call tool failed %v", err) - } - if configMapList.IsError { - t.Fatalf("call tool failed") - } + configMapList, err := s.CallTool("resources_list", map[string]interface{}{"apiVersion": "v1", "kind": "ConfigMap"}) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(configMapList.IsError, "call tool failed") }) + s.Require().NotNil(configMapList, "Expected tool result from call") outConfigMapList := configMapList.Content[0].(mcp.TextContent).Text - t.Run("resources_list returns column headers for ConfigMap list", func(t *testing.T) { + s.Run("returns column headers for ConfigMap list", func() { expectedHeaders := "NAMESPACE\\s+APIVERSION\\s+KIND\\s+NAME\\s+DATA\\s+AGE\\s+LABELS" - if m, e := regexp.MatchString(expectedHeaders, outConfigMapList); !m || e != nil { - t.Fatalf("Expected headers '%s' not found in output:\n%s", expectedHeaders, outConfigMapList) - } + m, e := regexp.MatchString(expectedHeaders, outConfigMapList) + s.Truef(m, "Expected headers '%s' not found in output:\n%s", expectedHeaders, outConfigMapList) + s.NoErrorf(e, "Error matching headers regex: %v", e) }) - t.Run("resources_list returns formatted row for a-configmap-to-list-as-table", func(t *testing.T) { + s.Run("returns formatted row for a-configmap-to-list-as-table", func() { expectedRow := "(?default)\\s+" + "(?v1)\\s+" + "(?ConfigMap)\\s+" + @@ -173,47 +174,46 @@ func TestResourcesListAsTable(t *testing.T) { "(?1)\\s+" + "(?(\\d+m)?(\\d+s)?)\\s+" + "(?resource=config-map)" - if m, e := regexp.MatchString(expectedRow, outConfigMapList); !m || e != nil { - t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, outConfigMapList) - } + m, e := regexp.MatchString(expectedRow, outConfigMapList) + s.Truef(m, "Expected row '%s' not found in output:\n%s", expectedRow, outConfigMapList) + s.NoErrorf(e, "Error matching row regex: %v", e) }) - // Custom Resource List + }) + + s.Run("resources_list(apiVersion=route.openshift.io/v1, kind=Route) (list_output=table)", func() { _, _ = dynamic.NewForConfigOrDie(envTestRestConfig). Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}). Namespace("default"). - Create(c.ctx, &unstructured.Unstructured{Object: map[string]interface{}{ + Create(s.T().Context(), &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": "route.openshift.io/v1", "kind": "Route", "metadata": map[string]interface{}{ "name": "an-openshift-route-to-list-as-table", }, }}, metav1.CreateOptions{}) - routeList, err := c.callTool("resources_list", map[string]interface{}{"apiVersion": "route.openshift.io/v1", "kind": "Route"}) - t.Run("resources_list returns Route list", func(t *testing.T) { - if err != nil { - t.Fatalf("call tool failed %v", err) - } - if routeList.IsError { - t.Fatalf("call tool failed") - } + routeList, err := s.CallTool("resources_list", map[string]interface{}{"apiVersion": "route.openshift.io/v1", "kind": "Route"}) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(routeList.IsError, "call tool failed") }) + s.Require().NotNil(routeList, "Expected tool result from call") outRouteList := routeList.Content[0].(mcp.TextContent).Text - t.Run("resources_list returns column headers for Route list", func(t *testing.T) { + s.Run("returns column headers for Route list", func() { expectedHeaders := "NAMESPACE\\s+APIVERSION\\s+KIND\\s+NAME\\s+AGE\\s+LABELS" - if m, e := regexp.MatchString(expectedHeaders, outRouteList); !m || e != nil { - t.Fatalf("Expected headers '%s' not found in output:\n%s", expectedHeaders, outRouteList) - } + m, e := regexp.MatchString(expectedHeaders, outRouteList) + s.Truef(m, "Expected headers '%s' not found in output:\n%s", expectedHeaders, outRouteList) + s.NoErrorf(e, "Error matching headers regex: %v", e) }) - t.Run("resources_list returns formatted row for an-openshift-route-to-list-as-table", func(t *testing.T) { + s.Run("returns formatted row for an-openshift-route-to-list-as-table", func() { expectedRow := "(?default)\\s+" + "(?route.openshift.io/v1)\\s+" + "(?Route)\\s+" + "(?an-openshift-route-to-list-as-table)\\s+" + "(?(\\d+m)?(\\d+s)?)\\s+" + "(?)" - if m, e := regexp.MatchString(expectedRow, outRouteList); !m || e != nil { - t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, outRouteList) - } + m, e := regexp.MatchString(expectedRow, outRouteList) + s.Truef(m, "Expected row '%s' not found in output:\n%s", expectedRow, outRouteList) + s.NoErrorf(e, "Error matching row regex: %v", e) }) }) } @@ -393,7 +393,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.CrdWaitUntilReady("customs.example.com") + s.Require().NoError(EnvTestCrdWaitUntilReady(s.T().Context(), "customs.example.com")) }) s.Run("resources_create_or_update creates custom resource", func() {