From b92c5082e773276f4b187355cb46fe3f0f1c3a8f Mon Sep 17 00:00:00 2001 From: phani2898 Date: Fri, 20 Mar 2026 11:27:57 +0530 Subject: [PATCH] add namespace support to resources_create_or_update similar to kubectl apply -f Signed-off-by: phani2898 --- pkg/kubernetes/pods.go | 2 +- pkg/kubernetes/resources.go | 11 +++-- pkg/mcp/resources_test.go | 61 +++++++++++++++++++++++++ pkg/toolsets/core/resources.go | 14 +++++- pkg/toolsets/kubevirt/vm/create/tool.go | 2 +- 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go index 29f1864c5..b3df242ba 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -193,7 +193,7 @@ func (c *Core) PodsRun(ctx context.Context, namespace, name, image string, port } toCreate = append(toCreate, u) } - return c.resourcesCreateOrUpdate(ctx, toCreate) + return c.resourcesCreateOrUpdate(ctx, toCreate, "") } func (c *Core) PodsTop(ctx context.Context, options api.PodsTopOptions) (*metrics.PodMetricsList, error) { diff --git a/pkg/kubernetes/resources.go b/pkg/kubernetes/resources.go index 0115c9455..c1e435be9 100644 --- a/pkg/kubernetes/resources.go +++ b/pkg/kubernetes/resources.go @@ -55,7 +55,7 @@ func (c *Core) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, n return c.DynamicClient().Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) } -func (c *Core) ResourcesCreateOrUpdate(ctx context.Context, resource string) ([]*unstructured.Unstructured, error) { +func (c *Core) ResourcesCreateOrUpdate(ctx context.Context, resource string, namespaceOverride string) ([]*unstructured.Unstructured, error) { separator := regexp.MustCompile(`\r?\n---\r?\n`) resources := separator.Split(resource, -1) var parsedResources []*unstructured.Unstructured @@ -70,7 +70,7 @@ func (c *Core) ResourcesCreateOrUpdate(ctx context.Context, resource string) ([] parsedResources = append(parsedResources, &obj) } - return c.resourcesCreateOrUpdate(ctx, parsedResources) + return c.resourcesCreateOrUpdate(ctx, parsedResources, namespaceOverride) } func (c *Core) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string, gracePeriodSeconds *int64) error { @@ -177,7 +177,7 @@ func (c *Core) resourcesListAsTable(ctx context.Context, gvk *schema.GroupVersio return &unstructured.Unstructured{Object: unstructuredObject}, err } -func (c *Core) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured) ([]*unstructured.Unstructured, error) { +func (c *Core) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured, namespaceOverride string) ([]*unstructured.Unstructured, error) { for i, obj := range resources { gvk := obj.GroupVersionKind() gvr, rErr := c.resourceFor(&gvk) @@ -186,6 +186,11 @@ func (c *Core) resourcesCreateOrUpdate(ctx context.Context, resources []*unstruc } namespace := obj.GetNamespace() + // If a namespace override was provided, it takes precedence over the namespace in the resource metadata + if namespaceOverride != "" { + namespace = namespaceOverride + obj.SetNamespace(namespace) + } // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := c.isNamespaced(&gvk); nsErr == nil && namespaced { namespace = c.NamespaceOrDefault(namespace) diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index 0f4c94177..ea93f183e 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -564,6 +564,67 @@ func (s *ResourcesSuite) TestResourcesCreateOrUpdate() { s.Falsef(hasStatus, "status should not be present on the persisted resource") }) }) + + s.Run("resources_create_or_update with namespace override and no namespace in resource", func() { + // Resource YAML has no namespace in metadata — namespace override should be applied + configMapYaml := "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: a-cm-ns-override\ndata:\n key: value\n" + result, err := s.CallTool("resources_create_or_update", map[string]interface{}{ + "resource": configMapYaml, + "namespace": "default", + }) + s.Run("returns success", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(result.IsError, "call tool should not fail, got: %v", result.Content) + }) + s.Run("creates ConfigMap in the overridden namespace", func() { + cm, cmErr := client.CoreV1().ConfigMaps("default").Get(s.T().Context(), "a-cm-ns-override", metav1.GetOptions{}) + s.Require().Nilf(cmErr, "ConfigMap not found in overridden namespace") + s.Equalf("default", cm.Namespace, "ConfigMap should be in the overridden namespace") + }) + }) + + s.Run("resources_create_or_update with namespace override takes precedence over namespace in resource", func() { + // Resource YAML has namespace: default but override should win + configMapYaml := "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: a-cm-ns-override-wins\n namespace: default\ndata:\n key: value\n" + result, err := s.CallTool("resources_create_or_update", map[string]interface{}{ + "resource": configMapYaml, + "namespace": "kube-public", + }) + s.Run("returns success", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(result.IsError, "call tool should not fail, got: %v", result.Content) + }) + s.Run("creates ConfigMap in the overridden namespace not the one in the resource", func() { + cm, cmErr := client.CoreV1().ConfigMaps("kube-public").Get(s.T().Context(), "a-cm-ns-override-wins", metav1.GetOptions{}) + s.Require().Nilf(cmErr, "ConfigMap not found in overridden namespace") + s.Equalf("kube-public", cm.Namespace, "ConfigMap should be in the overridden namespace, not the one in the resource metadata") + }) + s.Run("does not create ConfigMap in the namespace from the resource metadata", func() { + _, cmErr := client.CoreV1().ConfigMaps("default").Get(s.T().Context(), "a-cm-ns-override-wins", metav1.GetOptions{}) + s.Errorf(cmErr, "ConfigMap should not exist in the namespace from the resource metadata") + }) + }) + + s.Run("resources_create_or_update with namespace override applies to all resources in multi-document YAML", func() { + multiDocYaml := "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: a-cm-multi-1\ndata:\n key: value1\n\n---\n\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: a-cm-multi-2\ndata:\n key: value2\n" + result, err := s.CallTool("resources_create_or_update", map[string]interface{}{ + "resource": multiDocYaml, + "namespace": "default", + }) + s.Run("returns success", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(result.IsError, "call tool should not fail, got: %v", result.Content) + }) + s.Run("creates all ConfigMaps in the overridden namespace", func() { + cm1, cm1Err := client.CoreV1().ConfigMaps("default").Get(s.T().Context(), "a-cm-multi-1", metav1.GetOptions{}) + s.Require().Nilf(cm1Err, "first ConfigMap not found in overridden namespace") + s.Equalf("default", cm1.Namespace, "first ConfigMap should be in the overridden namespace") + + cm2, cm2Err := client.CoreV1().ConfigMaps("default").Get(s.T().Context(), "a-cm-multi-2", metav1.GetOptions{}) + s.Require().Nilf(cm2Err, "second ConfigMap not found in overridden namespace") + s.Equalf("default", cm2.Namespace, "second ConfigMap should be in the overridden namespace") + }) + }) } func (s *ResourcesSuite) TestResourcesCreateOrUpdateForcesSSA() { diff --git a/pkg/toolsets/core/resources.go b/pkg/toolsets/core/resources.go index cfefdd396..6a5629d55 100644 --- a/pkg/toolsets/core/resources.go +++ b/pkg/toolsets/core/resources.go @@ -101,6 +101,10 @@ func initResources(o api.Openshift) []api.ServerTool { Type: "string", Description: "A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec", }, + "namespace": { + Type: "string", + Description: "Optional namespace to apply the resource(s) to. Overrides the namespace defined in the resource metadata. Useful when resources are rendered without a hardcoded namespace (similar to kubectl apply -n )", + }, }, Required: []string{"resource"}, }, @@ -270,7 +274,15 @@ func resourcesCreateOrUpdate(params api.ToolHandlerParams) (*api.ToolCallResult, return api.NewToolCallResult("", fmt.Errorf("resource is not a string")), nil } - resources, err := kubernetes.NewCore(params).ResourcesCreateOrUpdate(params, r) + ns := "" + if namespace := params.GetArguments()["namespace"]; namespace != nil { + ns, ok = namespace.(string) + if !ok { + return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil + } + } + + resources, err := kubernetes.NewCore(params).ResourcesCreateOrUpdate(params, r, ns) if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to create or update resources: %w", err)), nil } diff --git a/pkg/toolsets/kubevirt/vm/create/tool.go b/pkg/toolsets/kubevirt/vm/create/tool.go index 608b76966..9ac351cbe 100644 --- a/pkg/toolsets/kubevirt/vm/create/tool.go +++ b/pkg/toolsets/kubevirt/vm/create/tool.go @@ -171,7 +171,7 @@ func create(params api.ToolHandlerParams) (*api.ToolCallResult, error) { } // Create the VM in the cluster - resources, err := kubernetes.NewCore(params).ResourcesCreateOrUpdate(params, vmYaml) + resources, err := kubernetes.NewCore(params).ResourcesCreateOrUpdate(params, vmYaml, "") if err != nil { return api.NewToolCallResult("", fmt.Errorf("failed to create VirtualMachine: %w", err)), nil }