diff --git a/README.md b/README.md index 7cc1c6d6..1b1bdf10 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,13 @@ In case multi-cluster support is enabled (default) and you have access to multip - `name` (`string`) **(required)** - Name of the resource - `namespace` (`string`) - Optional Namespace to delete the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will delete resource from configured namespace +- **resources_scale** - Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource + - `apiVersion` (`string`) **(required)** - apiVersion of the resource (examples of valid apiVersion are apps/v1) + - `kind` (`string`) **(required)** - kind of the resource (examples of valid kind are: StatefulSet, Deployment) + - `name` (`string`) **(required)** - Name of the resource + - `namespace` (`string`) - Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace + - `scale` (`integer`) - Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it +
@@ -397,7 +404,7 @@ In case multi-cluster support is enabled (default) and you have access to multip - `resource_name` (`string`) - Name of the resource to get traces for. Required if traceId is not provided. - `resource_type` (`string`) - Type of resource to get traces for (app, service, workload). Required if traceId is not provided. - `startMicros` (`string`) - Start time for traces in microseconds since epoch (optional, defaults to 10 minutes before current time if not provided, only used when traceId is not provided) - - `tags` (`string`) - JSON string of tags to filter traces (optional, only used when traceId is not provided) + - `tags` (`string`) - JSON string of tags to filter traces (optional, only used when traceId is not provided) - `traceId` (`string`) - Unique identifier of the trace to retrieve detailed information for. If provided, this will return detailed trace information and other parameters (resource_type, namespace, resource_name) are not required.
diff --git a/pkg/kubernetes/resources.go b/pkg/kubernetes/resources.go index 4f25052e..eda7da6f 100644 --- a/pkg/kubernetes/resources.go +++ b/pkg/kubernetes/resources.go @@ -36,7 +36,7 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion } // Check if operation is allowed for all namespaces (applicable for namespaced resources) - isNamespaced, _ := k.isNamespaced(gvk) + isNamespaced, _ := k.IsNamespaced(gvk) if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" { namespace = k.configuredNamespace() } @@ -53,7 +53,7 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK } // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one - if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { + if namespaced, nsErr := k.IsNamespaced(gvk); nsErr == nil && namespaced { namespace = k.NamespaceOrDefault(namespace) } return k.AccessControlClientset().DynamicClient().Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) @@ -80,7 +80,7 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi } // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one - if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { + if namespaced, nsErr := k.IsNamespaced(gvk); nsErr == nil && namespaced { namespace = k.NamespaceOrDefault(namespace) } return k.AccessControlClientset().DynamicClient().Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) @@ -143,7 +143,7 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u namespace := obj.GetNamespace() // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one - if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced { + if namespaced, nsErr := k.IsNamespaced(&gvk); nsErr == nil && namespaced { namespace = k.NamespaceOrDefault(namespace) } resources[i], rErr = k.AccessControlClientset().DynamicClient().Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{ @@ -168,7 +168,7 @@ func (k *Kubernetes) resourceFor(gvk *schema.GroupVersionKind) (*schema.GroupVer return &m.Resource, nil } -func (k *Kubernetes) isNamespaced(gvk *schema.GroupVersionKind) (bool, error) { +func (k *Kubernetes) IsNamespaced(gvk *schema.GroupVersionKind) (bool, error) { apiResourceList, err := k.AccessControlClientset().DiscoveryClient().ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { return false, err diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index a1f176ff..93b761e2 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -8,6 +8,7 @@ import ( "github.com/BurntSushi/toml" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/suite" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" @@ -16,6 +17,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/utils/ptr" "sigs.k8s.io/yaml" ) @@ -605,6 +607,122 @@ func (s *ResourcesSuite) TestResourcesDeleteDenied() { }) } +func (s *ResourcesSuite) TestResourcesScale() { + s.InitMcpClient() + kc := kubernetes.NewForConfigOrDie(envTestRestConfig) + deploymentName := "deployment-to-scale" + _, _ = kc.AppsV1().Deployments("default").Create(s.T().Context(), &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: deploymentName}, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(2)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": deploymentName}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": deploymentName}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}, + }, + }, + }, + }, metav1.CreateOptions{}) + + s.Run("resources_scale with missing apiVersion returns error", func() { + toolResult, _ := s.CallTool("resources_scale", map[string]interface{}{}) + s.Truef(toolResult.IsError, "call tool should fail") + s.Equalf("failed to get/update resource scale, missing argument apiVersion", toolResult.Content[0].(mcp.TextContent).Text, + "invalid error message, got %v", toolResult.Content[0].(mcp.TextContent).Text) + }) + s.Run("resources_scale with missing kind returns error", func() { + toolResult, _ := s.CallTool("resources_scale", map[string]interface{}{"apiVersion": "apps/v1"}) + s.Truef(toolResult.IsError, "call tool should fail") + s.Equalf("failed to get/update resource scale, missing argument kind", toolResult.Content[0].(mcp.TextContent).Text, + "invalid error message, got %v", toolResult.Content[0].(mcp.TextContent).Text) + }) + s.Run("resources_scale with missing name returns error", func() { + toolResult, _ := s.CallTool("resources_scale", map[string]interface{}{"apiVersion": "apps/v1", "kind": "Deployment"}) + s.Truef(toolResult.IsError, "call tool should fail") + s.Equalf("failed to get/update resource scale, missing argument name", toolResult.Content[0].(mcp.TextContent).Text, + "invalid error message, got %v", toolResult.Content[0].(mcp.TextContent).Text) + }) + s.Run("resources_scale get returns current scale", func() { + result, err := s.CallTool("resources_scale", map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "namespace": "default", + "name": deploymentName, + }) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + }) + s.Run("returns scale yaml", func() { + content := result.Content[0].(mcp.TextContent).Text + s.Truef(strings.HasPrefix(content, "# Current resource scale (YAML) is below"), + "Expected success message, got %v", content) + var decodedScale unstructured.Unstructured + err = yaml.Unmarshal([]byte(strings.TrimPrefix(content, "# Current resource scale (YAML) is below\n")), &decodedScale) + s.Nilf(err, "invalid tool result content %v", err) + replicas, found, _ := unstructured.NestedInt64(decodedScale.Object, "spec", "replicas") + s.Truef(found, "replicas not found in scale object") + s.Equalf(int64(2), replicas, "expected 2 replicas, got %d", replicas) + }) + }) + s.Run("resources_scale update changes the scale", func() { + result, err := s.CallTool("resources_scale", map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "namespace": "default", + "name": deploymentName, + "scale": 5, + }) + s.Run("no error", func() { + s.Nilf(err, "call tool failed %v", err) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + }) + s.Run("returns updated scale yaml", func() { + content := result.Content[0].(mcp.TextContent).Text + var decodedScale unstructured.Unstructured + err = yaml.Unmarshal([]byte(strings.TrimPrefix(content, "# Current resource scale (YAML) is below\n")), &decodedScale) + s.Nilf(err, "invalid tool result content %v", err) + replicas, found, _ := unstructured.NestedInt64(decodedScale.Object, "spec", "replicas") + s.Truef(found, "replicas not found in scale object") + s.Equalf(int64(5), replicas, "expected 5 replicas after update, got %d", replicas) + }) + s.Run("deployment was actually scaled", func() { + deployment, _ := kc.AppsV1().Deployments("default").Get(s.T().Context(), deploymentName, metav1.GetOptions{}) + s.Equalf(int32(5), *deployment.Spec.Replicas, "expected 5 replicas in deployment, got %d", *deployment.Spec.Replicas) + }) + }) + s.Run("resources_scale with nonexistent resource returns error", func() { + toolResult, _ := s.CallTool("resources_scale", map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "namespace": "default", + "name": "nonexistent-deployment", + }) + s.Truef(toolResult.IsError, "call tool should fail") + s.Containsf(toolResult.Content[0].(mcp.TextContent).Text, "not found", + "expected not found error, got %v", toolResult.Content[0].(mcp.TextContent).Text) + }) + s.Run("resources_scale with resource that does not support scale subresource returns error", func() { + configMapName := "configmap-without-scale" + _, _ = kc.CoreV1().ConfigMaps("default").Create(s.T().Context(), &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: configMapName}, + Data: map[string]string{"key": "value"}, + }, metav1.CreateOptions{}) + toolResult, _ := s.CallTool("resources_scale", map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "namespace": "default", + "name": configMapName, + }) + s.Truef(toolResult.IsError, "call tool should fail") + s.Containsf(toolResult.Content[0].(mcp.TextContent).Text, "the server could not find the requested resource", + "expected scale subresource not found error, got %v", toolResult.Content[0].(mcp.TextContent).Text) + }) +} + func TestResources(t *testing.T) { suite.Run(t, new(ResourcesSuite)) } diff --git a/pkg/mcp/testdata/toolsets-core-tools.json b/pkg/mcp/testdata/toolsets-core-tools.json index b4c5667f..c351a239 100644 --- a/pkg/mcp/testdata/toolsets-core-tools.json +++ b/pkg/mcp/testdata/toolsets-core-tools.json @@ -483,5 +483,45 @@ ] }, "name": "resources_list" + }, + { + "annotations": { + "title": "Resources: Scale", + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + }, + "description": "Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource", + "inputSchema": { + "type": "object", + "properties": { + "apiVersion": { + "description": "apiVersion of the resource (examples of valid apiVersion are apps/v1)", + "type": "string" + }, + "kind": { + "description": "kind of the resource (examples of valid kind are: StatefulSet, Deployment)", + "type": "string" + }, + "name": { + "description": "Name of the resource", + "type": "string" + }, + "namespace": { + "description": "Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace", + "type": "string" + }, + "scale": { + "description": "Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it", + "type": "integer" + } + }, + "required": [ + "apiVersion", + "kind", + "name" + ] + }, + "name": "resources_scale" } ] diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json index 7831c054..6b394f01 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json +++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster-enum.json @@ -760,5 +760,53 @@ ] }, "name": "resources_list" + }, + { + "annotations": { + "title": "Resources: Scale", + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + }, + "description": "Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource", + "inputSchema": { + "type": "object", + "properties": { + "apiVersion": { + "description": "apiVersion of the resource (examples of valid apiVersion are apps/v1)", + "type": "string" + }, + "context": { + "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set", + "enum": [ + "extra-cluster", + "fake-context" + ], + "type": "string" + }, + "kind": { + "description": "kind of the resource (examples of valid kind are: StatefulSet, Deployment)", + "type": "string" + }, + "name": { + "description": "Name of the resource", + "type": "string" + }, + "namespace": { + "description": "Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace", + "type": "string" + }, + "scale": { + "description": "Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it", + "type": "integer" + } + }, + "required": [ + "apiVersion", + "kind", + "name" + ] + }, + "name": "resources_scale" } ] diff --git a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json index b95f179c..289c6f3d 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-multicluster.json +++ b/pkg/mcp/testdata/toolsets-full-tools-multicluster.json @@ -680,5 +680,49 @@ ] }, "name": "resources_list" + }, + { + "annotations": { + "title": "Resources: Scale", + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + }, + "description": "Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource", + "inputSchema": { + "type": "object", + "properties": { + "apiVersion": { + "description": "apiVersion of the resource (examples of valid apiVersion are apps/v1)", + "type": "string" + }, + "context": { + "description": "Optional parameter selecting which context to run the tool in. Defaults to fake-context if not set", + "type": "string" + }, + "kind": { + "description": "kind of the resource (examples of valid kind are: StatefulSet, Deployment)", + "type": "string" + }, + "name": { + "description": "Name of the resource", + "type": "string" + }, + "namespace": { + "description": "Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace", + "type": "string" + }, + "scale": { + "description": "Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it", + "type": "integer" + } + }, + "required": [ + "apiVersion", + "kind", + "name" + ] + }, + "name": "resources_scale" } ] diff --git a/pkg/mcp/testdata/toolsets-full-tools-openshift.json b/pkg/mcp/testdata/toolsets-full-tools-openshift.json index e4488b0a..58661521 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-openshift.json +++ b/pkg/mcp/testdata/toolsets-full-tools-openshift.json @@ -597,5 +597,45 @@ ] }, "name": "resources_list" + }, + { + "annotations": { + "title": "Resources: Scale", + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + }, + "description": "Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource", + "inputSchema": { + "type": "object", + "properties": { + "apiVersion": { + "description": "apiVersion of the resource (examples of valid apiVersion are apps/v1)", + "type": "string" + }, + "kind": { + "description": "kind of the resource (examples of valid kind are: StatefulSet, Deployment)", + "type": "string" + }, + "name": { + "description": "Name of the resource", + "type": "string" + }, + "namespace": { + "description": "Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace", + "type": "string" + }, + "scale": { + "description": "Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it", + "type": "integer" + } + }, + "required": [ + "apiVersion", + "kind", + "name" + ] + }, + "name": "resources_scale" } ] diff --git a/pkg/mcp/testdata/toolsets-full-tools.json b/pkg/mcp/testdata/toolsets-full-tools.json index ca270027..9384218b 100644 --- a/pkg/mcp/testdata/toolsets-full-tools.json +++ b/pkg/mcp/testdata/toolsets-full-tools.json @@ -584,5 +584,45 @@ ] }, "name": "resources_list" + }, + { + "annotations": { + "title": "Resources: Scale", + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + }, + "description": "Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource", + "inputSchema": { + "type": "object", + "properties": { + "apiVersion": { + "description": "apiVersion of the resource (examples of valid apiVersion are apps/v1)", + "type": "string" + }, + "kind": { + "description": "kind of the resource (examples of valid kind are: StatefulSet, Deployment)", + "type": "string" + }, + "name": { + "description": "Name of the resource", + "type": "string" + }, + "namespace": { + "description": "Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace", + "type": "string" + }, + "scale": { + "description": "Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it", + "type": "integer" + } + }, + "required": [ + "apiVersion", + "kind", + "name" + ] + }, + "name": "resources_scale" } ] diff --git a/pkg/toolsets/core/resources.go b/pkg/toolsets/core/resources.go index 52a613b3..24a01337 100644 --- a/pkg/toolsets/core/resources.go +++ b/pkg/toolsets/core/resources.go @@ -6,7 +6,10 @@ import ( "fmt" "github.com/google/jsonschema-go/jsonschema" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" "k8s.io/utils/ptr" "github.com/containers/kubernetes-mcp-server/pkg/api" @@ -138,6 +141,42 @@ func initResources(o internalk8s.Openshift) []api.ServerTool { OpenWorldHint: ptr.To(true), }, }, Handler: resourcesDelete}, + {Tool: api.Tool{ + Name: "resources_scale", + Description: "Get or update the scale of a Kubernetes resource in the current cluster by providing its apiVersion, kind, name, and optionally the namespace. If the scale is set in the tool call, the scale will be updated to that value. Always returns the current scale of the resource", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "apiVersion": { + Type: "string", + Description: "apiVersion of the resource (examples of valid apiVersion are apps/v1)", + }, + "kind": { + Type: "string", + Description: "kind of the resource (examples of valid kind are: StatefulSet, Deployment)", + }, + "namespace": { + Type: "string", + Description: "Optional Namespace to get/update the namespaced resource scale from (ignored in case of cluster scoped resources). If not provided, will get/update resource scale from configured namespace", + }, + "name": { + Type: "string", + Description: "Name of the resource", + }, + "scale": { + Type: "integer", + Description: "Optional scale to update the resources scale to. If not provided, will return the current scale of the resource, and not update it", + }, + }, + Required: []string{"apiVersion", "kind", "name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "Resources: Scale", + DestructiveHint: ptr.To(true), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: resourcesScale}, } } @@ -259,6 +298,103 @@ func resourcesDelete(params api.ToolHandlerParams) (*api.ToolCallResult, error) return api.NewToolCallResult("Resource deleted successfully", err), nil } +func resourcesScale(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + namespace := params.GetArguments()["namespace"] + if namespace == nil { + namespace = "" + } + + gvk, err := parseGroupVersionKind(params.GetArguments()) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get/update resource scale, %w", err)), nil + } + + name := params.GetArguments()["name"] + if name == nil { + return api.NewToolCallResult("", errors.New("failed to get/update resource scale, missing argument name")), nil + } + + ns, ok := namespace.(string) + if !ok { + return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil + } + + ns = params.NamespaceOrDefault(ns) + + n, ok := name.(string) + if !ok { + return api.NewToolCallResult("", fmt.Errorf("name is not a string")), nil + } + + resourceClient, err := getResourceClient(params, gvk, ns) + if err != nil { + return api.NewToolCallResult("", err), nil + } + + scale, err := resourceClient.Get(params.Context, n, metav1.GetOptions{}, "scale") + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to get scale subresource: %w", err)), nil + } + + if desiredScale, shouldScale := params.GetArguments()["scale"]; shouldScale { + newScale, err := parseScaleValue(desiredScale) + if err != nil { + return api.NewToolCallResult("", err), nil + } + + if err := unstructured.SetNestedField(scale.Object, newScale, "spec", "replicas"); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to set replicas in new unstructured object: %w", err)), nil + } + + scale, err = resourceClient.Update(params.Context, scale, metav1.UpdateOptions{}, "scale") + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to update scale of resource: %w", err)), nil + } + } + + marshalled, err := output.MarshalYaml(scale) + if err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to marshall scale to yaml format: %v", scale)), nil + } + + return api.NewToolCallResult("# Current resource scale (YAML) is below\n"+marshalled, err), nil +} + +func getResourceClient(params api.ToolHandlerParams, gvk *schema.GroupVersionKind, ns string) (dynamic.ResourceInterface, error) { + restMapper, err := params.ToRESTMapper() + if err != nil { + return nil, fmt.Errorf("encountered internal error while trying to get resource client") + } + + mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, fmt.Errorf("failed to get gvr for resource, %w", err) + } + + isNamespaced, err := params.IsNamespaced(gvk) + if err != nil { + return nil, fmt.Errorf("failed to determine if resource is namespaced") + } + + if isNamespaced { + return params.AccessControlClientset().DynamicClient().Resource(mapping.Resource).Namespace(ns), nil + } + return params.AccessControlClientset().DynamicClient().Resource(mapping.Resource), nil +} + +func parseScaleValue(desiredScale interface{}) (int64, error) { + switch s := desiredScale.(type) { + case float64: + return int64(s), nil + case int: + return int64(s), nil + case int64: + return s, nil + default: + return 0, fmt.Errorf("failed to parse scale parameter: expected integer, got %T", desiredScale) + } +} + func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) { apiVersion := arguments["apiVersion"] if apiVersion == nil {