From a41beae9a50a2f1bf59281541e21a6c2c12da5f4 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Tue, 25 Feb 2025 18:20:44 +0000 Subject: [PATCH 1/2] added helm tools, and some minor tool cleanup --- python/src/kagent/cli.py | 21 +- python/src/kagent/tools/helm/__init__.py | 3 + python/src/kagent/tools/helm/_helm.py | 219 +++++++++++++++++++++ python/src/kagent/tools/istio/_istioctl.py | 2 +- python/src/kagent/tools/k8s/_kubectl.py | 6 +- 5 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 python/src/kagent/tools/helm/__init__.py create mode 100644 python/src/kagent/tools/helm/_helm.py diff --git a/python/src/kagent/cli.py b/python/src/kagent/cli.py index 128526cfa2..4407b2bfda 100644 --- a/python/src/kagent/cli.py +++ b/python/src/kagent/cli.py @@ -3,6 +3,7 @@ from mcp.server.fastmcp import FastMCP from pydantic import BaseModel +from kagent.tools.helm._helm import helm_list, helm_uninstall, helm_upgrade from kagent.tools.istio._istio_crds import ( IstioCRDTool, IstioCRDToolConfig, @@ -28,6 +29,7 @@ create_resource, delete_resource, describe_resource, + get_available_api_resources, get_cluster_configuration, get_events, get_pod_logs, @@ -39,8 +41,6 @@ remove_label, rollout, scale, - get_available_api_resources, - ) from kagent.tools.prometheus._prometheus import ( AlertmanagersInput, @@ -205,6 +205,7 @@ def istio(): mcp.add_tool(remote_clusters._func, remote_clusters.name, remote_clusters.description) cfg = IstioCRDToolConfig(model="gpt-4o-mini", openai_api_key=None) + def istio_crd_tool(input: IstioCRDToolInput): return IstioCRDTool(cfg).run_json(input.model_dump(), CancellationToken()) @@ -229,11 +230,23 @@ def k8s(): mcp.add_tool(rollout._func, rollout.name, rollout.description) mcp.add_tool(scale._func, scale.name, scale.description) mcp.add_tool(patch_resource._func, patch_resource.name, patch_resource.description) - mcp.add_tool(check_service_connectivity._func, check_service_connectivity.name, check_service_connectivity.description) + mcp.add_tool( + check_service_connectivity._func, check_service_connectivity.name, check_service_connectivity.description + ) mcp.add_tool(create_resource._func, create_resource.name, create_resource.description) mcp.add_tool(get_events._func, get_events.name, get_events.description) - mcp.add_tool(get_available_api_resources._func, get_available_api_resources.name, get_available_api_resources.description) + mcp.add_tool( + get_available_api_resources._func, get_available_api_resources.name, get_available_api_resources.description + ) + + mcp.run() + +@app.command() +def helm(): + mcp.add_tool(helm_list._func, helm_list.name, helm_list.description) + mcp.add_tool(helm_upgrade._func, helm_upgrade.name, helm_upgrade.description) + mcp.add_tool(helm_uninstall._func, helm_uninstall.name, helm_uninstall.description) mcp.run() diff --git a/python/src/kagent/tools/helm/__init__.py b/python/src/kagent/tools/helm/__init__.py new file mode 100644 index 0000000000..b88b32bd84 --- /dev/null +++ b/python/src/kagent/tools/helm/__init__.py @@ -0,0 +1,3 @@ +from ._helm import HelmList, HelmListConfig, HelmUninstall, HelmUninstallConfig, HelmUpgrade, HelmUpgradeConfig + +__all__ = ["HelmUpgrade", "HelmUpgradeConfig", "HelmList", "HelmListConfig", "HelmUninstall", "HelmUninstallConfig"] diff --git a/python/src/kagent/tools/helm/_helm.py b/python/src/kagent/tools/helm/_helm.py new file mode 100644 index 0000000000..98056971b1 --- /dev/null +++ b/python/src/kagent/tools/helm/_helm.py @@ -0,0 +1,219 @@ +from typing import Annotated, Optional + +from autogen_core.tools import FunctionTool + +from .._utils import create_typed_fn_tool +from ..common.shell import run_command + + +def _helm_list( + namespace: Annotated[Optional[str], "The namespace to list the helm charts from"] = None, + all_namespaces: Annotated[Optional[bool], "If true, list releases from all namespaces"] = False, + all: Annotated[Optional[bool], "If true, show all releases without any filter applied"] = False, + uninstalled: Annotated[Optional[bool], "If true, list uninstalled releases"] = False, + uninstalling: Annotated[Optional[bool], "If true, list uninstalling releases"] = False, + failed: Annotated[Optional[bool], "If true, list failed releases"] = False, + deployed: Annotated[Optional[bool], "If true, list deployed releases"] = False, + filter: Annotated[ + Optional[str], + "a regular expression (Perl compatible). Any releases that match the expression will be included in the results", + ] = None, + pending: Annotated[Optional[bool], "If true, list pending releases"] = False, + output: Annotated[ + Optional[str], + "The output format of the helm list command, one of: table, json, yaml. Prefer table for human readability.", + ] = None, +) -> str: + args: list[str] = [] + if namespace: + args.append(f"-n {namespace}") + + if all_namespaces: + args.append("-A") + + if all: + args.append("-a") + + if uninstalled: + args.append("--uninstalled") + + if uninstalling: + args.append("--uninstalling") + + if failed: + args.append("--failed") + + if deployed: + args.append("--deployed") + + if pending: + args.append("--pending") + + if output: + args.append(f"-o {output}") + + if filter: + args.append(f"-f {filter}") + + return _run_helm_command(f"list {' '.join(args)}") + + +helm_list = FunctionTool( + _helm_list, + description=""" +This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). + +By default, it lists only releases that are deployed or failed. Flags like +'--uninstalled' and '--all' will alter this behavior. Such flags can be combined: +'--uninstalled --failed'. + +By default, items are sorted alphabetically. Use the '-d' flag to sort by +release date. + +If the --filter flag is provided, it will be treated as a filter. Filters are +regular expressions (Perl compatible) that are applied to the list of releases. +Only items that match the filter will be returned. + + $ helm list --filter 'ara[a-z]+' + NAME UPDATED CHART + maudlin-arachnid 2020-06-18 14:17:46.125134977 +0000 UTC alpine-0.1.0 + """, + name="helm_list", +) + +HelmList, HelmListConfig = create_typed_fn_tool( + helm_list, "kagent.tools.helm.List", "HelmList" +) + +def _helm_upgrade( + name: Annotated[str, "The name of the release"], + chart: Annotated[str, "The chart to install"], + namespace: Annotated[str, "The namespace to install the release in"], + create_namespace: Annotated[Optional[bool], "If true, create the namespace if it does not exist"] = False, + set: Annotated[Optional[list[str]], "A list of key-value pairs to set on the release. (can specify multiple or separate values with commas: key1=val1,key2=val2)"] = None, + values: Annotated[Optional[list[str]], "A list of files to use as the value source. (can specify multiple or separate values with commas: myvalues.yaml,override.yaml)"] = None, + version: Annotated[Optional[str], "The version of the chart to install"] = None, + install: Annotated[Optional[bool], "If true, install the release if it does not exist"] = False, + dry_run: Annotated[Optional[bool], "If true, show which releases will be uninstalled without actually uninstalling them"] = False, + wait: Annotated[Optional[bool], "If true, wait for the release to be deployed"] = False, +) -> str: + args: list[str] = [name, chart, "-n", namespace] + if create_namespace: + args.append("--create-namespace") + if set: + args.append(f"--set {','.join(set)}") + if values: + args.append(f"--values {','.join(values)}") + if version: + args.append(f"--version {version}") + if install: + args.append("--install") + if dry_run: + args.append("--dry-run") + if wait: + args.append("--wait") + + return _run_helm_command(f"upgrade {' '.join(args)}") + + +helm_upgrade = FunctionTool( + _helm_upgrade, + description=""" +This command upgrades a release to a new version of a chart. + +The upgrade arguments must be a release and chart. The chart +argument can be either: a chart reference('example/mariadb'), a path to a chart directory, +a packaged chart, or a fully qualified URL. For chart references, the latest +version will be specified unless the '--version' flag is set. + +To override values in a chart, use either the '--values' flag and pass in a file +or use the '--set' flag and pass configuration from the command line, to force string +values, use '--set-string'. You can use '--set-file' to set individual +values from a file when the value itself is too long for the command line +or is dynamically generated. You can also use '--set-json' to set json values +(scalars/objects/arrays) from the command line. + +You can specify the '--values'/'-f' flag multiple times. The priority will be given to the +last (right-most) file specified. For example, if both myvalues.yaml and override.yaml +contained a key called 'Test', the value set in override.yaml would take precedence: + + $ helm upgrade -f myvalues.yaml -f override.yaml redis ./redis + +You can specify the '--set' flag multiple times. The priority will be given to the +last (right-most) set specified. For example, if both 'bar' and 'newbar' values are +set for a key called 'foo', the 'newbar' value would take precedence: + + $ helm upgrade --set foo=bar --set foo=newbar redis ./redis + +There are six different ways you can express the chart you want to install: + +1. By chart reference: helm install mymaria example/mariadb +2. By path to a packaged chart: helm install mynginx ./nginx-1.2.3.tgz +3. By path to an unpacked chart directory: helm install mynginx ./nginx +4. By absolute URL: helm install mynginx https://example.com/charts/nginx-1.2.3.tgz +5. By chart reference and repo url: helm install --repo https://example.com/charts/ mynginx nginx +6. By OCI registries: helm install mynginx --version 1.2.3 oci://example.com/charts/nginx + +CHART REFERENCES + +A chart reference is a convenient way of referencing a chart in a chart repository. + +When you use a chart reference with a repo prefix ('example/mariadb'), Helm will look in the local +configuration for a chart repository named 'example', and will then look for a +chart in that repository whose name is 'mariadb'. It will install the latest stable version of that chart +until you specify '--devel' flag to also include development version (alpha, beta, and release candidate releases), or +supply a version number with the '--version' flag. + +To see the list of chart repositories, use 'helm repo list'. To search for +charts in a repository, use 'helm search'. +""", + name="helm_upgrade", +) + +HelmUpgrade, HelmUpgradeConfig = create_typed_fn_tool( + helm_upgrade, "kagent.tools.helm.Upgrade", "HelmUpgrade" +) + +def _helm_uninstall( + name: Annotated[str, "The name of the release"], + namespace: Annotated[str, "The namespace to uninstall the release from"], + dry_run: Annotated[Optional[bool], "If true, show which releases will be uninstalled without actually uninstalling them"] = False, + wait: Annotated[Optional[bool], "If true, wait for the release to be uninstalled"] = False, +) -> str: + args: list[str] = [name, "-n", namespace] + if dry_run: + args.append("--dry-run") + if wait: + args.append("--wait") + return _run_helm_command(f"uninstall {' '.join(args)}") + + +helm_uninstall = FunctionTool( + _helm_uninstall, + description=""" +This command takes a release name and uninstalls the release. + +It removes all of the resources associated with the last release of the chart +as well as the release history, freeing it up for future use. + +Use the '--dry-run' flag to see which releases will be uninstalled without actually +uninstalling them. + +Usage: + helm uninstall RELEASE_NAME [...] [flags] + +""", + name="helm_uninstall", +) + + +HelmUninstall, HelmUninstallConfig = create_typed_fn_tool( + helm_uninstall, "kagent.tools.helm.Uninstall", "HelmUninstall" +) + + +def _run_helm_command(command: str) -> str: + # Split the command and remove empty strings + cmd_parts = command.split(" ") + cmd_parts = [part for part in cmd_parts if part] # Remove empty strings from the list + return run_command("helm", cmd_parts) diff --git a/python/src/kagent/tools/istio/_istioctl.py b/python/src/kagent/tools/istio/_istioctl.py index 625511bb96..ecd6536055 100644 --- a/python/src/kagent/tools/istio/_istioctl.py +++ b/python/src/kagent/tools/istio/_istioctl.py @@ -68,7 +68,7 @@ async def _proxy_config( ns: Annotated[Optional[str], "The namespace of the pod to get proxy configuration for"], config_type: Annotated[Optional[str], "The type of configuration to get, the allowed values are: all, bootstrap, cluster, ecds, listener, log, route, secret"] = "all", ) -> str: - return _run_istioctl_command(f"proxy-config {config_type} {'-n ' + ns if ns else ''} {pod_name}") + return _run_istioctl_command(f"proxy-config {config_type} {pod_name}{'.' + ns if ns else ''}") async def _generate_manifest( profile: Annotated[str, "Istio configuration profile to generate manifest for, the allowed values are: ambient, default, demo, minimal, empty"], diff --git a/python/src/kagent/tools/k8s/_kubectl.py b/python/src/kagent/tools/k8s/_kubectl.py index 15b47192f1..349bb7e84a 100644 --- a/python/src/kagent/tools/k8s/_kubectl.py +++ b/python/src/kagent/tools/k8s/_kubectl.py @@ -142,10 +142,10 @@ def _get_resource_yaml( resource_type: Annotated[ str, "The type of resource to get the YAML definition for (deployment, service, pod, node, ...)" ], - name: Annotated[str, "The name of the resource to get the YAML definition for"], + name: Annotated[Optional[str], "The name of the resource to get the YAML definition for. If not provided, all resources of the given type will be returned."], ns: Annotated[Optional[str], "The namespace of the resource to get the definition for"], ) -> str: - return _run_kubectl_command(f"get {resource_type} {name} {f'-n {ns} ' if ns else ''} -o yaml") + return _run_kubectl_command(f"get {resource_type} {name if name else ''} {f'-n {ns} ' if ns else ''} -o yaml") def _execute_command( @@ -157,7 +157,7 @@ def _execute_command( def _get_resources( - name: Annotated[str, "The name of the resource to get information about"], + name: Annotated[Optional[str], "The name of the resource to get information about. If not provided, all resources of the given type will be returned."], resource_type: Annotated[ str, "The type of resource to get information about (deployment, service, pod, node, ...)" ], From d0b3f770ce6b4ab18dfbca2238549ea78d499a6a Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Tue, 25 Feb 2025 18:31:16 +0000 Subject: [PATCH 2/2] formatting --- python/src/kagent/tools/helm/_helm.py | 28 ++++++++++++++++--------- python/src/kagent/tools/k8s/_kubectl.py | 10 +++++++-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/python/src/kagent/tools/helm/_helm.py b/python/src/kagent/tools/helm/_helm.py index 98056971b1..1689478ab3 100644 --- a/python/src/kagent/tools/helm/_helm.py +++ b/python/src/kagent/tools/helm/_helm.py @@ -81,20 +81,27 @@ def _helm_list( name="helm_list", ) -HelmList, HelmListConfig = create_typed_fn_tool( - helm_list, "kagent.tools.helm.List", "HelmList" -) +HelmList, HelmListConfig = create_typed_fn_tool(helm_list, "kagent.tools.helm.List", "HelmList") + def _helm_upgrade( name: Annotated[str, "The name of the release"], chart: Annotated[str, "The chart to install"], namespace: Annotated[str, "The namespace to install the release in"], create_namespace: Annotated[Optional[bool], "If true, create the namespace if it does not exist"] = False, - set: Annotated[Optional[list[str]], "A list of key-value pairs to set on the release. (can specify multiple or separate values with commas: key1=val1,key2=val2)"] = None, - values: Annotated[Optional[list[str]], "A list of files to use as the value source. (can specify multiple or separate values with commas: myvalues.yaml,override.yaml)"] = None, + set: Annotated[ + Optional[list[str]], + "A list of key-value pairs to set on the release. (can specify multiple or separate values with commas: key1=val1,key2=val2)", + ] = None, + values: Annotated[ + Optional[list[str]], + "A list of files to use as the value source. (can specify multiple or separate values with commas: myvalues.yaml,override.yaml)", + ] = None, version: Annotated[Optional[str], "The version of the chart to install"] = None, install: Annotated[Optional[bool], "If true, install the release if it does not exist"] = False, - dry_run: Annotated[Optional[bool], "If true, show which releases will be uninstalled without actually uninstalling them"] = False, + dry_run: Annotated[ + Optional[bool], "If true, show which releases will be uninstalled without actually uninstalling them" + ] = False, wait: Annotated[Optional[bool], "If true, wait for the release to be deployed"] = False, ) -> str: args: list[str] = [name, chart, "-n", namespace] @@ -170,14 +177,15 @@ def _helm_upgrade( name="helm_upgrade", ) -HelmUpgrade, HelmUpgradeConfig = create_typed_fn_tool( - helm_upgrade, "kagent.tools.helm.Upgrade", "HelmUpgrade" -) +HelmUpgrade, HelmUpgradeConfig = create_typed_fn_tool(helm_upgrade, "kagent.tools.helm.Upgrade", "HelmUpgrade") + def _helm_uninstall( name: Annotated[str, "The name of the release"], namespace: Annotated[str, "The namespace to uninstall the release from"], - dry_run: Annotated[Optional[bool], "If true, show which releases will be uninstalled without actually uninstalling them"] = False, + dry_run: Annotated[ + Optional[bool], "If true, show which releases will be uninstalled without actually uninstalling them" + ] = False, wait: Annotated[Optional[bool], "If true, wait for the release to be uninstalled"] = False, ) -> str: args: list[str] = [name, "-n", namespace] diff --git a/python/src/kagent/tools/k8s/_kubectl.py b/python/src/kagent/tools/k8s/_kubectl.py index 6bc3d8bc07..8f8a4e9dfe 100644 --- a/python/src/kagent/tools/k8s/_kubectl.py +++ b/python/src/kagent/tools/k8s/_kubectl.py @@ -140,7 +140,10 @@ def _get_resource_yaml( resource_type: Annotated[ str, "The type of resource to get the YAML definition for (deployment, service, pod, node, ...)" ], - name: Annotated[Optional[str], "The name of the resource to get the YAML definition for. If not provided, all resources of the given type will be returned."], + name: Annotated[ + Optional[str], + "The name of the resource to get the YAML definition for. If not provided, all resources of the given type will be returned.", + ], ns: Annotated[Optional[str], "The namespace of the resource to get the definition for"], ) -> str: return _run_kubectl_command(f"get {resource_type} {name if name else ''} {f'-n {ns} ' if ns else ''} -o yaml") @@ -155,7 +158,10 @@ def _execute_command( def _get_resources( - name: Annotated[Optional[str], "The name of the resource to get information about. If not provided, all resources of the given type will be returned."], + name: Annotated[ + Optional[str], + "The name of the resource to get information about. If not provided, all resources of the given type will be returned.", + ], resource_type: Annotated[ str, "The type of resource to get information about (deployment, service, pod, node, ...)" ],