diff --git a/docs/openshift/ovn-kubernetes.md b/docs/openshift/ovn-kubernetes.md new file mode 100644 index 000000000..9382260fd --- /dev/null +++ b/docs/openshift/ovn-kubernetes.md @@ -0,0 +1,425 @@ +# OVN-Kubernetes Toolset + +## Overview + +This toolset provides MCP tools for inspecting and troubleshooting the OVN (Open Virtual Network) layer of an OVN-Kubernetes cluster. The tools execute OVN CLI commands (`ovn-nbctl`, `ovn-sbctl`, `ovn-trace`) inside `ovnkube-node` pods via `pods/exec`. + +**OVN Layer Tools** — Query OVN Northbound/Southbound databases, list logical flows, and trace packets through the logical network: + +| Tool | Description | +|------|-------------| +| `ovn_show` | OVN configuration overview via `ovn-nbctl show` / `ovn-sbctl show` | +| `ovn_get` | Query records from OVN database tables via `ovn-nbctl list` / `ovn-sbctl list` | +| `ovn_lflow_list` | List logical flows from the Southbound database via `ovn-sbctl lflow-list` | +| `ovn_trace` | Trace a packet through the OVN logical network via `ovn-trace` | + +All OVN layer tools are read-only and do not modify OVN state. + +## Prerequisites + +### Cluster Requirements + +- **CNI**: OVN-Kubernetes installed and configured +- **Nodes**: At least one node running `ovnkube-node` pods with `nbdb`, `sbdb`, and `northd` containers + +### RBAC Requirements + +The OVN-Kubernetes toolset requires Kubernetes permissions to list ovnkube-node pods and execute commands inside them: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ovn-kubernetes-mcp-user +rules: + # Required to discover ovnkube-node pods + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] + # Required to execute OVN commands in ovnkube-node containers + - apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ovn-kubernetes-mcp-user-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ovn-kubernetes-mcp-user +subjects: + - kind: ServiceAccount + name: kubernetes-mcp-server + namespace: default +``` + +### Security Considerations + +**Important security notes:** + +1. **Pod exec access**: These tools use `pods/exec` on `ovnkube-node` pods, which run with elevated privileges +2. **Data sensitivity**: Output may contain network topology, IP addresses, MAC addresses, and traffic patterns +3. **Audit logging**: Tool invocations are recorded in Kubernetes audit logs + +**Recommended practices**: + +- Grant permissions only to trusted users +- Scope the ClusterRoleBinding to the `openshift-ovn-kubernetes` namespace where possible +- Monitor audit logs for unexpected tool usage + +## Configuration + +### Enabling the Toolset + +Add `ovn-kubernetes` to your toolsets configuration: + +```toml +# config.toml +toolsets = ["core", "ovn-kubernetes"] +``` + +The `core` toolset is required for discovering ovnkube-node pods via `pods_list`. + +## Tools Reference + +OVN layer tools (`ovn_show`, `ovn_get`, `ovn_lflow_list`, `ovn_trace`) execute against the `nbdb`, `sbdb`, or `northd` container of an `ovnkube-node` pod depending on the database or operation requested. + +**Default behavior notes:** + +- **`namespace`**: defaults to `openshift-ovn-kubernetes` +- **`name`**: required; the name of a specific `ovnkube-node` pod (discover with `pods_list`) +- **`head`**: omitted or `0` → first **100** lines when `tail` is not specified +- **`tail`**: omitted or `0` → not applied +- **`apply_tail_first`**: `false` by default; when both `head` and `tail` are set, `head` is applied first + +### `ovn_show` + +Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `namespace` | No | `openshift-ovn-kubernetes` | Kubernetes namespace of the ovnkube-node pod | +| `name` | Yes | — | Name of the ovnkube-node pod | +| `database` | Yes | — | One of: `nbdb`, `sbdb` | +| `head` | No | `100` | Return only the first N lines of output | +| `tail` | No | — (not applied) | Return only the last N lines of output | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | + +Database modes: + +- `nbdb` — runs `ovn-nbctl show`: logical switches, logical routers, their ports, and connections +- `sbdb` — runs `ovn-sbctl show`: chassis information, port bindings, and their relationships + +### `ovn_get` + +Query records from an OVN database table with flexible filtering. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `namespace` | No | `openshift-ovn-kubernetes` | Kubernetes namespace of the ovnkube-node pod | +| `name` | Yes | — | Name of the ovnkube-node pod | +| `database` | Yes | — | One of: `nbdb`, `sbdb` | +| `table` | Yes | — | Name of the OVN database table to query | +| `record` | No | — (list all) | Record identifier (UUID or name); if omitted, lists all records | +| `columns` | No | — (all columns) | Comma-separated list of columns to display (e.g., `name,_uuid,ports`) | +| `pattern` | No | — (empty) | Regex pattern to filter results (only applies when listing all records) | +| `head` | No | `100` | Return only the first N lines of output | +| `tail` | No | — (not applied) | Return only the last N lines of output | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | + +Common Northbound tables: `Logical_Switch`, `Logical_Router`, `Logical_Switch_Port`, `Logical_Router_Port`, `ACL`, `Address_Set`, `Port_Group`, `Load_Balancer`, `NAT` + +Common Southbound tables: `Chassis`, `Port_Binding`, `Datapath_Binding`, `Logical_Flow`, `MAC_Binding`, `Multicast_Group`, `SB_Global` + +### `ovn_lflow_list` + +List logical flows from the OVN Southbound database. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `namespace` | No | `openshift-ovn-kubernetes` | Kubernetes namespace of the ovnkube-node pod | +| `name` | Yes | — | Name of the ovnkube-node pod | +| `datapath` | No | — (all datapaths) | Datapath name or UUID to filter flows for a specific logical switch/router | +| `pattern` | No | — (empty) | Regex pattern to filter flows | +| `head` | No | `100` | Return only the first N lines of output | +| `tail` | No | — (not applied) | Return only the last N lines of output | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | + +### `ovn_trace` + +Trace a packet through the OVN logical network. + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `namespace` | No | `openshift-ovn-kubernetes` | Kubernetes namespace of the ovnkube-node pod | +| `name` | Yes | — | Name of the ovnkube-node pod | +| `datapath` | Yes | — | Name of the logical switch or router to start the trace | +| `microflow` | Yes | — | Microflow specification describing the packet | +| `mode` | No | `detailed` | Output verbosity: `detailed`, `summary`, or `minimal` | +| `pattern` | No | — (empty) | Regex pattern to filter trace output | +| `head` | No | `100` | Return only the first N lines of output | +| `tail` | No | — (not applied) | Return only the last N lines of output | +| `apply_tail_first` | No | `false` | When both `head` and `tail` are applied, apply `tail` before `head` | + +Microflow specification examples: + +- `inport=="pod1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5` +- `inport=="pod1" && eth.src==00:00:00:00:00:01 && icmp && ip4.src==10.244.0.5 && ip4.dst==8.8.8.8` + +## Usage Examples + +### OVN Configuration (`ovn_show`) + +**Show Northbound configuration on a specific ovnkube-node pod**: + +```json +{ + "name": "ovn_show", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "database": "nbdb" + } +} +``` + +**Show Southbound configuration (chassis and port bindings)**: + +```json +{ + "name": "ovn_show", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "database": "sbdb" + } +} +``` + +### Query OVN Database (`ovn_get`) + +**List all logical switches**: + +```json +{ + "name": "ovn_get", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "database": "nbdb", + "table": "Logical_Switch" + } +} +``` + +**Get a specific logical router with selected columns**: + +```json +{ + "name": "ovn_get", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "database": "nbdb", + "table": "Logical_Router", + "record": "ovn_cluster_router", + "columns": "name,ports,nat" + } +} +``` + +**List ACLs matching a pattern**: + +```json +{ + "name": "ovn_get", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "database": "nbdb", + "table": "ACL", + "pattern": "action=drop" + } +} +``` + +### Logical Flows (`ovn_lflow_list`) + +**List all logical flows**: + +```json +{ + "name": "ovn_lflow_list", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde" + } +} +``` + +**List logical flows for a specific datapath**: + +```json +{ + "name": "ovn_lflow_list", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "datapath": "node1" + } +} +``` + +**Filter logical flows by pattern**: + +```json +{ + "name": "ovn_lflow_list", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "pattern": "ls_in_acl" + } +} +``` + +### Packet Tracing (`ovn_trace`) + +**Trace a packet through the logical network**: + +```json +{ + "name": "ovn_trace", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "datapath": "node1", + "microflow": "inport==\"pod1\" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5" + } +} +``` + +**Trace with summary output mode**: + +```json +{ + "name": "ovn_trace", + "arguments": { + "namespace": "openshift-ovn-kubernetes", + "name": "ovnkube-node-abcde", + "datapath": "node1", + "microflow": "inport==\"pod1\" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5", + "mode": "summary" + } +} +``` + +## Common Diagnostic Workflows + +### Inspect Logical Network Topology + +**Scenario**: Understand the logical network layout for a cluster. + +``` +# 1. Find ovnkube-node pods +pods_list: labelSelector="app=ovnkube-node" + +# 2. Show Northbound logical topology (switches, routers, ports) +ovn_show: namespace="openshift-ovn-kubernetes", name="", database="nbdb" + +# 3. List all logical switches +ovn_get: namespace="openshift-ovn-kubernetes", name="", database="nbdb", table="Logical_Switch" + +# 4. List all logical routers +ovn_get: namespace="openshift-ovn-kubernetes", name="", database="nbdb", table="Logical_Router" +``` + +### Debug Pod Connectivity + +**Scenario**: A pod cannot reach another pod; trace the packet through OVN. + +``` +# 1. Find ovnkube-node pods +pods_list: labelSelector="app=ovnkube-node" + +# 2. Show Southbound chassis and port bindings to identify where pods are bound +ovn_show: namespace="openshift-ovn-kubernetes", name="", database="sbdb" + +# 3. Find the port binding for the source pod +ovn_get: namespace="openshift-ovn-kubernetes", name="", database="sbdb", + table="Port_Binding", pattern="" + +# 4. Trace the packet through the logical network +ovn_trace: namespace="openshift-ovn-kubernetes", name="", + datapath="", + microflow="inport==\"\" && eth.src== && ip4.src= && ip4.dst=" + +# 5. Inspect logical flows for the relevant datapath +ovn_lflow_list: namespace="openshift-ovn-kubernetes", name="", + datapath="" +``` + +### Inspect ACLs and Security Policies + +**Scenario**: Verify that network policies are correctly translated to OVN ACLs. + +``` +# 1. Find ovnkube-node pods +pods_list: labelSelector="app=ovnkube-node" + +# 2. List all ACLs in the Northbound database +ovn_get: namespace="openshift-ovn-kubernetes", name="", database="nbdb", table="ACL" + +# 3. Filter for drop rules +ovn_get: namespace="openshift-ovn-kubernetes", name="", database="nbdb", + table="ACL", pattern="action=drop" + +# 4. Check port groups associated with network policies +ovn_get: namespace="openshift-ovn-kubernetes", name="", database="nbdb", table="Port_Group" +``` + +## Troubleshooting + +### Common Issues + +#### "Forbidden" errors + +**Symptom**: `Error: pods "..." is forbidden: User "..." cannot create resource "pods/exec"` + +**Solution**: Grant the required RBAC permissions (see [RBAC Requirements](#rbac-requirements)). + +#### "Container not found" errors + +**Symptom**: `Error: container "nbdb" not found in pod "..."` + +**Solution**: + +- Confirm the pod is an `ovnkube-node` pod (`kubectl get pod -o jsonpath='{.spec.containers[*].name}'`) +- On some OVN-K variants the container names may differ; use `pods_list` with the `app=ovnkube-node` label selector to find the correct pod + +#### Empty or truncated output + +**Symptom**: Output appears incomplete or is cut off. + +**Solution**: + +- By default, output is limited to 100 lines. Use the `head` parameter with a larger value, or use `tail` to get the end of the output +- Use the `pattern` parameter to filter results and reduce output volume +- For `ovn_get`, specify `columns` to limit the data returned per record + +#### "no record" errors from `ovn_get` + +**Symptom**: `ovs-nbctl: no row "..." in table "..."` + +**Solution**: + +- Verify the table name is correct (table names are case-sensitive, e.g., `Logical_Switch` not `logical_switch`) +- Verify the record identifier (UUID or name) exists by listing all records first (omit the `record` parameter) + +## Related Documentation + +- [Configuration Reference](../configuration.md) +- [Core Toolset](../README.md) — for `pods_list`, `pods_exec`, and other Kubernetes primitives diff --git a/evals/core-eval-testing/acp-anthropic/eval-ovn-kubernetes.yaml b/evals/core-eval-testing/acp-anthropic/eval-ovn-kubernetes.yaml new file mode 100644 index 000000000..bc10b39fe --- /dev/null +++ b/evals/core-eval-testing/acp-anthropic/eval-ovn-kubernetes.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "ovn-kubernetes-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: ovn-kubernetes + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 \ No newline at end of file diff --git a/evals/core-eval-testing/acp-google/eval-ovn-kubernetes.yaml b/evals/core-eval-testing/acp-google/eval-ovn-kubernetes.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/evals/core-eval-testing/builtin-anthropic/eval-ovn-kubernetes.yaml b/evals/core-eval-testing/builtin-anthropic/eval-ovn-kubernetes.yaml new file mode 100644 index 000000000..bc10b39fe --- /dev/null +++ b/evals/core-eval-testing/builtin-anthropic/eval-ovn-kubernetes.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "ovn-kubernetes-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: ovn-kubernetes + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 \ No newline at end of file diff --git a/evals/core-eval-testing/builtin-google/eval-ovn-kubernetes.yaml b/evals/core-eval-testing/builtin-google/eval-ovn-kubernetes.yaml new file mode 100644 index 000000000..bc10b39fe --- /dev/null +++ b/evals/core-eval-testing/builtin-google/eval-ovn-kubernetes.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "ovn-kubernetes-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: ovn-kubernetes + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 \ No newline at end of file diff --git a/evals/core-eval-testing/builtin-openai/eval-ovn-kubernetes.yaml b/evals/core-eval-testing/builtin-openai/eval-ovn-kubernetes.yaml new file mode 100644 index 000000000..bc10b39fe --- /dev/null +++ b/evals/core-eval-testing/builtin-openai/eval-ovn-kubernetes.yaml @@ -0,0 +1,25 @@ +kind: Eval +metadata: + name: "ovn-kubernetes-e2e" +config: + agent: + type: "file" + path: agent.yaml + mcpConfigFile: ../../mcp-config.yaml + extensions: + kubernetes: + package: https://github.com/mcpchecker/kubernetes-extension@v0.0.4 + llmJudge: + ref: + type: file + path: agent.yaml + taskSets: + - glob: ../../tasks/*/*/*.yaml + labelSelector: + suite: ovn-kubernetes + assertions: + toolsUsed: + - server: kubernetes + toolPattern: ".*" + minToolCalls: 1 + maxToolCalls: 20 \ No newline at end of file diff --git a/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-list-logical-routers.yaml b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-list-logical-routers.yaml new file mode 100644 index 000000000..47be1edd3 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-list-logical-routers.yaml @@ -0,0 +1,27 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-get-list-logical-routers" + difficulty: easy + category: "OVN Database Query" + description: "List all Logical_Router records from OVN Northbound database" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + List all Logical_Router records from the OVN Northbound database. + verify: + - llmJudge: + contains: "Shows all the Logical_Router records from the OVN Northbound database. Should include a distributed router handling inter-node routing and a local gateway router handling external connectivity" + reasoning: "Output should show all Logical_Router records including distributed router and gateway router" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_get" diff --git a/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-pattern-with-columns.yaml b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-pattern-with-columns.yaml new file mode 100644 index 000000000..b4ed69a42 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-pattern-with-columns.yaml @@ -0,0 +1,28 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-get-pattern-with-columns" + difficulty: medium + category: "OVN Database Query" + description: "Filter records with pattern and show specific columns" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + From the Logical_Router table in the OVN Northbound database, show me only the name and nat + columns for entries matching the pattern "GR_". + verify: + - llmJudge: + contains: "Shows Logical_Router entry matching the pattern \"GR_\" with name and nat columns." + reasoning: "Output should show Logical_Router entries matching pattern GR_ with name and nat columns" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_get" diff --git a/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-southbound-chassis.yaml b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-southbound-chassis.yaml new file mode 100644 index 000000000..70c390825 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-southbound-chassis.yaml @@ -0,0 +1,27 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-get-southbound-chassis" + difficulty: easy + category: "OVN Database Query" + description: "Query Chassis table from OVN Southbound database" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + Show me all Chassis records from the OVN Southbound database. + verify: + - llmJudge: + contains: "Shows Chassis records corresponding to all cluster nodes from the OVN Southbound database. Indicates whether a chassis is local or remote" + reasoning: "Output should show Chassis records for all cluster nodes with local/remote indication" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_get" diff --git a/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-specific-router.yaml b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-specific-router.yaml new file mode 100644 index 000000000..499c04df0 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-specific-router.yaml @@ -0,0 +1,27 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-get-specific-router" + difficulty: easy + category: "OVN Database Query" + description: "Get ovn_cluster_router record from Logical_Router table" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + Get the ovn_cluster_router record from the Logical_Router table in the OVN Northbound database. + verify: + - llmJudge: + contains: "Shows ovn_cluster_router record from the Logical_Router table. Should include list of ports, routing policies and static routes configured for the cluster router(k8s-cluster-router)" + reasoning: "Output should show ovn_cluster_router record with ports, routing policies, and static routes" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_get" diff --git a/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-switch-with-columns.yaml b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-switch-with-columns.yaml new file mode 100644 index 000000000..d955946bb --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-get/ovn-get-switch-with-columns.yaml @@ -0,0 +1,27 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-get-switch-with-columns" + difficulty: medium + category: "OVN Database Query" + description: "Get specific columns from Logical_Switch table" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + From the Logical_Switch table in the OVN Northbound database, show me only the name and ports columns. + verify: + - llmJudge: + contains: "Shows Logical_Switch table in the OVN Northbound database showing only the name and ports columns indicating switches with their associated ports. Should contain join, transit and node specific switches in the list" + reasoning: "Output should show Logical_Switch entries with name and ports columns including join, transit, and node specific switches" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_get" diff --git a/evals/tasks/ovn-kubernetes/ovn-get/setup.sh b/evals/tasks/ovn-kubernetes/ovn-get/setup.sh new file mode 100755 index 000000000..48f9b43f8 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-get/setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl get pods -n openshift-ovn-kubernetes -l app=ovnkube-node --field-selector=status.phase=Running -o name | grep -q . || { + echo "ERROR: No running ovnkube-node pods found" + exit 1 +} diff --git a/evals/tasks/ovn-kubernetes/ovn-lflow-list/ovn-lflow-list-datapath-with-pattern.yaml b/evals/tasks/ovn-kubernetes/ovn-lflow-list/ovn-lflow-list-datapath-with-pattern.yaml new file mode 100644 index 000000000..b6e53b622 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-lflow-list/ovn-lflow-list-datapath-with-pattern.yaml @@ -0,0 +1,30 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-lflow-list-datapath-with-pattern" + difficulty: medium + category: "OVN Logical Flows" + description: "List flows for specific datapath with pattern filter" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + From the ovn_cluster_router datapath, show me logical flows from the routing table. + verify: + - llmJudge: + contains: "Shows logical flows equivalent to routes in routing table for the ovn_cluster_router datapath" + reasoning: "Output should show logical flows representing the routing table for ovn_cluster_router" + - llmJudge: + contains: "Comprised of routing information from ovn_cluster_router to other switches" + reasoning: "Output should describe routing information between ovn_cluster_router and connected switches" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_lflow_list" diff --git a/evals/tasks/ovn-kubernetes/ovn-lflow-list/ovn-lflow-list-for-datapath.yaml b/evals/tasks/ovn-kubernetes/ovn-lflow-list/ovn-lflow-list-for-datapath.yaml new file mode 100644 index 000000000..552b2c4c0 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-lflow-list/ovn-lflow-list-for-datapath.yaml @@ -0,0 +1,27 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-lflow-list-for-datapath" + difficulty: medium + category: "OVN Logical Flows" + description: "List logical flows for a specific datapath" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + List logical flows for the join datapath from the OVN Southbound database. + verify: + - llmJudge: + contains: "Shows data from OVN Southbound database. Contains logical flows from both ingress and egress pipeline for the join datapath" + reasoning: "Output should show logical flows from both ingress and egress pipelines for the join datapath" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_lflow_list" diff --git a/evals/tasks/ovn-kubernetes/ovn-lflow-list/setup.sh b/evals/tasks/ovn-kubernetes/ovn-lflow-list/setup.sh new file mode 100755 index 000000000..48f9b43f8 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-lflow-list/setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl get pods -n openshift-ovn-kubernetes -l app=ovnkube-node --field-selector=status.phase=Running -o name | grep -q . || { + echo "ERROR: No running ovnkube-node pods found" + exit 1 +} diff --git a/evals/tasks/ovn-kubernetes/ovn-show/ovn-show-northbound.yaml b/evals/tasks/ovn-kubernetes/ovn-show/ovn-show-northbound.yaml new file mode 100644 index 000000000..baa220bf9 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-show/ovn-show-northbound.yaml @@ -0,0 +1,30 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-show-northbound" + difficulty: easy + category: "OVN Configuration" + description: "Display OVN Northbound database configuration using ovn_show tool" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + Show me the OVN Northbound database configuration from one of the ovnkube-node pods. show me last 1000 lines. + verify: + - llmJudge: + contains: "Shows logical topology comprised of node specific switches, join switch, transit switch, gateway router and ovn_cluster_router" + reasoning: "Output should describe the logical topology including switches and routers" + - llmJudge: + contains: "Includes logical router ports, logical switch ports, IP address, MAC address of ports and SNAT rules" + reasoning: "Output should include details about ports, addresses, and SNAT rules" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_show" diff --git a/evals/tasks/ovn-kubernetes/ovn-show/ovn-show-southbound.yaml b/evals/tasks/ovn-kubernetes/ovn-show/ovn-show-southbound.yaml new file mode 100644 index 000000000..82c23bbeb --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-show/ovn-show-southbound.yaml @@ -0,0 +1,30 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-show-southbound" + difficulty: easy + category: "OVN Configuration" + description: "Display OVN Southbound database configuration using ovn_show tool" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + limits: + timeout: "5m" + setup: + - script: + file: ./setup.sh + prompt: + inline: | + Show me the OVN Southbound database configuration from one of the ovnkube-node pods. + verify: + - llmJudge: + contains: "Shows Southbound database configuration from a node comprised of chassis information and port bindings" + reasoning: "Output should describe chassis information and port bindings" + - llmJudge: + contains: "Shows a overlay network using Geneve encapsulation with all nodes participating as chassis and connected ports" + reasoning: "Output should describe the Geneve overlay network with chassis and ports" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_show" diff --git a/evals/tasks/ovn-kubernetes/ovn-show/setup.sh b/evals/tasks/ovn-kubernetes/ovn-show/setup.sh new file mode 100755 index 000000000..48f9b43f8 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-show/setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl get pods -n openshift-ovn-kubernetes -l app=ovnkube-node --field-selector=status.phase=Running -o name | grep -q . || { + echo "ERROR: No running ovnkube-node pods found" + exit 1 +} diff --git a/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-detailed.yaml b/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-detailed.yaml new file mode 100644 index 000000000..e47c1ff68 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-detailed.yaml @@ -0,0 +1,121 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-trace-detailed" + difficulty: hard + category: "OVN Packet Tracing" + description: "Trace packet through OVN logical network with detailed output" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + requires: + - extension: kubernetes + as: k8s + limits: + timeout: "10m" + cleanupTimeout: "2m" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_trace" + - server: kubernetes + toolPattern: "ovn_show" + setup: + - script: + file: ./setup.sh + - k8s.create: + apiVersion: v1 + kind: Namespace + metadata: + name: ovn-trace-test + - script: + inline: | + #!/usr/bin/env bash + # Label a worker node for pod scheduling + oc get nodes -l node-role.kubernetes.io/worker="" --no-headers -o jsonpath='{.items[0].metadata.name}' | xargs -I {} oc label node {} schedule-app=true + - k8s.create: + apiVersion: v1 + kind: Pod + metadata: + name: source-pod + namespace: ovn-trace-test + labels: + app: ovn-trace + spec: + nodeSelector: + schedule-app: "true" + containers: + - name: nginx + image: quay.io/nginx/nginx-unprivileged:latest + - k8s.create: + apiVersion: v1 + kind: Pod + metadata: + name: dest-pod + namespace: ovn-trace-test + labels: + app: ovn-trace + spec: + nodeSelector: + schedule-app: "true" + containers: + - name: nginx + image: quay.io/nginx/nginx-unprivileged:latest + - script: + inline: | + #!/usr/bin/env bash + # Wait for source pod to be ready + if kubectl wait --for=condition=Ready pod/source-pod -n ovn-trace-test --timeout=300s; then + exit 0 + else + echo "ERROR: source-pod did not reach Ready state in time" + exit 1 + fi + - script: + inline: | + #!/usr/bin/env bash + # Wait for destination pod to be ready + if kubectl wait --for=condition=Ready pod/dest-pod -n ovn-trace-test --timeout=300s; then + exit 0 + else + echo "ERROR: dest-pod did not reach Ready state in time" + exit 1 + fi + prompt: + inline: | + Trace a TCP packet from source-pod to dest-pod in namespace ovn-trace-test on port 8080 using detailed mode. + + To construct the microflow, follow these steps: + 1. Use ovn_show to get the Northbound database configuration + 2. Find the port "ovn-trace-test_source-pod" in a switch - note the switch name (this is your datapath) + 3. From that port's addresses field, extract: MAC (first value) and IP (second value) - these are eth.src and ip4.src + 4. Find the port "ovn-trace-test_dest-pod" in the same switch + 5. From that port's addresses field, extract: MAC and IP - these are eth.dst and ip4.dst + 6. Construct the microflow string: + inport=="ovn-trace-test_source-pod" && eth.src== && ip4.src== && eth.dst== && ip4.dst== && tcp.dst==8080 && ip.ttl==64 + 7. Run ovn_trace with: datapath=, microflow=, mode="detailed" + verify: + - llmJudge: + contains: "Packet successfully delivered to destination pod. Should show that packet exits to ovn-trace-test_dest-pod port." + reasoning: "Output should confirm the packet was successfully delivered to the destination pod port" + - llmJudge: + contains: "Shows key trace stages in both ingress and egress pipeline of node specific switch the packet must travarse through." + reasoning: "Output should show the packet exits to the destination pod port" + cleanup: + - k8s.delete: + apiVersion: v1 + kind: Namespace + metadata: + name: ovn-trace-test + ignoreNotFound: true + - script: + inline: | + #!/usr/bin/env bash + # Wait for namespace to be fully deleted + kubectl wait --for=delete namespace/ovn-trace-test --timeout=120s 2>/dev/null || true + - script: + inline: | + #!/usr/bin/env bash + # Remove the label from the worker node + oc get nodes -l node-role.kubernetes.io/worker="" --no-headers -o jsonpath='{.items[0].metadata.name}' | xargs -I {} oc label node {} schedule-app- diff --git a/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-summary.yaml b/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-summary.yaml new file mode 100644 index 000000000..d2a8fb7fc --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-summary.yaml @@ -0,0 +1,118 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-trace-summary" + difficulty: hard + category: "OVN Packet Tracing" + description: "Trace packet through OVN logical network with summary output" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + requires: + - extension: kubernetes + as: k8s + limits: + timeout: "10m" + cleanupTimeout: "2m" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_trace" + - server: kubernetes + toolPattern: "ovn_show" + setup: + - script: + file: ./setup.sh + - k8s.create: + apiVersion: v1 + kind: Namespace + metadata: + name: ovn-trace-test + - script: + inline: | + #!/usr/bin/env bash + # Label a worker node for pod scheduling + oc get nodes -l node-role.kubernetes.io/worker="" --no-headers -o jsonpath='{.items[0].metadata.name}' | xargs -I {} oc label node {} schedule-app=true + - k8s.create: + apiVersion: v1 + kind: Pod + metadata: + name: source-pod + namespace: ovn-trace-test + labels: + app: ovn-trace + spec: + nodeSelector: + schedule-app: "true" + containers: + - name: nginx + image: quay.io/nginx/nginx-unprivileged:latest + - k8s.create: + apiVersion: v1 + kind: Pod + metadata: + name: dest-pod + namespace: ovn-trace-test + labels: + app: ovn-trace + spec: + nodeSelector: + schedule-app: "true" + containers: + - name: nginx + image: quay.io/nginx/nginx-unprivileged:latest + - script: + inline: | + #!/usr/bin/env bash + # Wait for source pod to be ready + if kubectl wait --for=condition=Ready pod/source-pod -n ovn-trace-test --timeout=300s; then + exit 0 + else + echo "ERROR: source-pod did not reach Ready state in time" + exit 1 + fi + - script: + inline: | + #!/usr/bin/env bash + # Wait for destination pod to be ready + if kubectl wait --for=condition=Ready pod/dest-pod -n ovn-trace-test --timeout=300s; then + exit 0 + else + echo "ERROR: dest-pod did not reach Ready state in time" + exit 1 + fi + prompt: + inline: | + Trace a TCP packet from source-pod to dest-pod in namespace ovn-trace-test on port 8080 using summary mode. + + To construct the microflow, follow these steps: + 1. Use ovn_show to get the Northbound database configuration + 2. Find the port "ovn-trace-test_source-pod" in a switch - note the switch name (this is your datapath) + 3. From that port's addresses field, extract: MAC (first value) and IP (second value) - these are eth.src and ip4.src + 4. Find the port "ovn-trace-test_dest-pod" in the same switch + 5. From that port's addresses field, extract: MAC and IP - these are eth.dst and ip4.dst + 6. Construct the microflow string: + inport=="ovn-trace-test_source-pod" && eth.src== && ip4.src== && eth.dst== && ip4.dst== && tcp.dst==8080 && ip.ttl==64 + 7. Run ovn_trace with: datapath=, microflow=, mode="summary" + verify: + - llmJudge: + contains: "Packet successfully delivered to destination pod. Should show that packet exits to ovn-trace-test_dest-pod port" + reasoning: "Output should confirm the packet was successfully delivered to the destination pod port" + cleanup: + - k8s.delete: + apiVersion: v1 + kind: Namespace + metadata: + name: ovn-trace-test + ignoreNotFound: true + - script: + inline: | + #!/usr/bin/env bash + # Wait for namespace to be fully deleted + kubectl wait --for=delete namespace/ovn-trace-test --timeout=120s 2>/dev/null || true + - script: + inline: | + #!/usr/bin/env bash + # Remove the label from the worker node + oc get nodes -l node-role.kubernetes.io/worker="" --no-headers -o jsonpath='{.items[0].metadata.name}' | xargs -I {} oc label node {} schedule-app- diff --git a/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-with-pattern.yaml b/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-with-pattern.yaml new file mode 100644 index 000000000..9554ab08c --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-trace/ovn-trace-with-pattern.yaml @@ -0,0 +1,118 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: "ovn-trace-with-pattern" + difficulty: hard + category: "OVN Packet Tracing" + description: "Trace packet and filter output using regex pattern" + labels: + suite: ovn-kubernetes + requires: ovn-kubernetes +spec: + requires: + - extension: kubernetes + as: k8s + limits: + timeout: "10m" + cleanupTimeout: "2m" + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "ovn_trace" + - server: kubernetes + toolPattern: "ovn_show" + setup: + - script: + file: ./setup.sh + - k8s.create: + apiVersion: v1 + kind: Namespace + metadata: + name: ovn-trace-test + - script: + inline: | + #!/usr/bin/env bash + # Label a worker node for pod scheduling + oc get nodes -l node-role.kubernetes.io/worker="" --no-headers -o jsonpath='{.items[0].metadata.name}' | xargs -I {} oc label node {} schedule-app=true + - k8s.create: + apiVersion: v1 + kind: Pod + metadata: + name: source-pod + namespace: ovn-trace-test + labels: + app: ovn-trace + spec: + nodeSelector: + schedule-app: "true" + containers: + - name: nginx + image: quay.io/nginx/nginx-unprivileged:latest + - k8s.create: + apiVersion: v1 + kind: Pod + metadata: + name: dest-pod + namespace: ovn-trace-test + labels: + app: ovn-trace + spec: + nodeSelector: + schedule-app: "true" + containers: + - name: nginx + image: quay.io/nginx/nginx-unprivileged:latest + - script: + inline: | + #!/usr/bin/env bash + # Wait for source pod to be ready + if kubectl wait --for=condition=Ready pod/source-pod -n ovn-trace-test --timeout=300s; then + exit 0 + else + echo "ERROR: source-pod did not reach Ready state in time" + exit 1 + fi + - script: + inline: | + #!/usr/bin/env bash + # Wait for destination pod to be ready + if kubectl wait --for=condition=Ready pod/dest-pod -n ovn-trace-test --timeout=300s; then + exit 0 + else + echo "ERROR: dest-pod did not reach Ready state in time" + exit 1 + fi + prompt: + inline: | + Trace a TCP packet from source-pod to dest-pod in namespace ovn-trace-test and filter output to show only lines containing "output". + + To construct the microflow, follow these steps: + 1. Use ovn_show to get the Northbound database configuration + 2. Find the port "ovn-trace-test_source-pod" in a switch - note the switch name (this is your datapath) + 3. From that port's addresses field, extract: MAC (first value) and IP (second value) - these are eth.src and ip4.src + 4. Find the port "ovn-trace-test_dest-pod" in the same switch + 5. From that port's addresses field, extract: MAC and IP - these are eth.dst and ip4.dst + 6. Construct the microflow string: + inport=="ovn-trace-test_source-pod" && eth.src== && ip4.src== && eth.dst== && ip4.dst== && tcp.dst==8080 && ip.ttl==64 + 7. Run ovn_trace with: datapath=, microflow=, pattern="acl" + verify: + - llmJudge: + contains: "Packet exits to ovn-trace-test_dest-pod" + reasoning: "Output should show the packet exits to the destination pod port" + cleanup: + - k8s.delete: + apiVersion: v1 + kind: Namespace + metadata: + name: ovn-trace-test + ignoreNotFound: true + - script: + inline: | + #!/usr/bin/env bash + # Wait for namespace to be fully deleted + kubectl wait --for=delete namespace/ovn-trace-test --timeout=120s 2>/dev/null || true + - script: + inline: | + #!/usr/bin/env bash + # Remove the label from the worker node + oc get nodes -l node-role.kubernetes.io/worker="" --no-headers -o jsonpath='{.items[0].metadata.name}' | xargs -I {} oc label node {} schedule-app- diff --git a/evals/tasks/ovn-kubernetes/ovn-trace/setup.sh b/evals/tasks/ovn-kubernetes/ovn-trace/setup.sh new file mode 100755 index 000000000..48f9b43f8 --- /dev/null +++ b/evals/tasks/ovn-kubernetes/ovn-trace/setup.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl get pods -n openshift-ovn-kubernetes -l app=ovnkube-node --field-selector=status.phase=Running -o name | grep -q . || { + echo "ERROR: No running ovnkube-node pods found" + exit 1 +} diff --git a/go.mod b/go.mod index 83abedf02..61751c6ee 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/miekg/dns v1.1.72 github.com/modelcontextprotocol/go-sdk v1.6.1 + github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.1.0 github.com/prometheus/client_golang v1.23.2 github.com/rhobs/obs-mcp v0.6.0 github.com/spf13/afero v1.15.0 @@ -51,11 +52,11 @@ require ( k8s.io/cli-runtime v0.36.2 k8s.io/client-go v0.36.2 k8s.io/klog/v2 v2.140.0 - k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 + k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0 k8s.io/kubectl v0.36.2 k8s.io/metrics v0.36.2 k8s.io/streaming v0.36.2 - k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 + k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.24.1 sigs.k8s.io/yaml v1.6.0 @@ -200,5 +201,5 @@ require ( sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect ) diff --git a/go.sum b/go.sum index 5b577e32e..64cb4c180 100644 --- a/go.sum +++ b/go.sum @@ -328,16 +328,18 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= -github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= +github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260617154302-0835b732e6cd h1:sSOiEB68HiUbYx4iScn1FCyuksa5LWN7G3GXfnlUk2o= github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260617154302-0835b732e6cd/go.mod h1:ThsbZTM538g95wb3Aj/+XHfFfpW/YBHdO5o6W0f+IRw= +github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.1.0 h1:r2s9D5p8TYXJ7o0FUzDbnUSydsw7M02tmPiAqqI489s= +github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.1.0/go.mod h1:f1kKpNljFj4q4gJfbMW/KewCddoej+epqHMxs98VT3Y= github.com/pavolloffay/opentelemetry-mcp-server/modules/schemagen v0.0.0-20260710124846-8bb49fd6ccc7 h1:mUR6Z3GIYvOMUIkcs+2p0ggzkaK7La3ZF7Ki8X9Kx0A= github.com/pavolloffay/opentelemetry-mcp-server/modules/schemagen v0.0.0-20260710124846-8bb49fd6ccc7/go.mod h1:wW9qWwV3FCBH7YduES6KvpOw9dz5+Y63+yZqiQ9eT2Y= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -569,16 +571,16 @@ k8s.io/component-base v0.36.2 h1:Z0VH80O7Ng0HDZnZj3WRR3urEGa0kTwmO8CwEwjVK1w= k8s.io/component-base v0.36.2/go.mod h1:mGfFOA7Gwpdm1VW2cwSQYbiDIlz8GD2WGwH88QSeCyA= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 h1:mPMaPMpBij2V1Wv/fR+HW124vVGXXvOSS9ver/9yjWs= -k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= +k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0 h1:CVjOUCTXINUThEmDs25FNSna0+vnGSoTleN+wiJu6hE= +k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0/go.mod h1:rcZ+P5cEvHQB+m154WBOatIGBgOEPjzmLkXjkHfg3ms= k8s.io/kubectl v0.36.2 h1:rpUGGpeL09XVOLep2yle5jrtk//JA1L6ZHfkQQtVEwk= k8s.io/kubectl v0.36.2/go.mod h1:gVbQ3B/yb4bSR2ggQ7rd0W6icUSWs7sduH4e16Vii+0= k8s.io/metrics v0.36.2 h1:yfUIe2Vwx2cQAIpVYcin1JXdabrRz98oTxP2HJTxHj8= k8s.io/metrics v0.36.2/go.mod h1:Q/dNyLLzgSxPu0/e+996Du4pjutfEyyHOKgK0lkncp0= k8s.io/streaming v0.36.2 h1:NSKthPPg9UFSKsRauVJUVGH2Dvn8fhKmY4qrMkw/p98= k8s.io/streaming v0.36.2/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd h1:yeh+smYaouOwhkyCPj+AYACt1MeD+EI4mXSzSbmtj10= knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd/go.mod h1:o/XS1E/hYh9IR8deEEiJG4kKtQfqnf9Gwt5bwp2x4AU= oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= @@ -595,7 +597,7 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7 sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo= -sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/mcp/openshift_modules.go b/pkg/mcp/openshift_modules.go index ea284d2bf..528e16f60 100644 --- a/pkg/mcp/openshift_modules.go +++ b/pkg/mcp/openshift_modules.go @@ -6,5 +6,6 @@ import ( _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/netedge" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/oadp" _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/openshift" + _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/ovnkubernetes" _ "github.com/rhobs/obs-mcp/pkg/toolset" ) diff --git a/pkg/mcp/ovn_kubernetes_test.go b/pkg/mcp/ovn_kubernetes_test.go new file mode 100644 index 000000000..bcfdcadab --- /dev/null +++ b/pkg/mcp/ovn_kubernetes_test.go @@ -0,0 +1,336 @@ +package mcp + +import ( + "bytes" + "io" + "net/http" + "sync/atomic" + "testing" + + "github.com/BurntSushi/toml" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/suite" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/containers/kubernetes-mcp-server/internal/test" +) + +const ( + ovnTestNamespace = "openshift-ovn-kubernetes" + ovnTestPodName = "ovnkube-node-abc12" +) + +type OVNKubernetesSuite struct { + BaseMcpSuite + mockServer *test.MockServer + lastContainer atomic.Value +} + +func (s *OVNKubernetesSuite) SetupTest() { + s.BaseMcpSuite.SetupTest() + s.mockServer = test.NewMockServer() + s.mockServer.Handle(test.NewDiscoveryClientHandler()) + s.Cfg.KubeConfig = s.mockServer.KubeconfigFile(s.T()) + s.Require().NoError(toml.Unmarshal([]byte(` + toolsets = [ "ovn-kubernetes" ] + `), s.Cfg), "Expected to parse toolsets config") + s.setupPodHandler() +} + +func (s *OVNKubernetesSuite) TearDownTest() { + s.BaseMcpSuite.TearDownTest() + if s.mockServer != nil { + s.mockServer.Close() + } +} + +func ovnkubeNodePod() *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ovnTestNamespace, + Name: ovnTestPodName, + Annotations: map[string]string{ + "kubectl.kubernetes.io/default-container": "ovnkube-controller", + }, + }, + Spec: v1.PodSpec{Containers: []v1.Container{ + {Name: "ovn-controller"}, + {Name: "northd"}, + {Name: "nbdb"}, + {Name: "sbdb"}, + {Name: "ovnkube-controller"}, + }}, + } +} + +func (s *OVNKubernetesSuite) setupPodHandler() { + s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/api/v1/namespaces/"+ovnTestNamespace+"/pods/"+ovnTestPodName { + return + } + test.WriteObject(w, ovnkubeNodePod()) + })) +} + +func (s *OVNKubernetesSuite) setupRecordingExecHandler() { + s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/api/v1/namespaces/"+ovnTestNamespace+"/pods/"+ovnTestPodName+"/exec" { + return + } + s.lastContainer.Store(req.URL.Query().Get("container")) + var stdin, stdout bytes.Buffer + ctx, err := test.CreateHTTPStreams(w, req, &test.StreamOptions{ + Stdin: &stdin, + Stdout: &stdout, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + defer func() { _ = ctx.Close() }() + _, _ = io.WriteString(ctx.StdoutStream, "mock-output\n") + })) +} + +func (s *OVNKubernetesSuite) setupStderrExecHandler() { + s.mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/api/v1/namespaces/"+ovnTestNamespace+"/pods/"+ovnTestPodName+"/exec" { + return + } + var stdin, stdout bytes.Buffer + ctx, err := test.CreateHTTPStreams(w, req, &test.StreamOptions{ + Stdin: &stdin, + Stdout: &stdout, + }) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + return + } + defer func() { _ = ctx.Close() }() + _, _ = io.WriteString(ctx.StderrStream, "simulated error\n") + })) +} + +func (s *OVNKubernetesSuite) TestContainerRouting() { + s.setupRecordingExecHandler() + s.InitMcpClient() + + s.Run("ovn_show with nbdb routes to nbdb container", func() { + result, err := s.CallTool("ovn_show", map[string]any{ + "namespace": ovnTestNamespace, + "name": ovnTestPodName, + "database": "nbdb", + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("nbdb", s.lastContainer.Load().(string)) + }) + + s.Run("ovn_show with sbdb routes to sbdb container", func() { + result, err := s.CallTool("ovn_show", map[string]any{ + "namespace": ovnTestNamespace, + "name": ovnTestPodName, + "database": "sbdb", + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("sbdb", s.lastContainer.Load().(string)) + }) + + s.Run("ovn_get with nbdb routes to nbdb container", func() { + result, err := s.CallTool("ovn_get", map[string]any{ + "namespace": ovnTestNamespace, + "name": ovnTestPodName, + "database": "nbdb", + "table": "Logical_Switch", + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("nbdb", s.lastContainer.Load().(string)) + }) + + s.Run("ovn_get with sbdb routes to sbdb container", func() { + result, err := s.CallTool("ovn_get", map[string]any{ + "namespace": ovnTestNamespace, + "name": ovnTestPodName, + "database": "sbdb", + "table": "Chassis", + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("sbdb", s.lastContainer.Load().(string)) + }) + + s.Run("ovn_lflow_list routes to sbdb container", func() { + result, err := s.CallTool("ovn_lflow_list", map[string]any{ + "namespace": ovnTestNamespace, + "name": ovnTestPodName, + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("sbdb", s.lastContainer.Load().(string)) + }) + + s.Run("ovn_trace routes to northd container", func() { + result, err := s.CallTool("ovn_trace", map[string]any{ + "namespace": ovnTestNamespace, + "name": ovnTestPodName, + "datapath": "test-datapath", + "microflow": `inport=="port1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5`, + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("northd", s.lastContainer.Load().(string)) + }) +} + +func (s *OVNKubernetesSuite) TestDefaultNamespace() { + s.setupRecordingExecHandler() + s.InitMcpClient() + + s.Run("ovn_show uses default namespace when omitted", func() { + result, err := s.CallTool("ovn_show", map[string]any{ + "name": ovnTestPodName, + "database": "nbdb", + }) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + s.Equal("nbdb", s.lastContainer.Load().(string)) + }) +} + +func (s *OVNKubernetesSuite) TestExecStderrReturnsError() { + s.setupStderrExecHandler() + s.InitMcpClient() + + for _, tc := range []struct { + tool string + params map[string]any + }{ + {"ovn_show", map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName, "database": "nbdb"}}, + {"ovn_get", map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName, "database": "nbdb", "table": "Logical_Switch"}}, + {"ovn_lflow_list", map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName}}, + {"ovn_trace", map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName, "datapath": "test-dp", "microflow": `inport=="port1" && ip4.src==10.0.0.1`}}, + } { + s.Run(tc.tool+" returns error on stderr", func() { + result, err := s.CallTool(tc.tool, tc.params) + s.Require().NoError(err) + s.True(result.IsError) + }) + } +} + +func (s *OVNKubernetesSuite) TestMissingRequiredParams() { + s.setupRecordingExecHandler() + s.InitMcpClient() + + for _, tc := range []struct { + tool string + required []string + allParams map[string]any + }{ + { + tool: "ovn_show", + required: []string{"name", "database"}, + allParams: map[string]any{ + "namespace": ovnTestNamespace, "name": ovnTestPodName, "database": "nbdb", + }, + }, + { + tool: "ovn_get", + required: []string{"name", "database", "table"}, + allParams: map[string]any{ + "namespace": ovnTestNamespace, "name": ovnTestPodName, "database": "nbdb", "table": "Logical_Switch", + }, + }, + { + tool: "ovn_lflow_list", + required: []string{"name"}, + allParams: map[string]any{ + "namespace": ovnTestNamespace, "name": ovnTestPodName, + }, + }, + { + tool: "ovn_trace", + required: []string{"name", "datapath", "microflow"}, + allParams: map[string]any{ + "namespace": ovnTestNamespace, "name": ovnTestPodName, "datapath": "test-dp", + "microflow": `inport=="port1" && ip4.src==10.0.0.1`, + }, + }, + } { + s.Run(tc.tool+" missing required params", func() { + for _, param := range tc.required { + s.Run("missing "+param, func() { + params := make(map[string]any, len(tc.allParams)) + for k, v := range tc.allParams { + params[k] = v + } + delete(params, param) + result, err := s.CallTool(tc.tool, params) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Truef(result.IsError, "expected error for missing %s", param) + s.Contains(result.Content[0].(*mcp.TextContent).Text, param+" parameter required") + }) + } + }) + } +} + +func (s *OVNKubernetesSuite) TestOutputContent() { + s.setupRecordingExecHandler() + s.InitMcpClient() + + for _, tc := range []struct { + tool string + params map[string]any + expected []string + }{ + { + tool: "ovn_show", + params: map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName, "database": "nbdb"}, + expected: []string{`"database":"nbdb"`}, + }, + { + tool: "ovn_get", + params: map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName, "database": "sbdb", "table": "Chassis"}, + expected: []string{`"database":"sbdb"`, `"table":"Chassis"`}, + }, + { + tool: "ovn_lflow_list", + params: map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName}, + }, + { + tool: "ovn_trace", + params: map[string]any{"namespace": ovnTestNamespace, "name": ovnTestPodName, "datapath": "test-datapath", "microflow": `inport=="port1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5`}, + expected: []string{`"datapath":"test-datapath"`}, + }, + } { + s.Run(tc.tool+" returns output", func() { + result, err := s.CallTool(tc.tool, tc.params) + s.Require().NoError(err) + s.Require().NotNil(result) + s.Falsef(result.IsError, "call tool failed: %v", result.Content) + text := result.Content[0].(*mcp.TextContent).Text + s.Contains(text, "mock-output") + for _, expected := range tc.expected { + s.Contains(text, expected) + } + }) + } +} + +func TestOVNKubernetes(t *testing.T) { + suite.Run(t, new(OVNKubernetesSuite)) +} diff --git a/pkg/mcp/testdata/toolsets-ovn-kubernetes-tools.json b/pkg/mcp/testdata/toolsets-ovn-kubernetes-tools.json new file mode 100644 index 000000000..87ca0bb5f --- /dev/null +++ b/pkg/mcp/testdata/toolsets-ovn-kubernetes-tools.json @@ -0,0 +1,233 @@ +[ + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Get" + }, + "description": "Query records from an OVN database table with flexible filtering.\n\nThis is a versatile command that can:\n1. List all records in a table (when no record specified)\n2. Get a specific record (when record specified)\n\nCommon Northbound tables: Logical_Switch, Logical_Router, Logical_Switch_Port, \nLogical_Router_Port, ACL, Address_Set, Port_Group, Load_Balancer, NAT\n\nCommon Southbound tables: Chassis, Port_Binding, Datapath_Binding, Logical_Flow,\nMAC_Binding, Multicast_Group, SB_Global\n\nReturns 100 lines by default; use head/tail to adjust.\n\nExample listing all records:\n{\n \"database\": \"nbdb\",\n \"table\": \"Port_Group\",\n \"output\": \"_uuid: 1234-5678\\nname: \\\"pg_default\\\"\\nports: [...]\\n\\n_uuid: abcd-efgh\\n...\"\n}\n\nExample getting a specific record:\n{\n \"database\": \"nbdb\",\n \"table\": \"Logical_Router\",\n \"record\": \"ovn_cluster_router\",\n \"output\": \"_uuid: 4c4a0a35-348c-41cc-8417-53a618e0c383\\nname: ovn_cluster_router\\nports: [...]\"\n}\n\nExample getting specific columns:\n{\n \"database\": \"nbdb\",\n \"table\": \"Logical_Switch\",\n \"columns\": \"name,ports\",\n \"output\": \"name: ovn-worker\\nports: [uuid1, uuid2]\\n\\nname: join\\nports: [uuid3]\"\n}", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "columns": { + "description": "Comma-separated list of columns to display (e.g., \"name,_uuid,ports\")", + "type": "string" + }, + "database": { + "description": "OVN database to query - \"nbdb\" for Northbound or \"sbdb\" for Southbound", + "enum": [ + "nbdb", + "sbdb" + ], + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "type": "integer" + }, + "name": { + "description": "Name of the pod running OVN", + "type": "string" + }, + "namespace": { + "default": "openshift-ovn-kubernetes", + "description": "Kubernetes namespace of the OVN pod", + "type": "string" + }, + "pattern": { + "description": "Regex pattern to filter results. Only applies when listing all records.", + "type": "string" + }, + "record": { + "description": "Record identifier (UUID or name). If not specified, lists all records", + "type": "string" + }, + "table": { + "description": "Name of the table (e.g., \"Logical_Switch\", \"Port_Binding\")", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "type": "integer" + } + }, + "required": [ + "name", + "database", + "table" + ], + "type": "object" + }, + "name": "ovn_get", + "title": "OVN: Get" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Logical Flow List" + }, + "description": "List logical flows from the OVN Southbound database.\n\nRuns 'ovn-sbctl lflow-list' to retrieve logical flows which represent the compiled\nlogical network pipeline. This is essential for debugging packet forwarding.\nReturns 100 lines by default; use head/tail to adjust.\n\nExample output:\n{\n \"datapath\": \"node1\",\n \"flows\": [\n \"table=0 (ls_in_port_sec_l2), priority=100, match=(inport == \\\"pod1\\\"), action=(next;)\",\n \"table=1 (ls_in_port_sec_ip), priority=90, match=(ip4), action=(next;)\"\n ]\n}", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "datapath": { + "description": "Datapath name or UUID to filter flows for a specific logical switch/router", + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "type": "integer" + }, + "name": { + "description": "Name of the pod running OVN", + "type": "string" + }, + "namespace": { + "default": "openshift-ovn-kubernetes", + "description": "Kubernetes namespace of the OVN pod", + "type": "string" + }, + "pattern": { + "description": "Regex pattern to filter flows", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "type": "integer" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "ovn_lflow_list", + "title": "OVN: Logical Flow List" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Show" + }, + "description": "Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database.\n\nFor Northbound (nbdb): Runs 'ovn-nbctl show' and displays logical switches, logical routers,\ntheir ports, and connections between them.\n\nFor Southbound (sbdb): Runs 'ovn-sbctl show' and displays chassis information, port bindings,\nand their relationships. Returns 100 lines by default; use head/tail to adjust.\n\nExample output for nbdb:\n{\n \"database\": \"nbdb\",\n \"output\": \"switch 1234-5678 (node1)\\n port node1-k8s\\n addresses: [\\\"00:00:00:00:00:01\\\"]\\n...\"\n}", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "database": { + "description": "OVN database to query - \"nbdb\" for Northbound or \"sbdb\" for Southbound", + "enum": [ + "nbdb", + "sbdb" + ], + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "type": "integer" + }, + "name": { + "description": "Name of the pod running OVN (e.g., \"ovnkube-node-xxxxx\")", + "type": "string" + }, + "namespace": { + "default": "openshift-ovn-kubernetes", + "description": "Kubernetes namespace of the OVN pod (e.g., \"openshift-ovn-kubernetes\")", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "type": "integer" + } + }, + "required": [ + "name", + "database" + ], + "type": "object" + }, + "name": "ovn_show", + "title": "OVN: Show" + }, + { + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": true, + "title": "OVN: Trace" + }, + "description": "Trace a packet through the OVN logical network.\n\nRuns 'ovn-trace' to simulate packet processing through the logical network pipeline.\nThis shows which logical flows match, what actions are taken, and the final disposition.\n\nThe trace is essential for debugging connectivity issues and understanding how traffic\nflows through the OVN logical network. Returns 100 lines by default; use head/tail to adjust.\n\nMicroflow specification examples:\n- inport==\"pod1\" \u0026\u0026 eth.src==00:00:00:00:00:01 \u0026\u0026 ip4.src==10.244.0.5 \u0026\u0026 ip4.dst==10.244.1.5\n- inport==\"pod1\" \u0026\u0026 eth.src==00:00:00:00:00:01 \u0026\u0026 icmp \u0026\u0026 ip4.src==10.244.0.5 \u0026\u0026 ip4.dst==8.8.8.8\n\nExample output:\n{\n \"datapath\": \"node1\",\n \"microflow\": \"inport==\\\"pod1\\\" \u0026\u0026 ...\",\n \"output\": \"ingress(dp=\\\"node1\\\", inport=\\\"pod1\\\")\\n 0. ls_in_port_sec_l2: inport == \\\"pod1\\\", priority 50, uuid 1234\\n next;\\n...\"\n}", + "inputSchema": { + "properties": { + "apply_tail_first": { + "description": "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + "type": "boolean" + }, + "datapath": { + "description": "Name of the logical switch or router to start the trace", + "type": "string" + }, + "head": { + "description": "Return only first N lines. Default: 100 lines if tail is not specified", + "type": "integer" + }, + "microflow": { + "description": "Microflow specification describing the packet (e.g., \"inport==\\\"pod1\\\" \u0026\u0026 eth.src==00:00:00:00:00:01 \u0026\u0026 ip4.src==10.244.0.5 \u0026\u0026 ip4.dst==10.244.1.5\")", + "type": "string" + }, + "mode": { + "default": "detailed", + "description": "Output verbosity mode - \"detailed\" (default), \"summary\", or \"minimal\"", + "enum": [ + "detailed", + "summary", + "minimal" + ], + "type": "string" + }, + "name": { + "description": "Name of the pod running OVN", + "type": "string" + }, + "namespace": { + "default": "openshift-ovn-kubernetes", + "description": "Kubernetes namespace of the OVN pod", + "type": "string" + }, + "pattern": { + "description": "Regex pattern to filter trace output", + "type": "string" + }, + "tail": { + "description": "Return only last N lines", + "type": "integer" + } + }, + "required": [ + "name", + "datapath", + "microflow" + ], + "type": "object" + }, + "name": "ovn_trace", + "title": "OVN: Trace" + } +] diff --git a/pkg/mcp/toolsets_test.go b/pkg/mcp/toolsets_test.go index 9a19cff0b..43d9e9050 100644 --- a/pkg/mcp/toolsets_test.go +++ b/pkg/mcp/toolsets_test.go @@ -22,6 +22,7 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt" mgToolset "github.com/containers/kubernetes-mcp-server/pkg/toolsets/mustgather" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/openshift" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/ovnkubernetes" "github.com/containers/kubernetes-mcp-server/pkg/toolsets/tekton" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/suite" @@ -201,6 +202,7 @@ func (s *ToolsetsSuite) TestGranularToolsetsTools() { &helm.Toolset{}, &kiali.Toolset{}, &kubevirt.Toolset{}, + &ovnkubernetes.Toolset{}, &tekton.Toolset{}, &clusterDiagnosticsToolset.Toolset{}, } diff --git a/pkg/toolsets/ovnkubernetes/ovn/ovn.go b/pkg/toolsets/ovnkubernetes/ovn/ovn.go new file mode 100644 index 000000000..faaee5dcc --- /dev/null +++ b/pkg/toolsets/ovnkubernetes/ovn/ovn.go @@ -0,0 +1,449 @@ +package ovn + +import ( + "context" + "fmt" + + "github.com/google/jsonschema-go/jsonschema" + k8stypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types" + ovnmcp "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp" + ovntypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" +) + +func InitOVNTools() []api.ServerTool { + return []api.ServerTool{ + {Tool: api.Tool{ + Name: "ovn_show", + Description: `Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database. + +For Northbound (nbdb): Runs 'ovn-nbctl show' and displays logical switches, logical routers, +their ports, and connections between them. + +For Southbound (sbdb): Runs 'ovn-sbctl show' and displays chassis information, port bindings, +and their relationships. Returns 100 lines by default; use head/tail to adjust. + +Example output for nbdb: +{ + "database": "nbdb", + "output": "switch 1234-5678 (node1)\n port node1-k8s\n addresses: [\"00:00:00:00:00:01\"]\n..." +}`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Default: api.ToRawMessage("openshift-ovn-kubernetes"), + Description: `Kubernetes namespace of the OVN pod (e.g., "openshift-ovn-kubernetes")`, + }, + "name": { + Type: "string", + Description: `Name of the pod running OVN (e.g., "ovnkube-node-xxxxx")`, + }, + "database": { + Type: "string", + Enum: []any{"nbdb", "sbdb"}, + Description: `OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound`, + }, + "head": { + Type: "integer", + Description: fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnmcp.DefaultMaxLines), + }, + "tail": { + Type: "integer", + Description: "Return only last N lines", + }, + "apply_tail_first": { + Type: "boolean", + Description: "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + }, + }, + Required: []string{"name", "database"}, + }, + Annotations: api.ToolAnnotations{ + Title: "OVN: Show", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: ovnShow}, + {Tool: api.Tool{ + Name: "ovn_get", + Description: `Query records from an OVN database table with flexible filtering. + +This is a versatile command that can: +1. List all records in a table (when no record specified) +2. Get a specific record (when record specified) + +Common Northbound tables: Logical_Switch, Logical_Router, Logical_Switch_Port, +Logical_Router_Port, ACL, Address_Set, Port_Group, Load_Balancer, NAT + +Common Southbound tables: Chassis, Port_Binding, Datapath_Binding, Logical_Flow, +MAC_Binding, Multicast_Group, SB_Global + +Returns 100 lines by default; use head/tail to adjust. + +Example listing all records: +{ + "database": "nbdb", + "table": "Port_Group", + "output": "_uuid: 1234-5678\nname: \"pg_default\"\nports: [...]\n\n_uuid: abcd-efgh\n..." +} + +Example getting a specific record: +{ + "database": "nbdb", + "table": "Logical_Router", + "record": "ovn_cluster_router", + "output": "_uuid: 4c4a0a35-348c-41cc-8417-53a618e0c383\nname: ovn_cluster_router\nports: [...]" +} + +Example getting specific columns: +{ + "database": "nbdb", + "table": "Logical_Switch", + "columns": "name,ports", + "output": "name: ovn-worker\nports: [uuid1, uuid2]\n\nname: join\nports: [uuid3]" +}`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Default: api.ToRawMessage("openshift-ovn-kubernetes"), + Description: "Kubernetes namespace of the OVN pod", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN", + }, + "database": { + Type: "string", + Enum: []any{"nbdb", "sbdb"}, + Description: `OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound`, + }, + "table": { + Type: "string", + Description: `Name of the table (e.g., "Logical_Switch", "Port_Binding")`, + }, + "record": { + Type: "string", + Description: `Record identifier (UUID or name). If not specified, lists all records`, + }, + "columns": { + Type: "string", + Description: `Comma-separated list of columns to display (e.g., "name,_uuid,ports")`, + }, + "pattern": { + Type: "string", + Description: `Regex pattern to filter results. Only applies when listing all records.`, + }, + "head": { + Type: "integer", + Description: fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnmcp.DefaultMaxLines), + }, + "tail": { + Type: "integer", + Description: "Return only last N lines", + }, + "apply_tail_first": { + Type: "boolean", + Description: "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + }, + }, + Required: []string{"name", "database", "table"}, + }, + Annotations: api.ToolAnnotations{ + Title: "OVN: Get", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: ovnGet}, + {Tool: api.Tool{ + Name: "ovn_lflow_list", + Description: `List logical flows from the OVN Southbound database. + +Runs 'ovn-sbctl lflow-list' to retrieve logical flows which represent the compiled +logical network pipeline. This is essential for debugging packet forwarding. +Returns 100 lines by default; use head/tail to adjust. + +Example output: +{ + "datapath": "node1", + "flows": [ + "table=0 (ls_in_port_sec_l2), priority=100, match=(inport == \"pod1\"), action=(next;)", + "table=1 (ls_in_port_sec_ip), priority=90, match=(ip4), action=(next;)" + ] +}`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Default: api.ToRawMessage("openshift-ovn-kubernetes"), + Description: "Kubernetes namespace of the OVN pod", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN", + }, + "datapath": { + Type: "string", + Description: "Datapath name or UUID to filter flows for a specific logical switch/router", + }, + "pattern": { + Type: "string", + Description: "Regex pattern to filter flows", + }, + "head": { + Type: "integer", + Description: fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnmcp.DefaultMaxLines), + }, + "tail": { + Type: "integer", + Description: "Return only last N lines", + }, + "apply_tail_first": { + Type: "boolean", + Description: "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + }, + }, + Required: []string{"name"}, + }, + Annotations: api.ToolAnnotations{ + Title: "OVN: Logical Flow List", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: ovnLFlowList}, + {Tool: api.Tool{ + Name: "ovn_trace", + Description: `Trace a packet through the OVN logical network. + +Runs 'ovn-trace' to simulate packet processing through the logical network pipeline. +This shows which logical flows match, what actions are taken, and the final disposition. + +The trace is essential for debugging connectivity issues and understanding how traffic +flows through the OVN logical network. Returns 100 lines by default; use head/tail to adjust. + +Microflow specification examples: +- inport=="pod1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5 +- inport=="pod1" && eth.src==00:00:00:00:00:01 && icmp && ip4.src==10.244.0.5 && ip4.dst==8.8.8.8 + +Example output: +{ + "datapath": "node1", + "microflow": "inport==\"pod1\" && ...", + "output": "ingress(dp=\"node1\", inport=\"pod1\")\n 0. ls_in_port_sec_l2: inport == \"pod1\", priority 50, uuid 1234\n next;\n..." +}`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "namespace": { + Type: "string", + Default: api.ToRawMessage("openshift-ovn-kubernetes"), + Description: "Kubernetes namespace of the OVN pod", + }, + "name": { + Type: "string", + Description: "Name of the pod running OVN", + }, + "datapath": { + Type: "string", + Description: "Name of the logical switch or router to start the trace", + }, + "microflow": { + Type: "string", + Description: `Microflow specification describing the packet (e.g., "inport==\"pod1\" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5")`, + }, + "mode": { + Type: "string", + Enum: []any{"detailed", "summary", "minimal"}, + Default: api.ToRawMessage("detailed"), + Description: `Output verbosity mode - "detailed" (default), "summary", or "minimal"`, + }, + "pattern": { + Type: "string", + Description: "Regex pattern to filter trace output", + }, + "head": { + Type: "integer", + Description: fmt.Sprintf("Return only first N lines. Default: %d lines if tail is not specified", ovnmcp.DefaultMaxLines), + }, + "tail": { + Type: "integer", + Description: "Return only last N lines", + }, + "apply_tail_first": { + Type: "boolean", + Description: "If both head and tail are set and apply_tail_first is true, apply tail before head. Default: false", + }, + }, + Required: []string{"name", "datapath", "microflow"}, + }, + Annotations: api.ToolAnnotations{ + Title: "OVN: Trace", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + IdempotentHint: ptr.To(true), + OpenWorldHint: ptr.To(true), + }, + }, Handler: ovnTrace}, + } +} + +func ovnShow(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + namespace := p.OptionalString("namespace", "openshift-ovn-kubernetes") + name := p.RequiredString("name") + database := p.RequiredString("database") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + if err := p.Err(); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to run ovn show: %w", err)), nil + } + + server, err := newOVNServer(kubernetes.NewCore(params), containerForDatabase(database)) + if err != nil { + return api.NewToolCallResult("", err), nil + } + _, result, err := server.Show(params.Context, nil, ovntypes.ShowParams{ + NamespacedNameParams: namespacedName(namespace, name), + Database: ovntypes.Database(database), + HeadTailParams: headtail.HeadTailParams{Head: head, Tail: tail, ApplyTailFirst: applyTailFirst}, + }) + if err != nil { + return api.NewToolCallResult("", err), nil + } + return api.NewToolCallResultStructured(result, nil), nil +} + +func ovnGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + namespace := p.OptionalString("namespace", "openshift-ovn-kubernetes") + name := p.RequiredString("name") + database := p.RequiredString("database") + table := p.RequiredString("table") + record := p.OptionalString("record", "") + columns := p.OptionalString("columns", "") + pat := p.OptionalString("pattern", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + if err := p.Err(); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to run ovn get: %w", err)), nil + } + + server, err := newOVNServer(kubernetes.NewCore(params), containerForDatabase(database)) + if err != nil { + return api.NewToolCallResult("", err), nil + } + _, result, err := server.Get(params.Context, nil, ovntypes.GetParams{ + NamespacedNameParams: namespacedName(namespace, name), + Database: ovntypes.Database(database), + Table: table, + Record: record, + Columns: columns, + PatternParams: pattern.PatternParams{Pattern: pat}, + HeadTailParams: headtail.HeadTailParams{Head: head, Tail: tail, ApplyTailFirst: applyTailFirst}, + }) + if err != nil { + return api.NewToolCallResult("", err), nil + } + return api.NewToolCallResultStructured(result, nil), nil +} + +func ovnLFlowList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + namespace := p.OptionalString("namespace", "openshift-ovn-kubernetes") + name := p.RequiredString("name") + datapath := p.OptionalString("datapath", "") + pat := p.OptionalString("pattern", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + if err := p.Err(); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to run ovn lflow-list: %w", err)), nil + } + + server, err := newOVNServer(kubernetes.NewCore(params), "sbdb") + if err != nil { + return api.NewToolCallResult("", err), nil + } + _, result, err := server.ListLogicalFlows(params.Context, nil, ovntypes.LogicalFlowListParams{ + NamespacedNameParams: namespacedName(namespace, name), + Datapath: datapath, + PatternParams: pattern.PatternParams{Pattern: pat}, + HeadTailParams: headtail.HeadTailParams{Head: head, Tail: tail, ApplyTailFirst: applyTailFirst}, + }) + if err != nil { + return api.NewToolCallResult("", err), nil + } + return api.NewToolCallResultStructured(result, nil), nil +} + +func ovnTrace(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + p := api.WrapParams(params) + namespace := p.OptionalString("namespace", "openshift-ovn-kubernetes") + name := p.RequiredString("name") + datapath := p.RequiredString("datapath") + microflow := p.RequiredString("microflow") + mode := p.OptionalString("mode", "") + pat := p.OptionalString("pattern", "") + head := int(p.OptionalInt64("head", 0)) + tail := int(p.OptionalInt64("tail", 0)) + applyTailFirst := p.OptionalBool("apply_tail_first", false) + if err := p.Err(); err != nil { + return api.NewToolCallResult("", fmt.Errorf("failed to run ovn trace: %w", err)), nil + } + + server, err := newOVNServer(kubernetes.NewCore(params), "northd") + if err != nil { + return api.NewToolCallResult("", err), nil + } + _, result, err := server.Trace(params.Context, nil, ovntypes.OVNTraceParams{ + NamespacedNameParams: namespacedName(namespace, name), + Datapath: datapath, + Microflow: microflow, + Mode: ovntypes.TraceMode(mode), + PatternParams: pattern.PatternParams{Pattern: pat}, + HeadTailParams: headtail.HeadTailParams{Head: head, Tail: tail, ApplyTailFirst: applyTailFirst}, + }) + if err != nil { + return api.NewToolCallResult("", err), nil + } + return api.NewToolCallResultStructured(result, nil), nil +} + +// newOVNServer creates an ovnmcp.MCPServer that executes OVN CLI commands +// inside the given container of an OVN pod. +func newOVNServer(core *kubernetes.Core, container string) (*ovnmcp.MCPServer, error) { + return ovnmcp.NewMCPServer(func(ctx context.Context, namespace, name, _ string, command []string) (string, string, error) { + return core.PodsExec(ctx, namespace, name, container, command) + }) +} + +// containerForDatabase maps the database parameter to the container that has +// the corresponding OVN CLI tool. On OpenShift, nbdb and sbdb are separate +// containers in the ovnkube-node pod. +func containerForDatabase(database string) string { + if database == string(ovntypes.SouthboundDB) { + return "sbdb" + } + return "nbdb" +} + +func namespacedName(namespace, name string) k8stypes.NamespacedNameParams { + return k8stypes.NamespacedNameParams{Namespace: namespace, Name: name} +} diff --git a/pkg/toolsets/ovnkubernetes/toolset.go b/pkg/toolsets/ovnkubernetes/toolset.go new file mode 100644 index 000000000..2bb564ccf --- /dev/null +++ b/pkg/toolsets/ovnkubernetes/toolset.go @@ -0,0 +1,39 @@ +package ovnkubernetes + +import ( + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets/ovnkubernetes/ovn" +) + +type Toolset struct{} + +var _ api.Toolset = (*Toolset)(nil) + +func (t *Toolset) GetName() string { + return "ovn-kubernetes" +} + +func (t *Toolset) GetDescription() string { + return "OVN-Kubernetes CNI network troubleshooting tools" +} + +func (t *Toolset) GetTools(_ api.FilteringProvider) []api.ServerTool { + return ovn.InitOVNTools() +} + +func (t *Toolset) GetPrompts() []api.ServerPrompt { + return nil +} + +func (t *Toolset) GetResources() []api.ServerResource { + return nil +} + +func (t *Toolset) GetResourceTemplates() []api.ServerResourceTemplate { + return nil +} + +func init() { + toolsets.Register(&Toolset{}) +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/LICENSE b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/common.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/common.go new file mode 100644 index 000000000..b36683911 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/common.go @@ -0,0 +1,137 @@ +package types + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/util/jsonpath" + yaml "sigs.k8s.io/yaml" +) + +// FormattedOutput is a type that contains the formatted data of a resource. +type FormattedOutput struct { + Data string `json:"data"` +} + +// ToJSON gets the JSON data from a resource. +func (j *FormattedOutput) ToJSON(data any) error { + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + j.Data = string(jsonData) + return nil +} + +// ToJSONPath gets the JSONPath data from a resource. +func (j *FormattedOutput) ToJSONPath(template string, data map[string]any) error { + jp := jsonpath.New("jsonpath") + if err := jp.Parse(template); err != nil { + return err + } + dataBuffer := bytes.NewBuffer(nil) + err := jp.Execute(dataBuffer, data) + if err != nil { + return fmt.Errorf("failed to execute jsonpath template %s, error: %w", template, err) + } + j.Data = dataBuffer.String() + return nil +} + +// ToYAML gets the YAML data from a resource. +func (j *FormattedOutput) ToYAML(data any) error { + yamlData, err := yaml.Marshal(data) + if err != nil { + return err + } + j.Data = string(yamlData) + return nil +} + +// NamespacedNameParams is a type that contains the name and namespace of a resource. +type NamespacedNameParams struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +// NamespacedNameResult is a type that contains the name and namespace of a resource. +// The fields are optional. +type NamespacedNameResult struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// Resource is a type that contains the name, namespace, age, labels and annotations of a resource. +type Resource struct { + NamespacedNameResult + Age string `json:"age,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + FormattedOutput +} + +// GetResourceData gets the data of a resource. If isDetailed is true, the labels and annotations are also included. +func (r *Resource) GetResourceData(resource *unstructured.Unstructured, isDetailed bool) { + r.Name = resource.GetName() + r.Namespace = resource.GetNamespace() + r.Age = FormatAge(time.Since(resource.GetCreationTimestamp().Time)) + + if isDetailed { + r.Labels = resource.GetLabels() + r.Annotations = resource.GetAnnotations() + } +} + +// GroupVersionKind is a type that contains the group, version and kind of a resource. +type GroupVersionKind struct { + Group string `json:"group,omitempty"` + Version string `json:"version"` + Kind string `json:"kind"` +} + +// OutputType is a type that contains the output type of a resource. +type OutputType string + +const ( + // YAMLOutputType is the output type for yaml data. + YAMLOutputType OutputType = "yaml" + // JSONOutputType is the output type for json data. + JSONOutputType OutputType = "json" + // JSONPathOutputType is the output type for jsonpath data. + JSONPathOutputType OutputType = "jsonpath" + // WideOutputType is the output type for detailed data. + WideOutputType OutputType = "wide" +) + +// OutputParams is a type that contains the output type and JSONPathTemplate of a resource. +type OutputParams struct { + // OutputType is the output type of the resource. If set, it can be yaml, json, jsonpath or wide. + // For jsonpath, the template should be provided as part of the output type. + // For example, output_type="jsonpath='{.metadata.name}'". + OutputType OutputType `json:"output_type,omitempty"` +} + +// ValidateOutputParams validates the output parameters. +func (o *OutputParams) ValidateOutputParams() error { + if o.OutputType != "" && o.OutputType != YAMLOutputType && o.OutputType != JSONOutputType && + o.OutputType != WideOutputType && !strings.HasPrefix(string(o.OutputType), string(JSONPathOutputType)+"=") { + return fmt.Errorf("invalid output_type: %s", o.OutputType) + } + if jsonPathTemplate, found := strings.CutPrefix(string(o.OutputType), string(JSONPathOutputType)+"="); found { + err := jsonpath.NewParser("validate").Parse(jsonPathTemplate) + if err != nil { + return fmt.Errorf("invalid json_path_template: %s, error: %w", jsonPathTemplate, err) + } + } + return nil +} + +// GetParams is a type that contains the name, namespace and output type of a resource. +type GetParams struct { + NamespacedNameParams + OutputParams +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/nodes.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/nodes.go new file mode 100644 index 000000000..274d39394 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/nodes.go @@ -0,0 +1,20 @@ +package types + +import "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout" + +// DebugNodeParams is a type that contains the namespace, name, image, command, host path, mount path +// of a node along with timeout parameters to be used for the command execution. +type DebugNodeParams struct { + NamespacedNameParams + Image string `json:"image"` + Command []string `json:"command"` + HostPath string `json:"host_path,omitempty"` + MountPath string `json:"mount_path,omitempty"` + timeout.TimeoutParams +} + +// DebugNodeResult is a type that contains the stdout and stderr of the executed command. +type DebugNodeResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/pods.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/pods.go new file mode 100644 index 000000000..6231569f6 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/pods.go @@ -0,0 +1,34 @@ +package types + +import ( + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern" +) + +// GetPodLogsParams is a type that contains the name, namespace and container of a pod. +type GetPodLogsParams struct { + NamespacedNameParams + Container string `json:"container,omitempty"` + Previous bool `json:"previous,omitempty"` + pattern.PatternParams + headtail.HeadTailParams +} + +// GetPodLogsResult is a type that contains the logs of a pod where each log line +// is a separate element in the string slice. +type GetPodLogsResult struct { + Logs []string `json:"logs"` +} + +// ExecPodParams is a type that contains the name, namespace and container of a pod. +type ExecPodParams struct { + NamespacedNameParams + Container string `json:"container,omitempty"` + Command []string `json:"command"` +} + +// ExecPodResult is a type that contains the stdout and stderr of the executed command. +type ExecPodResult struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/resources.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/resources.go new file mode 100644 index 000000000..531275254 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/resources.go @@ -0,0 +1,30 @@ +package types + +// GetResourceParams is a type that contains the group, version, kind, name and namespace of a resource. +type GetResourceParams struct { + GroupVersionKind + GetParams +} + +// GetResourceResult is a type that contains the resource data. +type GetResourceResult struct { + Resource Resource `json:"resource"` +} + +// ListParams is a type that contains the namespace, label selector and output type of a resource. +type ListParams struct { + Namespace string `json:"namespace,omitempty"` + LabelSelector string `json:"label_selector,omitempty"` + OutputParams +} + +// ListResourcesParams is a type that contains the group, version, kind, namespace and output type of a resource. +type ListResourcesParams struct { + GroupVersionKind + ListParams +} + +// ListResourcesResult is a type that contains the resource data. +type ListResourcesResult struct { + Resources []Resource `json:"resources"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/utils.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/utils.go new file mode 100644 index 000000000..3991eea2c --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types/utils.go @@ -0,0 +1,19 @@ +package types + +import ( + "fmt" + "time" +) + +// FormatAge formats the age of a resource in a human readable format. +func FormatAge(age time.Duration) string { + if age < time.Minute { + return fmt.Sprintf("%ds", int64(age.Seconds())) + } else if age < time.Hour { + return fmt.Sprintf("%dm%ds", int64(age.Minutes()), int64(age.Seconds()-float64(int64(age.Minutes())*60))) + } else if age < time.Hour*24 { + return fmt.Sprintf("%dh%dm", int64(age.Hours()), int64(age.Minutes()-float64(int64(age.Hours())*60))) + } else { + return fmt.Sprintf("%dd%dh", int64(age.Hours()/24), int64(age.Hours()-float64(int64(age.Hours()/24))*24)) + } +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/commands.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/commands.go new file mode 100644 index 000000000..ce3d1cf30 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/commands.go @@ -0,0 +1,50 @@ +package mcp + +import ( + "fmt" + + ovntypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" +) + +// DefaultMaxLines defines the maximum number of lines to return from command output +const DefaultMaxLines = 100 + +// validateDatabase validates that the database is a valid OVN database. +func validateDatabase(db ovntypes.Database) error { + switch db { + case ovntypes.NorthboundDB, ovntypes.SouthboundDB: + return nil + default: + return fmt.Errorf("invalid database %q: must be 'nbdb' or 'sbdb'", db) + } +} + +// validateRecordName validates that a record identifier is safe and non-empty. +func validateRecordName(record string) error { + return utils.ValidateSafeString(record, "record identifier", false, utils.ShellMetaCharactersTypeDefault) +} + +// validateDatapath validates that a datapath name is safe and non-empty. +func validateDatapath(datapath string) error { + return utils.ValidateSafeString(datapath, "datapath name", false, utils.ShellMetaCharactersTypeDefault) +} + +// validateMicroflow validates that a microflow specification is safe and non-empty. +// Microflow specs can contain && for logical AND, so we allow & but block other dangerous chars. +func validateMicroflow(microflow string) error { + return utils.ValidateSafeString(microflow, "microflow specification", false, utils.ShellMetaCharactersTypeAllowBracketsAllowAmp) +} + +// validateColumnSpec validates that a column specification is safe (can be empty). +func validateColumnSpec(columns string) error { + return utils.ValidateSafeString(columns, "column specification", true, utils.ShellMetaCharactersTypeDefault) +} + +// getDBCommand returns the appropriate command (ovn-nbctl or ovn-sbctl) for the given database. +func getDBCommand(db ovntypes.Database) string { + if db == ovntypes.SouthboundDB { + return "ovn-sbctl" + } + return "ovn-nbctl" +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/mcp.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/mcp.go new file mode 100644 index 000000000..17561c4a6 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp/mcp.go @@ -0,0 +1,385 @@ +package mcp + +import ( + "context" + "fmt" + "strings" + + "github.com/modelcontextprotocol/go-sdk/mcp" + ovntypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb" +) + +type RunPodExecCommandFuncType func(ctx context.Context, namespace, name, container string, command []string) (string, string, error) + +// MCPServer provides OVN layer analysis tools +type MCPServer struct { + runPodExecCommand RunPodExecCommandFuncType +} + +// NewMCPServer creates a new OVN MCP server +func NewMCPServer(runPodExecCommand RunPodExecCommandFuncType) (*MCPServer, error) { + if runPodExecCommand == nil { + return nil, fmt.Errorf("function to run pod exec command is nil") + } + return &MCPServer{ + runPodExecCommand: runPodExecCommand, + }, nil +} + +// AddTools registers OVN tools with the MCP server +func (s *MCPServer) AddTools(server *mcp.Server) { + mcp.AddTool(server, + &mcp.Tool{ + Name: "ovn-show", + Description: fmt.Sprintf(`Display a comprehensive overview of OVN configuration from either the Northbound or Southbound database. + +For Northbound (nbdb): Runs 'ovn-nbctl show' and displays logical switches, logical routers, +their ports, and connections between them. + +For Southbound (sbdb): Runs 'ovn-sbctl show' and displays chassis information, port bindings, +and their relationships. + +Parameters: +- namespace: Kubernetes namespace of the OVN pod (e.g., "ovn-kubernetes") +- name: Name of the pod running OVN (e.g., "ovnkube-node-xxxxx") +- database: OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false + +Example output for nbdb: +{ + "database": "nbdb", + "output": "switch 1234-5678 (node1)\n port node1-k8s\n addresses: [\"00:00:00:00:00:01\"]\n..." +}`, DefaultMaxLines), + }, s.Show) + + mcp.AddTool(server, + &mcp.Tool{ + Name: "ovn-get", + Description: fmt.Sprintf(`Query records from an OVN database table with flexible filtering. + +This is a versatile command that can: +1. List all records in a table (when no record specified) +2. Get a specific record (when record specified) + +Common Northbound tables: Logical_Switch, Logical_Router, Logical_Switch_Port, +Logical_Router_Port, ACL, Address_Set, Port_Group, Load_Balancer, NAT + +Common Southbound tables: Chassis, Port_Binding, Datapath_Binding, Logical_Flow, +MAC_Binding, Multicast_Group, SB_Global + +Parameters: +- namespace: Kubernetes namespace of the OVN pod +- name: Name of the pod running OVN +- database: OVN database to query - "nbdb" for Northbound or "sbdb" for Southbound +- table: Name of the table (e.g., "Logical_Switch", "Port_Binding") +- record (optional): Record identifier (UUID or name). If not specified, lists all records +- columns (optional): Comma-separated list of columns to display (e.g., "name,_uuid,ports") +- pattern (optional): Regex pattern to filter results. Only applies when listing all records. +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false + +Example listing all records: +{ + "database": "nbdb", + "table": "Port_Group", + "output": "_uuid: 1234-5678\nname: \"pg_default\"\nports: [...]\n\n_uuid: abcd-efgh\n..." +} + +Example getting a specific record: +{ + "database": "nbdb", + "table": "Logical_Router", + "record": "ovn_cluster_router", + "output": "_uuid: 4c4a0a35-348c-41cc-8417-53a618e0c383\nname: ovn_cluster_router\nports: [...]" +} + +Example getting specific columns: +{ + "database": "nbdb", + "table": "Logical_Switch", + "columns": "name,ports", + "output": "name: ovn-worker\nports: [uuid1, uuid2]\n\nname: join\nports: [uuid3]" +}`, DefaultMaxLines), + }, s.Get) + + mcp.AddTool(server, + &mcp.Tool{ + Name: "ovn-lflow-list", + Description: fmt.Sprintf(`List logical flows from the OVN Southbound database. + +Runs 'ovn-sbctl lflow-list' to retrieve logical flows which represent the compiled +logical network pipeline. This is essential for debugging packet forwarding. + +Parameters: +- namespace: Kubernetes namespace of the OVN pod +- name: Name of the pod running OVN +- datapath (optional): Datapath name or UUID to filter flows for a specific logical switch/router +- pattern (optional): Regex pattern to filter flows +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false + +Example output: +{ + "datapath": "node1", + "flows": [ + "table=0 (ls_in_port_sec_l2), priority=100, match=(inport == \"pod1\"), action=(next;)", + "table=1 (ls_in_port_sec_ip), priority=90, match=(ip4), action=(next;)" + ] +}`, DefaultMaxLines), + }, s.ListLogicalFlows) + + mcp.AddTool(server, + &mcp.Tool{ + Name: "ovn-trace", + Description: fmt.Sprintf(`Trace a packet through the OVN logical network. + +Runs 'ovn-trace' to simulate packet processing through the logical network pipeline. +This shows which logical flows match, what actions are taken, and the final disposition. + +The trace is essential for debugging connectivity issues and understanding how traffic +flows through the OVN logical network. + +Parameters: +- namespace: Kubernetes namespace of the OVN pod +- name: Name of the pod running OVN +- datapath: Name of the logical switch or router to start the trace +- microflow: Microflow specification describing the packet (e.g., "inport==\"pod1\" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5") +- mode (optional): Output verbosity mode - "detailed" (default), "summary", or "minimal" +- pattern (optional): Regex pattern to filter trace output +- head (optional): Return only first N lines. Default: %d lines if tail is not specified +- tail (optional): Return only last N lines +- apply_tail_first (optional): If both head and tail are set and apply_tail_first is true, +apply tail before head. Default: false + +Microflow specification examples: +- inport=="pod1" && eth.src==00:00:00:00:00:01 && ip4.src==10.244.0.5 && ip4.dst==10.244.1.5 +- inport=="pod1" && eth.src==00:00:00:00:00:01 && icmp && ip4.src==10.244.0.5 && ip4.dst==8.8.8.8 + +Example output: +{ + "datapath": "node1", + "microflow": "inport==\"pod1\" && ...", + "output": "ingress(dp=\"node1\", inport=\"pod1\")\n 0. ls_in_port_sec_l2: inport == \"pod1\", priority 50, uuid 1234\n next;\n..." +}`, DefaultMaxLines), + }, s.Trace) +} + +// Show displays a comprehensive overview of OVN configuration. +func (s *MCPServer) Show(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.ShowParams) (*mcp.CallToolResult, ovntypes.ShowResult, error) { + result := ovntypes.ShowResult{ + Database: in.Database, + } + + // Validate database + if err := validateDatabase(in.Database); err != nil { + return nil, result, err + } + + // Build command + cmd := getDBCommand(in.Database) + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", []string{cmd, "show"}) + if err != nil { + return nil, result, fmt.Errorf("failed to retrieve OVN configuration from pod %s/%s: %w", + in.Namespace, in.Name, err) + } + if stderr != "" { + return nil, result, fmt.Errorf("failed to retrieve OVN configuration from pod %s/%s: %s", + in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxLines) + + // Join all lines into a single output string + result.Output = strings.Join(lines, "\n") + return nil, result, nil +} + +// Get queries records from an OVN table with flexible filtering. +// Supports two modes: +// 1. List all records (when Record is empty) +// 2. Get specific record (when Record is set) +// Both modes support filtering columns with the Columns parameter. +func (s *MCPServer) Get(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.GetParams) (*mcp.CallToolResult, ovntypes.GetResult, error) { + result := ovntypes.GetResult{ + Database: in.Database, + Table: in.Table, + Record: in.Record, + } + + // Validate inputs + if err := validateDatabase(in.Database); err != nil { + return nil, result, err + } + if err := ovndb.ValidateOVNTableName(in.Table); err != nil { + return nil, result, err + } + if err := validateColumnSpec(in.Columns); err != nil { + return nil, result, err + } + + cmd := getDBCommand(in.Database) + cmdArgs := []string{cmd} + + // Add columns filter if specified + if in.Columns != "" { + cmdArgs = append(cmdArgs, "--columns="+in.Columns) + } + + if in.Record == "" { + // Mode 1: List all records in the table + cmdArgs = append(cmdArgs, "list", in.Table) + } else { + // Mode 2: Get specific record + if err := validateRecordName(in.Record); err != nil { + return nil, result, err + } + cmdArgs = append(cmdArgs, "list", in.Table, in.Record) + } + + // Match the pattern to the get results if in list mode + lines, err := in.PatternParams.ExecuteWithMatch(func() ([]string, error) { + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", cmdArgs) + if err != nil { + if in.Record != "" { + return nil, fmt.Errorf("failed to get record %s from table %s on pod %s/%s: %w", + in.Record, in.Table, in.Namespace, in.Name, err) + } + return nil, fmt.Errorf("failed to list table %s from pod %s/%s: %w", + in.Table, in.Namespace, in.Name, err) + } + if stderr != "" { + if in.Record != "" { + return nil, fmt.Errorf("failed to get record %s from table %s on pod %s/%s: %s", + in.Record, in.Table, in.Namespace, in.Name, stderr) + } + return nil, fmt.Errorf("failed to list table %s from pod %s/%s: %s", + in.Table, in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + return lines, nil + }, in.Record == "") + if err != nil { + return nil, result, err + } + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxLines) + + result.Output = strings.Join(lines, "\n") + return nil, result, nil +} + +// ListLogicalFlows lists logical flows from the Southbound database. +func (s *MCPServer) ListLogicalFlows(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.LogicalFlowListParams) (*mcp.CallToolResult, ovntypes.LogicalFlowListResult, error) { + result := ovntypes.LogicalFlowListResult{ + Datapath: in.Datapath, + Flows: []string{}, + } + + // Validate datapath if provided + if in.Datapath != "" { + if err := validateDatapath(in.Datapath); err != nil { + return nil, result, err + } + } + + // Build command + cmdArgs := []string{"ovn-sbctl", "lflow-list"} + if in.Datapath != "" { + cmdArgs = append(cmdArgs, in.Datapath) + } + + // Match the pattern to the logical flows + lines, err := in.PatternParams.ExecuteWithMatch(func() ([]string, error) { + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", cmdArgs) + if err != nil { + return nil, fmt.Errorf("failed to list logical flows from pod %s/%s: %w", + in.Namespace, in.Name, err) + } + if stderr != "" { + return nil, fmt.Errorf("failed to list logical flows from pod %s/%s: %s", + in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + return lines, nil + }, true) + if err != nil { + return nil, result, err + } + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxLines) + + result.Flows = lines + return nil, result, nil +} + +// Trace traces a packet through the OVN logical network. +func (s *MCPServer) Trace(ctx context.Context, req *mcp.CallToolRequest, + in ovntypes.OVNTraceParams) (*mcp.CallToolResult, ovntypes.OVNTraceResult, error) { + result := ovntypes.OVNTraceResult{ + Datapath: in.Datapath, + Microflow: in.Microflow, + } + + // Validate inputs + if err := validateDatapath(in.Datapath); err != nil { + return nil, result, err + } + if err := validateMicroflow(in.Microflow); err != nil { + return nil, result, err + } + + // Build command: ovn-trace '' + cmdArgs := []string{"ovn-trace"} + + // Add output format flag based on mode (default to detailed) + switch in.Mode { + case ovntypes.TraceModeSummary: + cmdArgs = append(cmdArgs, "--summary") + case ovntypes.TraceModeMinimal: + cmdArgs = append(cmdArgs, "--minimal") + case ovntypes.TraceModeDetailed, "": + cmdArgs = append(cmdArgs, "--detailed") + } + + cmdArgs = append(cmdArgs, in.Datapath, in.Microflow) + + // Match the pattern to the trace output + lines, err := in.PatternParams.ExecuteWithMatch(func() ([]string, error) { + stdout, stderr, err := s.runPodExecCommand(ctx, in.Namespace, in.Name, "", cmdArgs) + if err != nil { + return nil, fmt.Errorf("failed to trace packet on pod %s/%s: %w", + in.Namespace, in.Name, err) + } + if stderr != "" { + return nil, fmt.Errorf("failed to trace packet on pod %s/%s: %s", + in.Namespace, in.Name, stderr) + } + lines := utils.StripEmptyLines(strings.Split(stdout, "\n")) + return lines, nil + }, true) + if err != nil { + return nil, result, err + } + + // Apply the head and tail parameters to the lines + lines = in.HeadTailParams.Apply(lines, DefaultMaxLines) + + result.Output = strings.Join(lines, "\n") + return nil, result, nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types/command.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types/command.go new file mode 100644 index 000000000..c9329a708 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types/command.go @@ -0,0 +1,96 @@ +package types + +import ( + k8stypes "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail" + "github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern" +) + +// Database represents an OVN database type. +type Database string + +const ( + // NorthboundDB is the OVN Northbound database. + NorthboundDB Database = "nbdb" + // SouthboundDB is the OVN Southbound database. + SouthboundDB Database = "sbdb" +) + +// ShowParams are the parameters for ovn-nbctl/ovn-sbctl show command. +type ShowParams struct { + k8stypes.NamespacedNameParams + Database Database `json:"database"` + headtail.HeadTailParams +} + +// ShowResult contains the output of ovn-nbctl/ovn-sbctl show command. +type ShowResult struct { + Database Database `json:"database"` + Output string `json:"output"` +} + +// LogicalFlowListParams are the parameters for listing logical flows from SBDB. +type LogicalFlowListParams struct { + k8stypes.NamespacedNameParams + Datapath string `json:"datapath,omitempty"` + pattern.PatternParams + headtail.HeadTailParams +} + +// LogicalFlowListResult contains the list of logical flows. +type LogicalFlowListResult struct { + Datapath string `json:"datapath,omitempty"` + Flows []string `json:"flows"` +} + +// TraceMode represents the output verbosity mode for ovn-trace. +type TraceMode string + +const ( + // TraceModeDetailed shows detailed trace output (default). + TraceModeDetailed TraceMode = "detailed" + // TraceModeSummary shows summary output only. + TraceModeSummary TraceMode = "summary" + // TraceModeMinimal shows minimal output. + TraceModeMinimal TraceMode = "minimal" +) + +// OVNTraceParams are the parameters for ovn-trace command. +type OVNTraceParams struct { + k8stypes.NamespacedNameParams + Datapath string `json:"datapath"` + Microflow string `json:"microflow"` + Mode TraceMode `json:"mode,omitempty"` // Output mode: detailed (default), summary, or minimal + pattern.PatternParams + headtail.HeadTailParams +} + +// OVNTraceResult contains the output of ovn-trace command. +type OVNTraceResult struct { + Datapath string `json:"datapath"` + Microflow string `json:"microflow"` + Output string `json:"output"` +} + +// GetParams are the parameters for querying records from an OVN table. +// This is a flexible command that supports: +// - Listing all records (when Record is empty) +// - Getting a specific record (when Record is set) +// - Getting specific columns (when Columns is set) +type GetParams struct { + k8stypes.NamespacedNameParams + Database Database `json:"database"` + Table string `json:"table"` + Record string `json:"record,omitempty"` // Optional: if empty, lists all records + Columns string `json:"columns,omitempty"` // Optional: comma-separated columns to retrieve + pattern.PatternParams + headtail.HeadTailParams +} + +// GetResult contains the output of ovn-nbctl/ovn-sbctl query. +type GetResult struct { + Database Database `json:"database"` + Table string `json:"table"` + Record string `json:"record,omitempty"` + Output string `json:"output"` +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail/head_tail.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail/head_tail.go new file mode 100644 index 000000000..4cc821a93 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail/head_tail.go @@ -0,0 +1,61 @@ +package headtail + +// HeadTailParams is a type that contains the head and tail parameters. +type HeadTailParams struct { + Head int `json:"head,omitempty"` + Tail int `json:"tail,omitempty"` + ApplyTailFirst bool `json:"apply_tail_first,omitempty"` +} + +// Apply applies the head and tail parameters to the lines. If none +// are set, then only head is applied and the default maximum +// number of lines is returned. If both Head and Tail are set, and +// ApplyTailFirst is true, tail will be applied first, otherwise +// head will be applied first. If only one of Head or Tail is set, +// that one will be applied and ApplyTailFirst will be ignored. +func (h *HeadTailParams) Apply(lines []string, defaultMaxLines int) []string { + // If neither Head nor Tail is set, return the default maximum number of lines. + if h.Head == 0 && h.Tail == 0 { + return head(lines, defaultMaxLines) + } + // If both Head and Tail are set, apply them in the order specified by ApplyTailFirst. + if h.Head != 0 && h.Tail != 0 { + if h.ApplyTailFirst { + return head(tail(lines, h.Tail), h.Head) + } else { + return tail(head(lines, h.Head), h.Tail) + } + } + // If only Head is set, apply it. + if h.Head != 0 { + return head(lines, h.Head) + } + // If only Tail is set, apply it. + return tail(lines, h.Tail) +} + +// head returns the first n lines of a slice of strings. It will return a new slice of strings +// with the first n lines. If n is less than or equal to 0, or greater than or equal to the +// length of the slice, it will return the entire slice. +func head(lines []string, n int) []string { + if len(lines) == 0 { + return lines + } + if n <= 0 || n >= len(lines) { + return lines + } + return lines[:n] +} + +// tail returns the last n lines of a slice of strings. It will return a new slice of strings +// with the last n lines. If n is less than or equal to 0, or greater than or equal to the +// length of the slice, it will return the entire slice. +func tail(lines []string, n int) []string { + if len(lines) == 0 { + return lines + } + if n <= 0 || n >= len(lines) { + return lines + } + return lines[len(lines)-n:] +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb/ovn_db.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb/ovn_db.go new file mode 100644 index 000000000..edc1d3ec5 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb/ovn_db.go @@ -0,0 +1,24 @@ +package ovndb + +import ( + "fmt" + "regexp" +) + +// validOVNTableNamePattern matches valid OVN table names: start with letter, alphanumeric +// and underscores. +var validOVNTableNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`) + +// ValidateOVNTableName validates that a table name is safe and non-empty. +// Table names should only contain alphanumeric characters and underscores. +func ValidateOVNTableName(table string) error { + if table == "" { + return fmt.Errorf("table name cannot be empty") + } + + if !validOVNTableNamePattern.MatchString(table) { + return fmt.Errorf("invalid table name %q: must start with a letter and contain only alphanumeric characters and underscores", table) + } + + return nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern/pattern.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern/pattern.go new file mode 100644 index 000000000..af84ff9d0 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern/pattern.go @@ -0,0 +1,45 @@ +package pattern + +import ( + "fmt" + "regexp" +) + +type PatternParams struct { + Pattern string `json:"pattern,omitempty"` +} + +// ExecuteWithMatch executes a function and matches the output to the pattern. +// If checkMatch is false, a non-empty Pattern is rejected with an error because +// pattern matching is not supported in that mode; an empty Pattern returns f() +// unchanged. If checkMatch is true, it returns f() when Pattern is empty, or the +// regex-matched subset of f()'s lines when Pattern is set. An error is returned +// if the pattern is invalid or if f() fails. +func (p *PatternParams) ExecuteWithMatch(f func() ([]string, error), checkMatch bool) ([]string, error) { + if !checkMatch { + if p.Pattern != "" { + return nil, fmt.Errorf("pattern matching is not supported in this mode (got pattern %q)", p.Pattern) + } + return f() + } + if p.Pattern == "" { + return f() + } + searchPattern, err := regexp.Compile(p.Pattern) + if err != nil { + return nil, fmt.Errorf("invalid search pattern %q: %w", p.Pattern, err) + } + + lines, err := f() + if err != nil { + return nil, err + } + + matchedLines := []string{} + for _, line := range lines { + if searchPattern.MatchString(line) { + matchedLines = append(matchedLines, line) + } + } + return matchedLines, nil +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout/timeout.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout/timeout.go new file mode 100644 index 000000000..f8ea72967 --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout/timeout.go @@ -0,0 +1,33 @@ +package timeout + +import ( + "context" + "time" +) + +// MaxTimeout is the maximum timeout in seconds for the command execution. +const MaxTimeout = 300 * time.Second + +// TimeoutParams is a type that contains the timeout seconds for the command execution. +type TimeoutParams struct { + // TimeoutSeconds is the timeout in seconds for the command execution. + // If not specified, server default timeout is used. The maximum value + // is MaxTimeout. + TimeoutSeconds uint32 `json:"timeout_seconds,omitempty"` +} + +// ToDuration converts the timeout seconds to a duration. The maximum value +// is MaxTimeout. +func (t *TimeoutParams) ToDuration() time.Duration { + duration := time.Duration(t.TimeoutSeconds) * time.Second + return min(duration, MaxTimeout) +} + +// WithTimeout returns a context with a timeout set to the timeout seconds. If the timeout is 0, +// the original context is returned and the cancel function is nil. +func (t *TimeoutParams) WithTimeout(ctx context.Context) (context.Context, context.CancelFunc) { + if t.TimeoutSeconds == 0 { + return ctx, nil + } + return context.WithTimeout(ctx, t.ToDuration()) +} diff --git a/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/utils.go b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/utils.go new file mode 100644 index 000000000..78ad19e5c --- /dev/null +++ b/vendor/github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/utils.go @@ -0,0 +1,162 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "unicode/utf8" +) + +var ( + // shellMetaCharacters is the pattern for shell metacharacters. + shellMetaCharacters = regexp.MustCompile(`[;&|$` + "`" + `<>\\()]`) + + // shellMetaCharactersNoBrackets is like shellMetaCharacters but allows brackets. + shellMetaCharactersNoBrackets = regexp.MustCompile(`[;&|$` + "`" + `<>\\]`) + + // shellMetaCharactersNoBracketsNoAmp is like shellMetaCharactersNoBrackets but allows & for + // commands which use && for logical AND operations. + shellMetaCharactersNoBracketsNoAmp = regexp.MustCompile(`[;|$` + "`" + `<>\\]`) + + // shellMetaCharactersNoBracketsSpecialCharacters is like shellMetaCharactersNoBrackets but also + // disallows newline, NUL byte, and single quote ('). + shellMetaCharactersNoBracketsSpecialCharacters = regexp.MustCompile(`[;&|$` + "`" + `\n\x00` + `'<>\\]`) + + // pathUnsafeChar matches the first rune not allowed in a mount path (alphanumeric, /, -, _, ., ~). + pathUnsafeChar = regexp.MustCompile(`[^a-zA-Z0-9/_.~-]`) +) + +// ShellMetaCharactersType is the type of shell metacharacters to validate. +type ShellMetaCharactersType string + +// ShellMetaCharactersType values. +const ( + ShellMetaCharactersTypeDefault ShellMetaCharactersType = "default" + ShellMetaCharactersTypeAllowBrackets ShellMetaCharactersType = "allow_brackets" + ShellMetaCharactersTypeAllowBracketsAllowAmp ShellMetaCharactersType = "allow_brackets_and_amp" + ShellMetaCharactersTypeDisallowSpecialCharacters ShellMetaCharactersType = "disallow_special_characters" +) + +// StripEmptyLines strips empty lines from a slice of strings. It will return a new slice of strings +// with the empty lines removed. +func StripEmptyLines(lines []string) []string { + if len(lines) == 0 { + return lines + } + result := []string{} + for _, line := range lines { + if strings.TrimSpace(line) != "" { + result = append(result, line) + } + } + return result +} + +// GetGitRepositoryRoot returns the root directory of the git repository. It will return an error +// if the current directory is not a git repository or the root directory cannot be found. +func GetGitRepositoryRoot() (string, error) { + currentDir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current directory: %w", err) + } + + for { + gitDirPath := filepath.Join(currentDir, ".git") + if _, err := os.Stat(gitDirPath); err == nil { + return currentDir, nil // Found .git directory, this is the root + } + + parentDir := filepath.Dir(currentDir) + if parentDir == currentDir { // Reached the file system root + return "", fmt.Errorf("failed to find git repository root") + } + currentDir = parentDir + } +} + +// validateShellMetacharacters validates whether the given parameter contains shell +// metacharacters or not. It returns an error if there are any shell metacharacters. +// If shellMetaCharactersType is ShellMetaCharactersTypeDefault, no shell metacharacters are allowed. +// If shellMetaCharactersType is ShellMetaCharactersTypeAllowBracketsAllowAmp, the ( and ) characters and the & character are allowed. +// If shellMetaCharactersType is ShellMetaCharactersTypeAllowBrackets, the ( and ) characters are allowed. +// If shellMetaCharactersType is ShellMetaCharactersTypeDisallowSpecialCharacters, the new line, null byte, +// and special characters are disallowed. +func validateShellMetacharacters(param string, shellMetaCharactersType ShellMetaCharactersType) error { + switch shellMetaCharactersType { + case ShellMetaCharactersTypeAllowBracketsAllowAmp: + if shellMetaCharactersNoBracketsNoAmp.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + case ShellMetaCharactersTypeAllowBrackets: + if shellMetaCharactersNoBrackets.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + case ShellMetaCharactersTypeDisallowSpecialCharacters: + if shellMetaCharactersNoBracketsSpecialCharacters.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + case ShellMetaCharactersTypeDefault: + if shellMetaCharacters.MatchString(param) { + return fmt.Errorf("invalid use of metacharacters in parameter: %s", param) + } + default: + return fmt.Errorf("invalid shell metacharacters type: %s", shellMetaCharactersType) + } + + return nil +} + +// ValidateSafeString is same as validateShellMetacharacters but it also checks if the string is empty. +// If allowEmpty is true, an empty string is allowed and no error is returned. +func ValidateSafeString(value, fieldName string, allowEmpty bool, shellMetaCharactersType ShellMetaCharactersType) error { + if value == "" { + if allowEmpty { + return nil + } + return fmt.Errorf("%s cannot be empty", fieldName) + } + + if err := validateShellMetacharacters(value, shellMetaCharactersType); err != nil { + return fmt.Errorf("invalid %s: contains potentially dangerous characters: %w", fieldName, err) + } + return nil +} + +// ValidatePath validates that a path is safe to use as a filesystem path. +// It ensures the path: +// - Is absolute (starts with /) +// - Does not contain path traversal patterns (..) +// - Contains only safe characters +func ValidatePath(path, pathType string, allowEmpty bool) error { + if path == "" { + if allowEmpty { + return nil + } + return fmt.Errorf("%s cannot be empty", pathType) + } + + // Ensure path is absolute + if !filepath.IsAbs(path) { + return fmt.Errorf("%s must be an absolute path (start with /), got: %s", pathType, path) + } + + // Check for path traversal patterns: reject any path element that is exactly ".." + if slices.Contains(strings.Split(path, string(filepath.Separator)), "..") { + return fmt.Errorf("%s contains path traversal element '..': %s", pathType, path) + } + + // Reject null bytes, control characters, shell specials, and other disallowed runes. + if loc := pathUnsafeChar.FindStringIndex(path); loc != nil { + i := loc[0] + r, size := utf8.DecodeRuneInString(path[i:]) + if r == utf8.RuneError && size <= 1 { + return fmt.Errorf("%s contains invalid/unsafe byte at position %d: 0x%02X", pathType, i, path[i]) + } + return fmt.Errorf("%s contains unsafe character at position %d: %c (U+%04X)", pathType, i, r, r) + } + + return nil +} diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/alias.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/alias.go deleted file mode 100644 index fbf256d52..000000000 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/alias.go +++ /dev/null @@ -1,967 +0,0 @@ -// Copyright 2025 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Code generated by alias_gen.go; DO NOT EDIT. - -//go:build goexperiment.jsonv2 && go1.25 - -// Package json implements semantic processing of JSON as specified in RFC 8259. -// JSON is a simple data interchange format that can represent -// primitive data types such as booleans, strings, and numbers, -// in addition to structured data types such as objects and arrays. -// -// [Marshal] and [Unmarshal] encode and decode Go values -// to/from JSON text contained within a []byte. -// [MarshalWrite] and [UnmarshalRead] operate on JSON text -// by writing to or reading from an [io.Writer] or [io.Reader]. -// [MarshalEncode] and [UnmarshalDecode] operate on JSON text -// by encoding to or decoding from a [jsontext.Encoder] or [jsontext.Decoder]. -// [Options] may be passed to each of the marshal or unmarshal functions -// to configure the semantic behavior of marshaling and unmarshaling -// (i.e., alter how JSON data is understood as Go data and vice versa). -// [jsontext.Options] may also be passed to the marshal or unmarshal functions -// to configure the syntactic behavior of encoding or decoding. -// -// The data types of JSON are mapped to/from the data types of Go based on -// the closest logical equivalent between the two type systems. For example, -// a JSON boolean corresponds with a Go bool, -// a JSON string corresponds with a Go string, -// a JSON number corresponds with a Go int, uint or float, -// a JSON array corresponds with a Go slice or array, and -// a JSON object corresponds with a Go struct or map. -// See the documentation on [Marshal] and [Unmarshal] for a comprehensive list -// of how the JSON and Go type systems correspond. -// -// Arbitrary Go types can customize their JSON representation by implementing -// [Marshaler], [MarshalerTo], [Unmarshaler], or [UnmarshalerFrom]. -// This provides authors of Go types with control over how their types are -// serialized as JSON. Alternatively, users can implement functions that match -// [MarshalFunc], [MarshalToFunc], [UnmarshalFunc], or [UnmarshalFromFunc] -// to specify the JSON representation for arbitrary types. -// This provides callers of JSON functionality with control over -// how any arbitrary type is serialized as JSON. -// -// # JSON Representation of Go structs -// -// A Go struct is naturally represented as a JSON object, -// where each Go struct field corresponds with a JSON object member. -// When marshaling, all Go struct fields are recursively encoded in depth-first -// order as JSON object members except those that are ignored or omitted. -// When unmarshaling, JSON object members are recursively decoded -// into the corresponding Go struct fields. -// Object members that do not match any struct fields, -// also known as “unknown members”, are ignored by default or rejected -// if [RejectUnknownMembers] is specified. -// -// The representation of each struct field can be customized in the -// "json" struct field tag, where the tag is a comma separated list of options. -// As a special case, if the entire tag is `json:"-"`, -// then the field is ignored with regard to its JSON representation. -// Some options also have equivalent behavior controlled by a caller-specified [Options]. -// Field-specified options take precedence over caller-specified options. -// -// The first option is the JSON object name override for the Go struct field. -// If the name is not specified, then the Go struct field name -// is used as the JSON object name. JSON names containing commas or quotes, -// or names identical to "" or "-", can be specified using -// a single-quoted string literal, where the syntax is identical to -// the Go grammar for a double-quoted string literal, -// but instead uses single quotes as the delimiters. -// By default, unmarshaling uses case-sensitive matching to identify -// the Go struct field associated with a JSON object name. -// -// After the name, the following tag options are supported: -// -// - omitzero: When marshaling, the "omitzero" option specifies that -// the struct field should be omitted if the field value is zero -// as determined by the "IsZero() bool" method if present, -// otherwise based on whether the field is the zero Go value. -// This option has no effect when unmarshaling. -// -// - omitempty: When marshaling, the "omitempty" option specifies that -// the struct field should be omitted if the field value would have been -// encoded as a JSON null, empty string, empty object, or empty array. -// This option has no effect when unmarshaling. -// -// - string: The "string" option specifies that [StringifyNumbers] -// be set when marshaling or unmarshaling a struct field value. -// This causes numeric types to be encoded as a JSON number -// within a JSON string, and to be decoded from a JSON string -// containing the JSON number without any surrounding whitespace. -// This extra level of encoding is often necessary since -// many JSON parsers cannot precisely represent 64-bit integers. -// -// - case: When unmarshaling, the "case" option specifies how -// JSON object names are matched with the JSON name for Go struct fields. -// The option is a key-value pair specified as "case:value" where -// the value must either be 'ignore' or 'strict'. -// The 'ignore' value specifies that matching is case-insensitive -// where dashes and underscores are also ignored. If multiple fields match, -// the first declared field in breadth-first order takes precedence. -// The 'strict' value specifies that matching is case-sensitive. -// This takes precedence over the [MatchCaseInsensitiveNames] option. -// -// - inline: The "inline" option specifies that -// the JSON representable content of this field type is to be promoted -// as if they were specified in the parent struct. -// It is the JSON equivalent of Go struct embedding. -// A Go embedded field is implicitly inlined unless an explicit JSON name -// is specified. The inlined field must be a Go struct -// (that does not implement any JSON methods), [jsontext.Value], -// map[~string]T, or an unnamed pointer to such types. When marshaling, -// inlined fields from a pointer type are omitted if it is nil. -// Inlined fields of type [jsontext.Value] and map[~string]T are called -// “inlined fallbacks” as they can represent all possible -// JSON object members not directly handled by the parent struct. -// Only one inlined fallback field may be specified in a struct, -// while many non-fallback fields may be specified. This option -// must not be specified with any other option (including the JSON name). -// -// - unknown: The "unknown" option is a specialized variant -// of the inlined fallback to indicate that this Go struct field -// contains any number of unknown JSON object members. The field type must -// be a [jsontext.Value], map[~string]T, or an unnamed pointer to such types. -// If [DiscardUnknownMembers] is specified when marshaling, -// the contents of this field are ignored. -// If [RejectUnknownMembers] is specified when unmarshaling, -// any unknown object members are rejected regardless of whether -// an inlined fallback with the "unknown" option exists. This option -// must not be specified with any other option (including the JSON name). -// -// - format: The "format" option specifies a format flag -// used to specialize the formatting of the field value. -// The option is a key-value pair specified as "format:value" where -// the value must be either a literal consisting of letters and numbers -// (e.g., "format:RFC3339") or a single-quoted string literal -// (e.g., "format:'2006-01-02'"). The interpretation of the format flag -// is determined by the struct field type. -// -// The "omitzero" and "omitempty" options are mostly semantically identical. -// The former is defined in terms of the Go type system, -// while the latter in terms of the JSON type system. -// Consequently they behave differently in some circumstances. -// For example, only a nil slice or map is omitted under "omitzero", while -// an empty slice or map is omitted under "omitempty" regardless of nilness. -// The "omitzero" option is useful for types with a well-defined zero value -// (e.g., [net/netip.Addr]) or have an IsZero method (e.g., [time.Time.IsZero]). -// -// Every Go struct corresponds to a list of JSON representable fields -// which is constructed by performing a breadth-first search over -// all struct fields (excluding unexported or ignored fields), -// where the search recursively descends into inlined structs. -// The set of non-inlined fields in a struct must have unique JSON names. -// If multiple fields all have the same JSON name, then the one -// at shallowest depth takes precedence and the other fields at deeper depths -// are excluded from the list of JSON representable fields. -// If multiple fields at the shallowest depth have the same JSON name, -// but exactly one is explicitly tagged with a JSON name, -// then that field takes precedence and all others are excluded from the list. -// This is analogous to Go visibility rules for struct field selection -// with embedded struct types. -// -// Marshaling or unmarshaling a non-empty struct -// without any JSON representable fields results in a [SemanticError]. -// Unexported fields must not have any `json` tags except for `json:"-"`. -// -// # Security Considerations -// -// JSON is frequently used as a data interchange format to communicate -// between different systems, possibly implemented in different languages. -// For interoperability and security reasons, it is important that -// all implementations agree upon the semantic meaning of the data. -// -// [For example, suppose we have two micro-services.] -// The first service is responsible for authenticating a JSON request, -// while the second service is responsible for executing the request -// (having assumed that the prior service authenticated the request). -// If an attacker were able to maliciously craft a JSON request such that -// both services believe that the same request is from different users, -// it could bypass the authenticator with valid credentials for one user, -// but maliciously perform an action on behalf of a different user. -// -// According to RFC 8259, there unfortunately exist many JSON texts -// that are syntactically valid but semantically ambiguous. -// For example, the standard does not define how to interpret duplicate -// names within an object. -// -// The v1 [encoding/json] and [encoding/json/v2] packages -// interpret some inputs in different ways. In particular: -// -// - The standard specifies that JSON must be encoded using UTF-8. -// By default, v1 replaces invalid bytes of UTF-8 in JSON strings -// with the Unicode replacement character, -// while v2 rejects inputs with invalid UTF-8. -// To change the default, specify the [jsontext.AllowInvalidUTF8] option. -// The replacement of invalid UTF-8 is a form of data corruption -// that alters the precise meaning of strings. -// -// - The standard does not specify a particular behavior when -// duplicate names are encountered within a JSON object, -// which means that different implementations may behave differently. -// By default, v1 allows for the presence of duplicate names, -// while v2 rejects duplicate names. -// To change the default, specify the [jsontext.AllowDuplicateNames] option. -// If allowed, object members are processed in the order they are observed, -// meaning that later values will replace or be merged into prior values, -// depending on the Go value type. -// -// - The standard defines a JSON object as an unordered collection of name/value pairs. -// While ordering can be observed through the underlying [jsontext] API, -// both v1 and v2 generally avoid exposing the ordering. -// No application should semantically depend on the order of object members. -// Allowing duplicate names is a vector through which ordering of members -// can accidentally be observed and depended upon. -// -// - The standard suggests that JSON object names are typically compared -// based on equality of the sequence of Unicode code points, -// which implies that comparing names is often case-sensitive. -// When unmarshaling a JSON object into a Go struct, -// by default, v1 uses a (loose) case-insensitive match on the name, -// while v2 uses a (strict) case-sensitive match on the name. -// To change the default, specify the [MatchCaseInsensitiveNames] option. -// The use of case-insensitive matching provides another vector through -// which duplicate names can occur. Allowing case-insensitive matching -// means that v1 or v2 might interpret JSON objects differently from most -// other JSON implementations (which typically use a case-sensitive match). -// -// - The standard does not specify a particular behavior when -// an unknown name in a JSON object is encountered. -// When unmarshaling a JSON object into a Go struct, by default -// both v1 and v2 ignore unknown names and their corresponding values. -// To change the default, specify the [RejectUnknownMembers] option. -// -// - The standard suggests that implementations may use a float64 -// to represent a JSON number. Consequently, large JSON integers -// may lose precision when stored as a floating-point type. -// Both v1 and v2 correctly preserve precision when marshaling and -// unmarshaling a concrete integer type. However, even if v1 and v2 -// preserve precision for concrete types, other JSON implementations -// may not be able to preserve precision for outputs produced by v1 or v2. -// The `string` tag option can be used to specify that an integer type -// is to be quoted within a JSON string to avoid loss of precision. -// Furthermore, v1 and v2 may still lose precision when unmarshaling -// into an any interface value, where unmarshal uses a float64 -// by default to represent a JSON number. -// To change the default, specify the [WithUnmarshalers] option -// with a custom unmarshaler that pre-populates the interface value -// with a concrete Go type that can preserve precision. -// -// RFC 8785 specifies a canonical form for any JSON text, -// which explicitly defines specific behaviors that RFC 8259 leaves undefined. -// In theory, if a text can successfully [jsontext.Value.Canonicalize] -// without changing the semantic meaning of the data, then it provides a -// greater degree of confidence that the data is more secure and interoperable. -// -// The v2 API generally chooses more secure defaults than v1, -// but care should still be taken with large integers or unknown members. -// -// [For example, suppose we have two micro-services.]: https://www.youtube.com/watch?v=avilmOcHKHE&t=1057s -package json - -import ( - "encoding/json/jsontext" - "encoding/json/v2" - "io" -) - -// Marshal serializes a Go value as a []byte according to the provided -// marshal and encode options (while ignoring unmarshal or decode options). -// It does not terminate the output with a newline. -// -// Type-specific marshal functions and methods take precedence -// over the default representation of a value. -// Functions or methods that operate on *T are only called when encoding -// a value of type T (by taking its address) or a non-nil value of *T. -// Marshal ensures that a value is always addressable -// (by boxing it on the heap if necessary) so that -// these functions and methods can be consistently called. For performance, -// it is recommended that Marshal be passed a non-nil pointer to the value. -// -// The input value is encoded as JSON according the following rules: -// -// - If any type-specific functions in a [WithMarshalers] option match -// the value type, then those functions are called to encode the value. -// If all applicable functions return [SkipFunc], -// then the value is encoded according to subsequent rules. -// -// - If the value type implements [MarshalerTo], -// then the MarshalJSONTo method is called to encode the value. -// -// - If the value type implements [Marshaler], -// then the MarshalJSON method is called to encode the value. -// -// - If the value type implements [encoding.TextAppender], -// then the AppendText method is called to encode the value and -// subsequently encode its result as a JSON string. -// -// - If the value type implements [encoding.TextMarshaler], -// then the MarshalText method is called to encode the value and -// subsequently encode its result as a JSON string. -// -// - Otherwise, the value is encoded according to the value's type -// as described in detail below. -// -// Most Go types have a default JSON representation. -// Certain types support specialized formatting according to -// a format flag optionally specified in the Go struct tag -// for the struct field that contains the current value -// (see the “JSON Representation of Go structs” section for more details). -// -// The representation of each type is as follows: -// -// - A Go boolean is encoded as a JSON boolean (e.g., true or false). -// It does not support any custom format flags. -// -// - A Go string is encoded as a JSON string. -// It does not support any custom format flags. -// -// - A Go []byte or [N]byte is encoded as a JSON string containing -// the binary value encoded using RFC 4648. -// If the format is "base64" or unspecified, then this uses RFC 4648, section 4. -// If the format is "base64url", then this uses RFC 4648, section 5. -// If the format is "base32", then this uses RFC 4648, section 6. -// If the format is "base32hex", then this uses RFC 4648, section 7. -// If the format is "base16" or "hex", then this uses RFC 4648, section 8. -// If the format is "array", then the bytes value is encoded as a JSON array -// where each byte is recursively JSON-encoded as each JSON array element. -// -// - A Go integer is encoded as a JSON number without fractions or exponents. -// If [StringifyNumbers] is specified or encoding a JSON object name, -// then the JSON number is encoded within a JSON string. -// It does not support any custom format flags. -// -// - A Go float is encoded as a JSON number. -// If [StringifyNumbers] is specified or encoding a JSON object name, -// then the JSON number is encoded within a JSON string. -// If the format is "nonfinite", then NaN, +Inf, and -Inf are encoded as -// the JSON strings "NaN", "Infinity", and "-Infinity", respectively. -// Otherwise, the presence of non-finite numbers results in a [SemanticError]. -// -// - A Go map is encoded as a JSON object, where each Go map key and value -// is recursively encoded as a name and value pair in the JSON object. -// The Go map key must encode as a JSON string, otherwise this results -// in a [SemanticError]. The Go map is traversed in a non-deterministic order. -// For deterministic encoding, consider using the [Deterministic] option. -// If the format is "emitnull", then a nil map is encoded as a JSON null. -// If the format is "emitempty", then a nil map is encoded as an empty JSON object, -// regardless of whether [FormatNilMapAsNull] is specified. -// Otherwise by default, a nil map is encoded as an empty JSON object. -// -// - A Go struct is encoded as a JSON object. -// See the “JSON Representation of Go structs” section -// in the package-level documentation for more details. -// -// - A Go slice is encoded as a JSON array, where each Go slice element -// is recursively JSON-encoded as the elements of the JSON array. -// If the format is "emitnull", then a nil slice is encoded as a JSON null. -// If the format is "emitempty", then a nil slice is encoded as an empty JSON array, -// regardless of whether [FormatNilSliceAsNull] is specified. -// Otherwise by default, a nil slice is encoded as an empty JSON array. -// -// - A Go array is encoded as a JSON array, where each Go array element -// is recursively JSON-encoded as the elements of the JSON array. -// The JSON array length is always identical to the Go array length. -// It does not support any custom format flags. -// -// - A Go pointer is encoded as a JSON null if nil, otherwise it is -// the recursively JSON-encoded representation of the underlying value. -// Format flags are forwarded to the encoding of the underlying value. -// -// - A Go interface is encoded as a JSON null if nil, otherwise it is -// the recursively JSON-encoded representation of the underlying value. -// It does not support any custom format flags. -// -// - A Go [time.Time] is encoded as a JSON string containing the timestamp -// formatted in RFC 3339 with nanosecond precision. -// If the format matches one of the format constants declared -// in the time package (e.g., RFC1123), then that format is used. -// If the format is "unix", "unixmilli", "unixmicro", or "unixnano", -// then the timestamp is encoded as a possibly fractional JSON number -// of the number of seconds (or milliseconds, microseconds, or nanoseconds) -// since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC. -// To avoid a fractional component, round the timestamp to the relevant unit. -// Otherwise, the format is used as-is with [time.Time.Format] if non-empty. -// -// - A Go [time.Duration] currently has no default representation and -// requires an explicit format to be specified. -// If the format is "sec", "milli", "micro", or "nano", -// then the duration is encoded as a possibly fractional JSON number -// of the number of seconds (or milliseconds, microseconds, or nanoseconds). -// To avoid a fractional component, round the duration to the relevant unit. -// If the format is "units", it is encoded as a JSON string formatted using -// [time.Duration.String] (e.g., "1h30m" for 1 hour 30 minutes). -// If the format is "iso8601", it is encoded as a JSON string using the -// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes) -// using only accurate units of hours, minutes, and seconds. -// -// - All other Go types (e.g., complex numbers, channels, and functions) -// have no default representation and result in a [SemanticError]. -// -// JSON cannot represent cyclic data structures and Marshal does not handle them. -// Passing cyclic structures will result in an error. -func Marshal(in any, opts ...Options) (out []byte, err error) { - return json.Marshal(in, opts...) -} - -// MarshalWrite serializes a Go value into an [io.Writer] according to the provided -// marshal and encode options (while ignoring unmarshal or decode options). -// It does not terminate the output with a newline. -// See [Marshal] for details about the conversion of a Go value into JSON. -func MarshalWrite(out io.Writer, in any, opts ...Options) (err error) { - return json.MarshalWrite(out, in, opts...) -} - -// MarshalEncode serializes a Go value into an [jsontext.Encoder] according to -// the provided marshal options (while ignoring unmarshal, encode, or decode options). -// Any marshal-relevant options already specified on the [jsontext.Encoder] -// take lower precedence than the set of options provided by the caller. -// Unlike [Marshal] and [MarshalWrite], encode options are ignored because -// they must have already been specified on the provided [jsontext.Encoder]. -// -// See [Marshal] for details about the conversion of a Go value into JSON. -func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) (err error) { - return json.MarshalEncode(out, in, opts...) -} - -// Unmarshal decodes a []byte input into a Go value according to the provided -// unmarshal and decode options (while ignoring marshal or encode options). -// The input must be a single JSON value with optional whitespace interspersed. -// The output must be a non-nil pointer. -// -// Type-specific unmarshal functions and methods take precedence -// over the default representation of a value. -// Functions or methods that operate on *T are only called when decoding -// a value of type T (by taking its address) or a non-nil value of *T. -// Unmarshal ensures that a value is always addressable -// (by boxing it on the heap if necessary) so that -// these functions and methods can be consistently called. -// -// The input is decoded into the output according the following rules: -// -// - If any type-specific functions in a [WithUnmarshalers] option match -// the value type, then those functions are called to decode the JSON -// value. If all applicable functions return [SkipFunc], -// then the input is decoded according to subsequent rules. -// -// - If the value type implements [UnmarshalerFrom], -// then the UnmarshalJSONFrom method is called to decode the JSON value. -// -// - If the value type implements [Unmarshaler], -// then the UnmarshalJSON method is called to decode the JSON value. -// -// - If the value type implements [encoding.TextUnmarshaler], -// then the input is decoded as a JSON string and -// the UnmarshalText method is called with the decoded string value. -// This fails with a [SemanticError] if the input is not a JSON string. -// -// - Otherwise, the JSON value is decoded according to the value's type -// as described in detail below. -// -// Most Go types have a default JSON representation. -// Certain types support specialized formatting according to -// a format flag optionally specified in the Go struct tag -// for the struct field that contains the current value -// (see the “JSON Representation of Go structs” section for more details). -// A JSON null may be decoded into every supported Go value where -// it is equivalent to storing the zero value of the Go value. -// If the input JSON kind is not handled by the current Go value type, -// then this fails with a [SemanticError]. Unless otherwise specified, -// the decoded value replaces any pre-existing value. -// -// The representation of each type is as follows: -// -// - A Go boolean is decoded from a JSON boolean (e.g., true or false). -// It does not support any custom format flags. -// -// - A Go string is decoded from a JSON string. -// It does not support any custom format flags. -// -// - A Go []byte or [N]byte is decoded from a JSON string -// containing the binary value encoded using RFC 4648. -// If the format is "base64" or unspecified, then this uses RFC 4648, section 4. -// If the format is "base64url", then this uses RFC 4648, section 5. -// If the format is "base32", then this uses RFC 4648, section 6. -// If the format is "base32hex", then this uses RFC 4648, section 7. -// If the format is "base16" or "hex", then this uses RFC 4648, section 8. -// If the format is "array", then the Go slice or array is decoded from a -// JSON array where each JSON element is recursively decoded for each byte. -// When decoding into a non-nil []byte, the slice length is reset to zero -// and the decoded input is appended to it. -// When decoding into a [N]byte, the input must decode to exactly N bytes, -// otherwise it fails with a [SemanticError]. -// -// - A Go integer is decoded from a JSON number. -// It must be decoded from a JSON string containing a JSON number -// if [StringifyNumbers] is specified or decoding a JSON object name. -// It fails with a [SemanticError] if the JSON number -// has a fractional or exponent component. -// It also fails if it overflows the representation of the Go integer type. -// It does not support any custom format flags. -// -// - A Go float is decoded from a JSON number. -// It must be decoded from a JSON string containing a JSON number -// if [StringifyNumbers] is specified or decoding a JSON object name. -// It fails if it overflows the representation of the Go float type. -// If the format is "nonfinite", then the JSON strings -// "NaN", "Infinity", and "-Infinity" are decoded as NaN, +Inf, and -Inf. -// Otherwise, the presence of such strings results in a [SemanticError]. -// -// - A Go map is decoded from a JSON object, -// where each JSON object name and value pair is recursively decoded -// as the Go map key and value. Maps are not cleared. -// If the Go map is nil, then a new map is allocated to decode into. -// If the decoded key matches an existing Go map entry, the entry value -// is reused by decoding the JSON object value into it. -// The formats "emitnull" and "emitempty" have no effect when decoding. -// -// - A Go struct is decoded from a JSON object. -// See the “JSON Representation of Go structs” section -// in the package-level documentation for more details. -// -// - A Go slice is decoded from a JSON array, where each JSON element -// is recursively decoded and appended to the Go slice. -// Before appending into a Go slice, a new slice is allocated if it is nil, -// otherwise the slice length is reset to zero. -// The formats "emitnull" and "emitempty" have no effect when decoding. -// -// - A Go array is decoded from a JSON array, where each JSON array element -// is recursively decoded as each corresponding Go array element. -// Each Go array element is zeroed before decoding into it. -// It fails with a [SemanticError] if the JSON array does not contain -// the exact same number of elements as the Go array. -// It does not support any custom format flags. -// -// - A Go pointer is decoded based on the JSON kind and underlying Go type. -// If the input is a JSON null, then this stores a nil pointer. -// Otherwise, it allocates a new underlying value if the pointer is nil, -// and recursively JSON decodes into the underlying value. -// Format flags are forwarded to the decoding of the underlying type. -// -// - A Go interface is decoded based on the JSON kind and underlying Go type. -// If the input is a JSON null, then this stores a nil interface value. -// Otherwise, a nil interface value of an empty interface type is initialized -// with a zero Go bool, string, float64, map[string]any, or []any if the -// input is a JSON boolean, string, number, object, or array, respectively. -// If the interface value is still nil, then this fails with a [SemanticError] -// since decoding could not determine an appropriate Go type to decode into. -// For example, unmarshaling into a nil io.Reader fails since -// there is no concrete type to populate the interface value with. -// Otherwise an underlying value exists and it recursively decodes -// the JSON input into it. It does not support any custom format flags. -// -// - A Go [time.Time] is decoded from a JSON string containing the time -// formatted in RFC 3339 with nanosecond precision. -// If the format matches one of the format constants declared in -// the time package (e.g., RFC1123), then that format is used for parsing. -// If the format is "unix", "unixmilli", "unixmicro", or "unixnano", -// then the timestamp is decoded from an optionally fractional JSON number -// of the number of seconds (or milliseconds, microseconds, or nanoseconds) -// since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC. -// Otherwise, the format is used as-is with [time.Time.Parse] if non-empty. -// -// - A Go [time.Duration] currently has no default representation and -// requires an explicit format to be specified. -// If the format is "sec", "milli", "micro", or "nano", -// then the duration is decoded from an optionally fractional JSON number -// of the number of seconds (or milliseconds, microseconds, or nanoseconds). -// If the format is "units", it is decoded from a JSON string parsed using -// [time.ParseDuration] (e.g., "1h30m" for 1 hour 30 minutes). -// If the format is "iso8601", it is decoded from a JSON string using the -// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes) -// accepting only accurate units of hours, minutes, or seconds. -// -// - All other Go types (e.g., complex numbers, channels, and functions) -// have no default representation and result in a [SemanticError]. -// -// In general, unmarshaling follows merge semantics (similar to RFC 7396) -// where the decoded Go value replaces the destination value -// for any JSON kind other than an object. -// For JSON objects, the input object is merged into the destination value -// where matching object members recursively apply merge semantics. -func Unmarshal(in []byte, out any, opts ...Options) (err error) { - return json.Unmarshal(in, out, opts...) -} - -// UnmarshalRead deserializes a Go value from an [io.Reader] according to the -// provided unmarshal and decode options (while ignoring marshal or encode options). -// The input must be a single JSON value with optional whitespace interspersed. -// It consumes the entirety of [io.Reader] until [io.EOF] is encountered, -// without reporting an error for EOF. The output must be a non-nil pointer. -// See [Unmarshal] for details about the conversion of JSON into a Go value. -func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) { - return json.UnmarshalRead(in, out, opts...) -} - -// UnmarshalDecode deserializes a Go value from a [jsontext.Decoder] according to -// the provided unmarshal options (while ignoring marshal, encode, or decode options). -// Any unmarshal options already specified on the [jsontext.Decoder] -// take lower precedence than the set of options provided by the caller. -// Unlike [Unmarshal] and [UnmarshalRead], decode options are ignored because -// they must have already been specified on the provided [jsontext.Decoder]. -// -// The input may be a stream of one or more JSON values, -// where this only unmarshals the next JSON value in the stream. -// The output must be a non-nil pointer. -// See [Unmarshal] for details about the conversion of JSON into a Go value. -func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) { - return json.UnmarshalDecode(in, out, opts...) -} - -// Marshalers is a list of functions that may override the marshal behavior -// of specific types. Populate [WithMarshalers] to use it with -// [Marshal], [MarshalWrite], or [MarshalEncode]. -// A nil *Marshalers is equivalent to an empty list. -// There are no exported fields or methods on Marshalers. -type Marshalers = json.Marshalers - -// JoinMarshalers constructs a flattened list of marshal functions. -// If multiple functions in the list are applicable for a value of a given type, -// then those earlier in the list take precedence over those that come later. -// If a function returns [SkipFunc], then the next applicable function is called, -// otherwise the default marshaling behavior is used. -// -// For example: -// -// m1 := JoinMarshalers(f1, f2) -// m2 := JoinMarshalers(f0, m1, f3) // equivalent to m3 -// m3 := JoinMarshalers(f0, f1, f2, f3) // equivalent to m2 -func JoinMarshalers(ms ...*Marshalers) *Marshalers { - return json.JoinMarshalers(ms...) -} - -// Unmarshalers is a list of functions that may override the unmarshal behavior -// of specific types. Populate [WithUnmarshalers] to use it with -// [Unmarshal], [UnmarshalRead], or [UnmarshalDecode]. -// A nil *Unmarshalers is equivalent to an empty list. -// There are no exported fields or methods on Unmarshalers. -type Unmarshalers = json.Unmarshalers - -// JoinUnmarshalers constructs a flattened list of unmarshal functions. -// If multiple functions in the list are applicable for a value of a given type, -// then those earlier in the list take precedence over those that come later. -// If a function returns [SkipFunc], then the next applicable function is called, -// otherwise the default unmarshaling behavior is used. -// -// For example: -// -// u1 := JoinUnmarshalers(f1, f2) -// u2 := JoinUnmarshalers(f0, u1, f3) // equivalent to u3 -// u3 := JoinUnmarshalers(f0, f1, f2, f3) // equivalent to u2 -func JoinUnmarshalers(us ...*Unmarshalers) *Unmarshalers { - return json.JoinUnmarshalers(us...) -} - -// MarshalFunc constructs a type-specific marshaler that -// specifies how to marshal values of type T. -// T can be any type except a named pointer. -// The function is always provided with a non-nil pointer value -// if T is an interface or pointer type. -// -// The function must marshal exactly one JSON value. -// The value of T must not be retained outside the function call. -// It may not return [SkipFunc]. -func MarshalFunc[T any](fn func(T) ([]byte, error)) *Marshalers { - return json.MarshalFunc[T](fn) -} - -// MarshalToFunc constructs a type-specific marshaler that -// specifies how to marshal values of type T. -// T can be any type except a named pointer. -// The function is always provided with a non-nil pointer value -// if T is an interface or pointer type. -// -// The function must marshal exactly one JSON value by calling write methods -// on the provided encoder. It may return [SkipFunc] such that marshaling can -// move on to the next marshal function. However, no mutable method calls may -// be called on the encoder if [SkipFunc] is returned. -// The pointer to [jsontext.Encoder] and the value of T -// must not be retained outside the function call. -func MarshalToFunc[T any](fn func(*jsontext.Encoder, T) error) *Marshalers { - return json.MarshalToFunc[T](fn) -} - -// UnmarshalFunc constructs a type-specific unmarshaler that -// specifies how to unmarshal values of type T. -// T must be an unnamed pointer or an interface type. -// The function is always provided with a non-nil pointer value. -// -// The function must unmarshal exactly one JSON value. -// The input []byte must not be mutated. -// The input []byte and value T must not be retained outside the function call. -// It may not return [SkipFunc]. -func UnmarshalFunc[T any](fn func([]byte, T) error) *Unmarshalers { - return json.UnmarshalFunc[T](fn) -} - -// UnmarshalFromFunc constructs a type-specific unmarshaler that -// specifies how to unmarshal values of type T. -// T must be an unnamed pointer or an interface type. -// The function is always provided with a non-nil pointer value. -// -// The function must unmarshal exactly one JSON value by calling read methods -// on the provided decoder. It may return [SkipFunc] such that unmarshaling can -// move on to the next unmarshal function. However, no mutable method calls may -// be called on the decoder if [SkipFunc] is returned. -// The pointer to [jsontext.Decoder] and the value of T -// must not be retained outside the function call. -func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers { - return json.UnmarshalFromFunc[T](fn) -} - -// Marshaler is implemented by types that can marshal themselves. -// It is recommended that types implement [MarshalerTo] unless the implementation -// is trying to avoid a hard dependency on the "jsontext" package. -// -// It is recommended that implementations return a buffer that is safe -// for the caller to retain and potentially mutate. -type Marshaler = json.Marshaler - -// MarshalerTo is implemented by types that can marshal themselves. -// It is recommended that types implement MarshalerTo instead of [Marshaler] -// since this is both more performant and flexible. -// If a type implements both Marshaler and MarshalerTo, -// then MarshalerTo takes precedence. In such a case, both implementations -// should aim to have equivalent behavior for the default marshal options. -// -// The implementation must write only one JSON value to the Encoder and -// must not retain the pointer to [jsontext.Encoder]. -type MarshalerTo = json.MarshalerTo - -// Unmarshaler is implemented by types that can unmarshal themselves. -// It is recommended that types implement [UnmarshalerFrom] unless the implementation -// is trying to avoid a hard dependency on the "jsontext" package. -// -// The input can be assumed to be a valid encoding of a JSON value -// if called from unmarshal functionality in this package. -// UnmarshalJSON must copy the JSON data if it is retained after returning. -// It is recommended that UnmarshalJSON implement merge semantics when -// unmarshaling into a pre-populated value. -// -// Implementations must not retain or mutate the input []byte. -type Unmarshaler = json.Unmarshaler - -// UnmarshalerFrom is implemented by types that can unmarshal themselves. -// It is recommended that types implement UnmarshalerFrom instead of [Unmarshaler] -// since this is both more performant and flexible. -// If a type implements both Unmarshaler and UnmarshalerFrom, -// then UnmarshalerFrom takes precedence. In such a case, both implementations -// should aim to have equivalent behavior for the default unmarshal options. -// -// The implementation must read only one JSON value from the Decoder. -// It is recommended that UnmarshalJSONFrom implement merge semantics when -// unmarshaling into a pre-populated value. -// -// Implementations must not retain the pointer to [jsontext.Decoder]. -type UnmarshalerFrom = json.UnmarshalerFrom - -// ErrUnknownName indicates that a JSON object member could not be -// unmarshaled because the name is not known to the target Go struct. -// This error is directly wrapped within a [SemanticError] when produced. -// -// The name of an unknown JSON object member can be extracted as: -// -// err := ... -// var serr json.SemanticError -// if errors.As(err, &serr) && serr.Err == json.ErrUnknownName { -// ptr := serr.JSONPointer // JSON pointer to unknown name -// name := ptr.LastToken() // unknown name itself -// ... -// } -// -// This error is only returned if [RejectUnknownMembers] is true. -var ErrUnknownName = json.ErrUnknownName - -// SemanticError describes an error determining the meaning -// of JSON data as Go data or vice-versa. -// -// The contents of this error as produced by this package may change over time. -type SemanticError = json.SemanticError - -// Options configure [Marshal], [MarshalWrite], [MarshalEncode], -// [Unmarshal], [UnmarshalRead], and [UnmarshalDecode] with specific features. -// Each function takes in a variadic list of options, where properties -// set in later options override the value of previously set properties. -// -// The Options type is identical to [encoding/json.Options] and -// [encoding/json/jsontext.Options]. Options from the other packages can -// be used interchangeably with functionality in this package. -// -// Options represent either a singular option or a set of options. -// It can be functionally thought of as a Go map of option properties -// (even though the underlying implementation avoids Go maps for performance). -// -// The constructors (e.g., [Deterministic]) return a singular option value: -// -// opt := Deterministic(true) -// -// which is analogous to creating a single entry map: -// -// opt := Options{"Deterministic": true} -// -// [JoinOptions] composes multiple options values to together: -// -// out := JoinOptions(opts...) -// -// which is analogous to making a new map and copying the options over: -// -// out := make(Options) -// for _, m := range opts { -// for k, v := range m { -// out[k] = v -// } -// } -// -// [GetOption] looks up the value of options parameter: -// -// v, ok := GetOption(opts, Deterministic) -// -// which is analogous to a Go map lookup: -// -// v, ok := Options["Deterministic"] -// -// There is a single Options type, which is used with both marshal and unmarshal. -// Some options affect both operations, while others only affect one operation: -// -// - [StringifyNumbers] affects marshaling and unmarshaling -// - [Deterministic] affects marshaling only -// - [FormatNilSliceAsNull] affects marshaling only -// - [FormatNilMapAsNull] affects marshaling only -// - [OmitZeroStructFields] affects marshaling only -// - [MatchCaseInsensitiveNames] affects marshaling and unmarshaling -// - [DiscardUnknownMembers] affects marshaling only -// - [RejectUnknownMembers] affects unmarshaling only -// - [WithMarshalers] affects marshaling only -// - [WithUnmarshalers] affects unmarshaling only -// -// Options that do not affect a particular operation are ignored. -type Options = json.Options - -// JoinOptions coalesces the provided list of options into a single Options. -// Properties set in later options override the value of previously set properties. -func JoinOptions(srcs ...Options) Options { - return json.JoinOptions(srcs...) -} - -// GetOption returns the value stored in opts with the provided setter, -// reporting whether the value is present. -// -// Example usage: -// -// v, ok := json.GetOption(opts, json.Deterministic) -// -// Options are most commonly introspected to alter the JSON representation of -// [MarshalerTo.MarshalJSONTo] and [UnmarshalerFrom.UnmarshalJSONFrom] methods, and -// [MarshalToFunc] and [UnmarshalFromFunc] functions. -// In such cases, the presence bit should generally be ignored. -func GetOption[T any](opts Options, setter func(T) Options) (T, bool) { - return json.GetOption[T](opts, setter) -} - -// DefaultOptionsV2 is the full set of all options that define v2 semantics. -// It is equivalent to all options under [Options], [encoding/json.Options], -// and [encoding/json/jsontext.Options] being set to false or the zero value, -// except for the options related to whitespace formatting. -func DefaultOptionsV2() Options { - return json.DefaultOptionsV2() -} - -// StringifyNumbers specifies that numeric Go types should be marshaled -// as a JSON string containing the equivalent JSON number value. -// When unmarshaling, numeric Go types are parsed from a JSON string -// containing the JSON number without any surrounding whitespace. -// -// According to RFC 8259, section 6, a JSON implementation may choose to -// limit the representation of a JSON number to an IEEE 754 binary64 value. -// This may cause decoders to lose precision for int64 and uint64 types. -// Quoting JSON numbers as a JSON string preserves the exact precision. -// -// This affects either marshaling or unmarshaling. -func StringifyNumbers(v bool) Options { - return json.StringifyNumbers(v) -} - -// Deterministic specifies that the same input value will be serialized -// as the exact same output bytes. Different processes of -// the same program will serialize equal values to the same bytes, -// but different versions of the same program are not guaranteed -// to produce the exact same sequence of bytes. -// -// This only affects marshaling and is ignored when unmarshaling. -func Deterministic(v bool) Options { - return json.Deterministic(v) -} - -// FormatNilSliceAsNull specifies that a nil Go slice should marshal as a -// JSON null instead of the default representation as an empty JSON array -// (or an empty JSON string in the case of ~[]byte). -// Slice fields explicitly marked with `format:emitempty` still marshal -// as an empty JSON array. -// -// This only affects marshaling and is ignored when unmarshaling. -func FormatNilSliceAsNull(v bool) Options { - return json.FormatNilSliceAsNull(v) -} - -// FormatNilMapAsNull specifies that a nil Go map should marshal as a -// JSON null instead of the default representation as an empty JSON object. -// Map fields explicitly marked with `format:emitempty` still marshal -// as an empty JSON object. -// -// This only affects marshaling and is ignored when unmarshaling. -func FormatNilMapAsNull(v bool) Options { - return json.FormatNilMapAsNull(v) -} - -// OmitZeroStructFields specifies that a Go struct should marshal in such a way -// that all struct fields that are zero are omitted from the marshaled output -// if the value is zero as determined by the "IsZero() bool" method if present, -// otherwise based on whether the field is the zero Go value. -// This is semantically equivalent to specifying the `omitzero` tag option -// on every field in a Go struct. -// -// This only affects marshaling and is ignored when unmarshaling. -func OmitZeroStructFields(v bool) Options { - return json.OmitZeroStructFields(v) -} - -// MatchCaseInsensitiveNames specifies that JSON object members are matched -// against Go struct fields using a case-insensitive match of the name. -// Go struct fields explicitly marked with `case:strict` or `case:ignore` -// always use case-sensitive (or case-insensitive) name matching, -// regardless of the value of this option. -// -// This affects either marshaling or unmarshaling. -// For marshaling, this option may alter the detection of duplicate names -// (assuming [jsontext.AllowDuplicateNames] is false) from inlined fields -// if it matches one of the declared fields in the Go struct. -func MatchCaseInsensitiveNames(v bool) Options { - return json.MatchCaseInsensitiveNames(v) -} - -// RejectUnknownMembers specifies that unknown members should be rejected -// when unmarshaling a JSON object, regardless of whether there is a field -// to store unknown members. -// -// This only affects unmarshaling and is ignored when marshaling. -func RejectUnknownMembers(v bool) Options { - return json.RejectUnknownMembers(v) -} - -// WithMarshalers specifies a list of type-specific marshalers to use, -// which can be used to override the default marshal behavior for values -// of particular types. -// -// This only affects marshaling and is ignored when unmarshaling. -func WithMarshalers(v *Marshalers) Options { - return json.WithMarshalers(v) -} - -// WithUnmarshalers specifies a list of type-specific unmarshalers to use, -// which can be used to override the default unmarshal behavior for values -// of particular types. -// -// This only affects unmarshaling and is ignored when marshaling. -func WithUnmarshalers(v *Unmarshalers) Options { - return json.WithUnmarshalers(v) -} diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal.go index 85d530389..dbf92432d 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_any.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_any.go index 22ed430fb..fd034c1a1 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_any.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_any.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_default.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_default.go index 64d2b7a9b..3f21c8d35 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_default.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_default.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_funcs.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_funcs.go index 1f5d01868..50dca26b8 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_funcs.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_funcs.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_inlined.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_inlined.go index f73ed3240..4509b87e4 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_inlined.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_inlined.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_methods.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_methods.go index d6736342b..42d6ee876 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_methods.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_methods.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_time.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_time.go index 4d328ebee..30dd584d1 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_time.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/arshal_time.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/doc.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/doc.go index a46316858..ff70951ab 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/doc.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/doc.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - // Package json implements semantic processing of JSON as specified in RFC 8259. // JSON is a simple data interchange format that can represent // primitive data types such as booleans, strings, and numbers, diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/errors.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/errors.go index 5b5d5f93a..1df765384 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/errors.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/errors.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fields.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fields.go index 045c6988a..d539feedd 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fields.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fields.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fold.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fold.go index 973f52e73..9ab735814 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fold.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/fold.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/intern.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/intern.go index 1bfb8ca63..42fc3d471 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/intern.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/intern.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/internal.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/internal.go index 00b43fa30..05801c6b2 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/internal.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/internal.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package internal import "errors" diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonflags/flags.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonflags/flags.go index 36300011e..810128b51 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonflags/flags.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonflags/flags.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - // jsonflags implements all the optional boolean flags. // These flags are shared across both "json", "jsontext", and "jsonopts". package jsonflags diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonopts/options.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonopts/options.go index c4fc8dba8..142653518 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonopts/options.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonopts/options.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsonopts import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/decode.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/decode.go index 6a5acb8ec..02787719c 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/decode.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/decode.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsonwire import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/encode.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/encode.go index e74ed713e..35b238a2e 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/encode.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/encode.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsonwire import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/wire.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/wire.go index a0622c65b..1831a66bd 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/wire.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/internal/jsonwire/wire.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - // Package jsonwire implements stateless functionality for handling JSON text. package jsonwire diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/alias.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/alias.go deleted file mode 100644 index dc18d5d55..000000000 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/alias.go +++ /dev/null @@ -1,536 +0,0 @@ -// Copyright 2025 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Code generated by alias_gen.go; DO NOT EDIT. - -//go:build goexperiment.jsonv2 && go1.25 - -// Package jsontext implements syntactic processing of JSON -// as specified in RFC 4627, RFC 7159, RFC 7493, RFC 8259, and RFC 8785. -// JSON is a simple data interchange format that can represent -// primitive data types such as booleans, strings, and numbers, -// in addition to structured data types such as objects and arrays. -// -// The [Encoder] and [Decoder] types are used to encode or decode -// a stream of JSON tokens or values. -// -// # Tokens and Values -// -// A JSON token refers to the basic structural elements of JSON: -// -// - a JSON literal (i.e., null, true, or false) -// - a JSON string (e.g., "hello, world!") -// - a JSON number (e.g., 123.456) -// - a begin or end delimiter for a JSON object (i.e., '{' or '}') -// - a begin or end delimiter for a JSON array (i.e., '[' or ']') -// -// A JSON token is represented by the [Token] type in Go. Technically, -// there are two additional structural characters (i.e., ':' and ','), -// but there is no [Token] representation for them since their presence -// can be inferred by the structure of the JSON grammar itself. -// For example, there must always be an implicit colon between -// the name and value of a JSON object member. -// -// A JSON value refers to a complete unit of JSON data: -// -// - a JSON literal, string, or number -// - a JSON object (e.g., `{"name":"value"}`) -// - a JSON array (e.g., `[1,2,3,]`) -// -// A JSON value is represented by the [Value] type in Go and is a []byte -// containing the raw textual representation of the value. There is some overlap -// between tokens and values as both contain literals, strings, and numbers. -// However, only a value can represent the entirety of a JSON object or array. -// -// The [Encoder] and [Decoder] types contain methods to read or write the next -// [Token] or [Value] in a sequence. They maintain a state machine to validate -// whether the sequence of JSON tokens and/or values produces a valid JSON. -// [Options] may be passed to the [NewEncoder] or [NewDecoder] constructors -// to configure the syntactic behavior of encoding and decoding. -// -// # Terminology -// -// The terms "encode" and "decode" are used for syntactic functionality -// that is concerned with processing JSON based on its grammar, and -// the terms "marshal" and "unmarshal" are used for semantic functionality -// that determines the meaning of JSON values as Go values and vice-versa. -// This package (i.e., [jsontext]) deals with JSON at a syntactic layer, -// while [encoding/json/v2] deals with JSON at a semantic layer. -// The goal is to provide a clear distinction between functionality that -// is purely concerned with encoding versus that of marshaling. -// For example, one can directly encode a stream of JSON tokens without -// needing to marshal a concrete Go value representing them. -// Similarly, one can decode a stream of JSON tokens without -// needing to unmarshal them into a concrete Go value. -// -// This package uses JSON terminology when discussing JSON, which may differ -// from related concepts in Go or elsewhere in computing literature. -// -// - a JSON "object" refers to an unordered collection of name/value members. -// - a JSON "array" refers to an ordered sequence of elements. -// - a JSON "value" refers to either a literal (i.e., null, false, or true), -// string, number, object, or array. -// -// See RFC 8259 for more information. -// -// # Specifications -// -// Relevant specifications include RFC 4627, RFC 7159, RFC 7493, RFC 8259, -// and RFC 8785. Each RFC is generally a stricter subset of another RFC. -// In increasing order of strictness: -// -// - RFC 4627 and RFC 7159 do not require (but recommend) the use of UTF-8 -// and also do not require (but recommend) that object names be unique. -// - RFC 8259 requires the use of UTF-8, -// but does not require (but recommends) that object names be unique. -// - RFC 7493 requires the use of UTF-8 -// and also requires that object names be unique. -// - RFC 8785 defines a canonical representation. It requires the use of UTF-8 -// and also requires that object names be unique and in a specific ordering. -// It specifies exactly how strings and numbers must be formatted. -// -// The primary difference between RFC 4627 and RFC 7159 is that the former -// restricted top-level values to only JSON objects and arrays, while -// RFC 7159 and subsequent RFCs permit top-level values to additionally be -// JSON nulls, booleans, strings, or numbers. -// -// By default, this package operates on RFC 7493, but can be configured -// to operate according to the other RFC specifications. -// RFC 7493 is a stricter subset of RFC 8259 and fully compliant with it. -// In particular, it makes specific choices about behavior that RFC 8259 -// leaves as undefined in order to ensure greater interoperability. -// -// # Security Considerations -// -// See the "Security Considerations" section in [encoding/json/v2]. -package jsontext - -import ( - "encoding/json/jsontext" - "io" -) - -// Decoder is a streaming decoder for raw JSON tokens and values. -// It is used to read a stream of top-level JSON values, -// each separated by optional whitespace characters. -// -// [Decoder.ReadToken] and [Decoder.ReadValue] calls may be interleaved. -// For example, the following JSON value: -// -// {"name":"value","array":[null,false,true,3.14159],"object":{"k":"v"}} -// -// can be parsed with the following calls (ignoring errors for brevity): -// -// d.ReadToken() // { -// d.ReadToken() // "name" -// d.ReadToken() // "value" -// d.ReadValue() // "array" -// d.ReadToken() // [ -// d.ReadToken() // null -// d.ReadToken() // false -// d.ReadValue() // true -// d.ReadToken() // 3.14159 -// d.ReadToken() // ] -// d.ReadValue() // "object" -// d.ReadValue() // {"k":"v"} -// d.ReadToken() // } -// -// The above is one of many possible sequence of calls and -// may not represent the most sensible method to call for any given token/value. -// For example, it is probably more common to call [Decoder.ReadToken] to obtain a -// string token for object names. -type Decoder = jsontext.Decoder - -// NewDecoder constructs a new streaming decoder reading from r. -// -// If r is a [bytes.Buffer], then the decoder parses directly from the buffer -// without first copying the contents to an intermediate buffer. -// Additional writes to the buffer must not occur while the decoder is in use. -func NewDecoder(r io.Reader, opts ...Options) *Decoder { - return jsontext.NewDecoder(r, opts...) -} - -// Encoder is a streaming encoder from raw JSON tokens and values. -// It is used to write a stream of top-level JSON values, -// each terminated with a newline character. -// -// [Encoder.WriteToken] and [Encoder.WriteValue] calls may be interleaved. -// For example, the following JSON value: -// -// {"name":"value","array":[null,false,true,3.14159],"object":{"k":"v"}} -// -// can be composed with the following calls (ignoring errors for brevity): -// -// e.WriteToken(BeginObject) // { -// e.WriteToken(String("name")) // "name" -// e.WriteToken(String("value")) // "value" -// e.WriteValue(Value(`"array"`)) // "array" -// e.WriteToken(BeginArray) // [ -// e.WriteToken(Null) // null -// e.WriteToken(False) // false -// e.WriteValue(Value("true")) // true -// e.WriteToken(Float(3.14159)) // 3.14159 -// e.WriteToken(EndArray) // ] -// e.WriteValue(Value(`"object"`)) // "object" -// e.WriteValue(Value(`{"k":"v"}`)) // {"k":"v"} -// e.WriteToken(EndObject) // } -// -// The above is one of many possible sequence of calls and -// may not represent the most sensible method to call for any given token/value. -// For example, it is probably more common to call [Encoder.WriteToken] with a string -// for object names. -type Encoder = jsontext.Encoder - -// NewEncoder constructs a new streaming encoder writing to w -// configured with the provided options. -// It flushes the internal buffer when the buffer is sufficiently full or -// when a top-level value has been written. -// -// If w is a [bytes.Buffer], then the encoder appends directly into the buffer -// without copying the contents from an intermediate buffer. -func NewEncoder(w io.Writer, opts ...Options) *Encoder { - return jsontext.NewEncoder(w, opts...) -} - -// SyntacticError is a description of a syntactic error that occurred when -// encoding or decoding JSON according to the grammar. -// -// The contents of this error as produced by this package may change over time. -type SyntacticError = jsontext.SyntacticError - -// Options configures [NewEncoder], [Encoder.Reset], [NewDecoder], -// and [Decoder.Reset] with specific features. -// Each function takes in a variadic list of options, where properties -// set in latter options override the value of previously set properties. -// -// There is a single Options type, which is used with both encoding and decoding. -// Some options affect both operations, while others only affect one operation: -// -// - [AllowDuplicateNames] affects encoding and decoding -// - [AllowInvalidUTF8] affects encoding and decoding -// - [EscapeForHTML] affects encoding only -// - [EscapeForJS] affects encoding only -// - [PreserveRawStrings] affects encoding only -// - [CanonicalizeRawInts] affects encoding only -// - [CanonicalizeRawFloats] affects encoding only -// - [ReorderRawObjects] affects encoding only -// - [SpaceAfterColon] affects encoding only -// - [SpaceAfterComma] affects encoding only -// - [Multiline] affects encoding only -// - [WithIndent] affects encoding only -// - [WithIndentPrefix] affects encoding only -// -// Options that do not affect a particular operation are ignored. -// -// The Options type is identical to [encoding/json.Options] and -// [encoding/json/v2.Options]. Options from the other packages may -// be passed to functionality in this package, but are ignored. -// Options from this package may be used with the other packages. -type Options = jsontext.Options - -// AllowDuplicateNames specifies that JSON objects may contain -// duplicate member names. Disabling the duplicate name check may provide -// performance benefits, but breaks compliance with RFC 7493, section 2.3. -// The input or output will still be compliant with RFC 8259, -// which leaves the handling of duplicate names as unspecified behavior. -// -// This affects either encoding or decoding. -func AllowDuplicateNames(v bool) Options { - return jsontext.AllowDuplicateNames(v) -} - -// AllowInvalidUTF8 specifies that JSON strings may contain invalid UTF-8, -// which will be mangled as the Unicode replacement character, U+FFFD. -// This causes the encoder or decoder to break compliance with -// RFC 7493, section 2.1, and RFC 8259, section 8.1. -// -// This affects either encoding or decoding. -func AllowInvalidUTF8(v bool) Options { - return jsontext.AllowInvalidUTF8(v) -} - -// EscapeForHTML specifies that '<', '>', and '&' characters within JSON strings -// should be escaped as a hexadecimal Unicode codepoint (e.g., \u003c) so that -// the output is safe to embed within HTML. -// -// This only affects encoding and is ignored when decoding. -func EscapeForHTML(v bool) Options { - return jsontext.EscapeForHTML(v) -} - -// EscapeForJS specifies that U+2028 and U+2029 characters within JSON strings -// should be escaped as a hexadecimal Unicode codepoint (e.g., \u2028) so that -// the output is valid to embed within JavaScript. See RFC 8259, section 12. -// -// This only affects encoding and is ignored when decoding. -func EscapeForJS(v bool) Options { - return jsontext.EscapeForJS(v) -} - -// PreserveRawStrings specifies that when encoding a raw JSON string in a -// [Token] or [Value], pre-escaped sequences -// in a JSON string are preserved to the output. -// However, raw strings still respect [EscapeForHTML] and [EscapeForJS] -// such that the relevant characters are escaped. -// If [AllowInvalidUTF8] is enabled, bytes of invalid UTF-8 -// are preserved to the output. -// -// This only affects encoding and is ignored when decoding. -func PreserveRawStrings(v bool) Options { - return jsontext.PreserveRawStrings(v) -} - -// CanonicalizeRawInts specifies that when encoding a raw JSON -// integer number (i.e., a number without a fraction and exponent) in a -// [Token] or [Value], the number is canonicalized -// according to RFC 8785, section 3.2.2.3. As a special case, -// the number -0 is canonicalized as 0. -// -// JSON numbers are treated as IEEE 754 double precision numbers. -// Any numbers with precision beyond what is representable by that form -// will lose their precision when canonicalized. For example, -// integer values beyond ±2⁵³ will lose their precision. -// For example, 1234567890123456789 is formatted as 1234567890123456800. -// -// This only affects encoding and is ignored when decoding. -func CanonicalizeRawInts(v bool) Options { - return jsontext.CanonicalizeRawInts(v) -} - -// CanonicalizeRawFloats specifies that when encoding a raw JSON -// floating-point number (i.e., a number with a fraction or exponent) in a -// [Token] or [Value], the number is canonicalized -// according to RFC 8785, section 3.2.2.3. As a special case, -// the number -0 is canonicalized as 0. -// -// JSON numbers are treated as IEEE 754 double precision numbers. -// It is safe to canonicalize a serialized single precision number and -// parse it back as a single precision number and expect the same value. -// If a number exceeds ±1.7976931348623157e+308, which is the maximum -// finite number, then it saturated at that value and formatted as such. -// -// This only affects encoding and is ignored when decoding. -func CanonicalizeRawFloats(v bool) Options { - return jsontext.CanonicalizeRawFloats(v) -} - -// ReorderRawObjects specifies that when encoding a raw JSON object in a -// [Value], the object members are reordered according to -// RFC 8785, section 3.2.3. -// -// This only affects encoding and is ignored when decoding. -func ReorderRawObjects(v bool) Options { - return jsontext.ReorderRawObjects(v) -} - -// SpaceAfterColon specifies that the JSON output should emit a space character -// after each colon separator following a JSON object name. -// If false, then no space character appears after the colon separator. -// -// This only affects encoding and is ignored when decoding. -func SpaceAfterColon(v bool) Options { - return jsontext.SpaceAfterColon(v) -} - -// SpaceAfterComma specifies that the JSON output should emit a space character -// after each comma separator following a JSON object value or array element. -// If false, then no space character appears after the comma separator. -// -// This only affects encoding and is ignored when decoding. -func SpaceAfterComma(v bool) Options { - return jsontext.SpaceAfterComma(v) -} - -// Multiline specifies that the JSON output should expand to multiple lines, -// where every JSON object member or JSON array element appears on -// a new, indented line according to the nesting depth. -// -// If [SpaceAfterColon] is not specified, then the default is true. -// If [SpaceAfterComma] is not specified, then the default is false. -// If [WithIndent] is not specified, then the default is "\t". -// -// If set to false, then the output is a single-line, -// where the only whitespace emitted is determined by the current -// values of [SpaceAfterColon] and [SpaceAfterComma]. -// -// This only affects encoding and is ignored when decoding. -func Multiline(v bool) Options { - return jsontext.Multiline(v) -} - -// WithIndent specifies that the encoder should emit multiline output -// where each element in a JSON object or array begins on a new, indented line -// beginning with the indent prefix (see [WithIndentPrefix]) -// followed by one or more copies of indent according to the nesting depth. -// The indent must only be composed of space or tab characters. -// -// If the intent to emit indented output without a preference for -// the particular indent string, then use [Multiline] instead. -// -// This only affects encoding and is ignored when decoding. -// Use of this option implies [Multiline] being set to true. -func WithIndent(indent string) Options { - return jsontext.WithIndent(indent) -} - -// WithIndentPrefix specifies that the encoder should emit multiline output -// where each element in a JSON object or array begins on a new, indented line -// beginning with the indent prefix followed by one or more copies of indent -// (see [WithIndent]) according to the nesting depth. -// The prefix must only be composed of space or tab characters. -// -// This only affects encoding and is ignored when decoding. -// Use of this option implies [Multiline] being set to true. -func WithIndentPrefix(prefix string) Options { - return jsontext.WithIndentPrefix(prefix) -} - -// AppendQuote appends a double-quoted JSON string literal representing src -// to dst and returns the extended buffer. -// It uses the minimal string representation per RFC 8785, section 3.2.2.2. -// Invalid UTF-8 bytes are replaced with the Unicode replacement character -// and an error is returned at the end indicating the presence of invalid UTF-8. -// The dst must not overlap with the src. -func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error) { - return jsontext.AppendQuote[Bytes](dst, src) -} - -// AppendUnquote appends the decoded interpretation of src as a -// double-quoted JSON string literal to dst and returns the extended buffer. -// The input src must be a JSON string without any surrounding whitespace. -// Invalid UTF-8 bytes are replaced with the Unicode replacement character -// and an error is returned at the end indicating the presence of invalid UTF-8. -// Any trailing bytes after the JSON string literal results in an error. -// The dst must not overlap with the src. -func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error) { - return jsontext.AppendUnquote[Bytes](dst, src) -} - -// ErrDuplicateName indicates that a JSON token could not be -// encoded or decoded because it results in a duplicate JSON object name. -// This error is directly wrapped within a [SyntacticError] when produced. -// -// The name of a duplicate JSON object member can be extracted as: -// -// err := ... -// var serr jsontext.SyntacticError -// if errors.As(err, &serr) && serr.Err == jsontext.ErrDuplicateName { -// ptr := serr.JSONPointer // JSON pointer to duplicate name -// name := ptr.LastToken() // duplicate name itself -// ... -// } -// -// This error is only returned if [AllowDuplicateNames] is false. -var ErrDuplicateName = jsontext.ErrDuplicateName - -// ErrNonStringName indicates that a JSON token could not be -// encoded or decoded because it is not a string, -// as required for JSON object names according to RFC 8259, section 4. -// This error is directly wrapped within a [SyntacticError] when produced. -var ErrNonStringName = jsontext.ErrNonStringName - -// Pointer is a JSON Pointer (RFC 6901) that references a particular JSON value -// relative to the root of the top-level JSON value. -// -// A Pointer is a slash-separated list of tokens, where each token is -// either a JSON object name or an index to a JSON array element -// encoded as a base-10 integer value. -// It is impossible to distinguish between an array index and an object name -// (that happens to be an base-10 encoded integer) without also knowing -// the structure of the top-level JSON value that the pointer refers to. -// -// There is exactly one representation of a pointer to a particular value, -// so comparability of Pointer values is equivalent to checking whether -// they both point to the exact same value. -type Pointer = jsontext.Pointer - -// Token represents a lexical JSON token, which may be one of the following: -// - a JSON literal (i.e., null, true, or false) -// - a JSON string (e.g., "hello, world!") -// - a JSON number (e.g., 123.456) -// - a begin or end delimiter for a JSON object (i.e., { or } ) -// - a begin or end delimiter for a JSON array (i.e., [ or ] ) -// -// A Token cannot represent entire array or object values, while a [Value] can. -// There is no Token to represent commas and colons since -// these structural tokens can be inferred from the surrounding context. -type Token = jsontext.Token - -var ( - Null = jsontext.Null - False = jsontext.False - True = jsontext.True - BeginObject = jsontext.BeginObject - EndObject = jsontext.EndObject - BeginArray = jsontext.BeginArray - EndArray = jsontext.EndArray -) - -// Bool constructs a Token representing a JSON boolean. -func Bool(b bool) Token { - return jsontext.Bool(b) -} - -// String constructs a Token representing a JSON string. -// The provided string should contain valid UTF-8, otherwise invalid characters -// may be mangled as the Unicode replacement character. -func String(s string) Token { - return jsontext.String(s) -} - -// Float constructs a Token representing a JSON number. -// The values NaN, +Inf, and -Inf will be represented -// as a JSON string with the values "NaN", "Infinity", and "-Infinity". -func Float(n float64) Token { - return jsontext.Float(n) -} - -// Int constructs a Token representing a JSON number from an int64. -func Int(n int64) Token { - return jsontext.Int(n) -} - -// Uint constructs a Token representing a JSON number from a uint64. -func Uint(n uint64) Token { - return jsontext.Uint(n) -} - -// Kind represents each possible JSON token kind with a single byte, -// which is conveniently the first byte of that kind's grammar -// with the restriction that numbers always be represented with '0': -// -// - 'n': null -// - 'f': false -// - 't': true -// - '"': string -// - '0': number -// - '{': object begin -// - '}': object end -// - '[': array begin -// - ']': array end -// -// An invalid kind is usually represented using 0, -// but may be non-zero due to invalid JSON data. -type Kind = jsontext.Kind - -// AppendFormat formats the JSON value in src and appends it to dst -// according to the specified options. -// See [Value.Format] for more details about the formatting behavior. -// -// The dst and src may overlap. -// If an error is reported, then the entirety of src is appended to dst. -func AppendFormat(dst, src []byte, opts ...Options) ([]byte, error) { - return jsontext.AppendFormat(dst, src, opts...) -} - -// Value represents a single raw JSON value, which may be one of the following: -// - a JSON literal (i.e., null, true, or false) -// - a JSON string (e.g., "hello, world!") -// - a JSON number (e.g., 123.456) -// - an entire JSON object (e.g., {"fizz":"buzz"} ) -// - an entire JSON array (e.g., [1,2,3] ) -// -// Value can represent entire array or object values, while [Token] cannot. -// Value may contain leading and/or trailing whitespace. -type Value = jsontext.Value diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/decode.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/decode.go index 7e847de37..96fd5c27c 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/decode.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/decode.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/doc.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/doc.go index 22081df05..bea3ccd34 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/doc.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/doc.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - // Package jsontext implements syntactic processing of JSON // as specified in RFC 4627, RFC 7159, RFC 7493, RFC 8259, and RFC 8785. // JSON is a simple data interchange format that can represent diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/encode.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/encode.go index c2e88045a..36bf7efdf 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/encode.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/encode.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/errors.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/errors.go index 3c53151b3..1af86d5b6 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/errors.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/errors.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/export.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/export.go index 0d6dc58c0..b30bb99ed 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/export.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/export.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/options.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/options.go index d22d0635d..e973b58d6 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/options.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/options.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/pools.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/pools.go index cf59d99b9..63941baac 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/pools.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/pools.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/quote.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/quote.go index a4353be3a..77897b43f 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/quote.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/quote.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/state.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/state.go index 6f1aa8e21..fdbfd5d3d 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/state.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/state.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/token.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/token.go index 3e87c9140..ea8c30eb5 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/token.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/token.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/value.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/value.go index f29f32356..3ea05d3a0 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/value.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/jsontext/value.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package jsontext import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/options.go b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/options.go index de401b0de..2f4744980 100644 --- a/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/options.go +++ b/vendor/k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json/options.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !goexperiment.jsonv2 || !go1.25 - package json import ( diff --git a/vendor/k8s.io/kube-openapi/pkg/schemaconv/openapi.go b/vendor/k8s.io/kube-openapi/pkg/schemaconv/openapi.go index 6067ee03d..06bcf0c37 100644 --- a/vendor/k8s.io/kube-openapi/pkg/schemaconv/openapi.go +++ b/vendor/k8s.io/kube-openapi/pkg/schemaconv/openapi.go @@ -147,11 +147,13 @@ func (c *convert) makeOpenAPIRef(specSchema *spec.Schema) schema.TypeRef { return schema.TypeRef{ NamedType: &n, ElementRelationship: &mapRelationship, + Nullable: specSchema.Nullable, } } return schema.TypeRef{ NamedType: &n, + Nullable: specSchema.Nullable, } } @@ -164,7 +166,8 @@ func (c *convert) makeOpenAPIRef(specSchema *spec.Schema) schema.TypeRef { c.pop(c2) return schema.TypeRef{ - Inlined: inlined, + Inlined: inlined, + Nullable: specSchema.Nullable, } } diff --git a/vendor/k8s.io/utils/dump/dump.go b/vendor/k8s.io/utils/dump/dump.go index cf61ef76a..d50465b37 100644 --- a/vendor/k8s.io/utils/dump/dump.go +++ b/vendor/k8s.io/utils/dump/dump.go @@ -39,16 +39,16 @@ var prettyPrintConfigForHash = &spew.ConfigState{ // Pretty wrap the spew.Sdump with Indent, and disabled methods like error() and String() // The output may change over time, so for guaranteed output please take more direct control -func Pretty(a interface{}) string { +func Pretty(a any) string { return prettyPrintConfig.Sdump(a) } // ForHash keeps the original Spew.Sprintf format to ensure the same checksum -func ForHash(a interface{}) string { +func ForHash(a any) string { return prettyPrintConfigForHash.Sprintf("%#v", a) } // OneLine outputs the object in one line -func OneLine(a interface{}) string { +func OneLine(a any) string { return prettyPrintConfig.Sprintf("%#v", a) } diff --git a/vendor/k8s.io/utils/internal/third_party/forked/golang/net/ip.go b/vendor/k8s.io/utils/internal/third_party/forked/golang/net/ip.go index 4340b6e74..407c24636 100644 --- a/vendor/k8s.io/utils/internal/third_party/forked/golang/net/ip.go +++ b/vendor/k8s.io/utils/internal/third_party/forked/golang/net/ip.go @@ -41,7 +41,7 @@ var IPv4 = stdnet.IPv4 // Parse IPv4 address (d.d.d.d). func parseIPv4(s string) IP { var p [IPv4len]byte - for i := 0; i < IPv4len; i++ { + for i := range IPv4len { if len(s) == 0 { // Missing octets. return nil diff --git a/vendor/k8s.io/utils/ptr/OWNERS b/vendor/k8s.io/utils/ptr/OWNERS index 0d6392752..55da8262a 100644 --- a/vendor/k8s.io/utils/ptr/OWNERS +++ b/vendor/k8s.io/utils/ptr/OWNERS @@ -2,9 +2,11 @@ approvers: - apelisse +- skitt - stewart-yu - thockin reviewers: - apelisse +- skitt - stewart-yu - thockin diff --git a/vendor/k8s.io/utils/ptr/ptr.go b/vendor/k8s.io/utils/ptr/ptr.go index ea847fd36..260f4f29a 100644 --- a/vendor/k8s.io/utils/ptr/ptr.go +++ b/vendor/k8s.io/utils/ptr/ptr.go @@ -27,19 +27,19 @@ import ( // // This function is only valid for structs and pointers to structs. Any other // type will cause a panic. Passing a typed nil pointer will return true. -func AllPtrFieldsNil(obj interface{}) bool { +func AllPtrFieldsNil(obj any) bool { v := reflect.ValueOf(obj) if !v.IsValid() { panic(fmt.Sprintf("reflect.ValueOf() produced a non-valid Value for %#v", obj)) } - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { if v.IsNil() { return true } v = v.Elem() } for i := 0; i < v.NumField(); i++ { - if v.Field(i).Kind() == reflect.Ptr && !v.Field(i).IsNil() { + if v.Field(i).Kind() == reflect.Pointer && !v.Field(i).IsNil() { return false } } diff --git a/vendor/k8s.io/utils/trace/trace.go b/vendor/k8s.io/utils/trace/trace.go index 559aebb59..7fa546450 100644 --- a/vendor/k8s.io/utils/trace/trace.go +++ b/vendor/k8s.io/utils/trace/trace.go @@ -34,7 +34,7 @@ var klogV = func(lvl klog.Level) bool { // Field is a key value pair that provides additional details about the trace. type Field struct { Key string - Value interface{} + Value any } func (f Field) format() string { diff --git a/vendor/modules.txt b/vendor/modules.txt index e07b168c6..06995b6fd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -388,6 +388,16 @@ github.com/opencontainers/image-spec/specs-go/v1 # github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260617154302-0835b732e6cd ## explicit; go 1.26.0 github.com/os-observability/redhat-opentelemetry-collector/configschemas +# github.com/ovn-kubernetes/ovn-kubernetes-mcp v0.1.0 +## explicit; go 1.26.0 +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/kubernetes/types +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/mcp +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/ovn/types +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/headtail +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/ovndb +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/pattern +github.com/ovn-kubernetes/ovn-kubernetes-mcp/pkg/utils/timeout # github.com/pavolloffay/opentelemetry-mcp-server/modules/schemagen v0.0.0-20260710124846-8bb49fd6ccc7 ## explicit; go 1.25.0 github.com/pavolloffay/opentelemetry-mcp-server/modules/schemagen @@ -1459,7 +1469,7 @@ k8s.io/klog/v2/internal/severity k8s.io/klog/v2/internal/sloghandler k8s.io/klog/v2/internal/verbosity k8s.io/klog/v2/textlogger -# k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 +# k8s.io/kube-openapi v0.0.0-20260706235625-cdb1db5517a0 ## explicit; go 1.24.0 k8s.io/kube-openapi/pkg/cached k8s.io/kube-openapi/pkg/common @@ -1501,8 +1511,8 @@ k8s.io/streaming/pkg/httpstream k8s.io/streaming/pkg/httpstream/spdy k8s.io/streaming/pkg/httpstream/wsstream k8s.io/streaming/pkg/runtime -# k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 -## explicit; go 1.23 +# k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 +## explicit; go 1.25 k8s.io/utils/buffer k8s.io/utils/clock k8s.io/utils/dump @@ -1677,7 +1687,7 @@ sigs.k8s.io/kustomize/kyaml/yaml/walk ## explicit; go 1.18 sigs.k8s.io/randfill sigs.k8s.io/randfill/bytesource -# sigs.k8s.io/structured-merge-diff/v6 v6.4.0 +# sigs.k8s.io/structured-merge-diff/v6 v6.4.2 ## explicit; go 1.23 sigs.k8s.io/structured-merge-diff/v6/fieldpath sigs.k8s.io/structured-merge-diff/v6/merge diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize-pe.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize-pe.go index f4b00c2ee..b2f62dc69 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize-pe.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize-pe.go @@ -97,6 +97,14 @@ func DeserializePathElement(s string) (PathElement, error) { if err != nil { return PathElement{}, err } + // Lookahead validates that there is no unexpected trailing data. + // io.EOF is a successful termination indicator; all other errors or trailing data fail. + if iter.WhatIsNext(); iter.Error != io.EOF { + if iter.Error == nil { + iter.ReportError("managedFields parsing", "unexpected trailing data after JSON object") + } + return PathElement{}, iter.Error + } return PathElement{Value: &v}, nil case peKeySepBytes[0]: iter := readPool.BorrowIterator(b) @@ -112,8 +120,16 @@ func DeserializePathElement(s string) (PathElement, error) { fields = append(fields, value.Field{Name: key, Value: v}) return true }) + // Lookahead validates that there is no unexpected trailing data. + // io.EOF is a successful termination indicator; all other errors or trailing data fail. + if iter.WhatIsNext(); iter.Error != io.EOF { + if iter.Error == nil { + iter.ReportError("managedFields parsing", "unexpected trailing data after JSON object") + } + return PathElement{}, iter.Error + } fields.Sort() - return PathElement{Key: &fields}, iter.Error + return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) if err != nil { diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize.go index b992b93c5..6701f17ee 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/fieldpath/serialize.go @@ -175,6 +175,17 @@ func (s *Set) FromJSON(r io.Reader) error { } else { *s = *found } + if iter.Error != nil { + return iter.Error + } + if iter.WhatIsNext(); iter.Error == nil { + // Piggy back on scanner aware error reporting here. + iter.ReportError("managedFields parsing", "unexpected trailing data after JSON object") + return iter.Error + } + if iter.Error == io.EOF { + return nil + } return iter.Error } diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go index c8138a654..4c0e30b25 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/elements.go @@ -62,6 +62,10 @@ type TypeRef struct { // If this field is nil, then it has no effect. // See `Map` and `List` for more information about `ElementRelationship` ElementRelationship *ElementRelationship `yaml:"elementRelationship,omitempty"` + + // Nullable indicates that an explicit null is a valid value, + // corresponding to OpenAPI's `nullable: true`. + Nullable bool `yaml:"nullable,omitempty"` } // Atom represents the smallest possible pieces of the type system. diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/equals.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/equals.go index b668eff83..e1fa5c95d 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/equals.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/equals.go @@ -55,6 +55,9 @@ func (a *TypeRef) Equals(b *TypeRef) bool { if a.ElementRelationship != b.ElementRelationship { return false } + if a.Nullable != b.Nullable { + return false + } return a.Inlined.Equals(&b.Inlined) } diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/schemaschema.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/schemaschema.go index 6eb6c36df..4eb367b65 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/schemaschema.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/schema/schemaschema.go @@ -69,6 +69,9 @@ var SchemaSchemaYAML = `types: - name: elementRelationship type: scalar: string + - name: nullable + type: + scalar: boolean - name: scalar scalar: string - name: map diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go index 0db1734f9..78ba6f50d 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/typed/remove.go @@ -75,7 +75,6 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { } var newItems []interface{} - hadMatches := false iter := l.RangeUsing(w.allocator) defer w.allocator.Free(iter) for iter.Next() { @@ -99,26 +98,12 @@ func (w *removingWalker) doList(t *schema.List) (errs ValidationErrors) { continue } if isPrefixMatch { - // Removing nested items within this list item and preserve if it becomes empty - hadMatches = true - wasMap := item.IsMap() - wasList := item.IsList() item = removeItemsWithSchema(item, w.toRemove.WithPrefix(pe), w.schema, t.ElementType, w.shouldExtract) - // If item returned null but we're removing items within the structure(not the item itself), - // preserve the empty container structure - if item.IsNull() && !w.shouldExtract { - if wasMap { - item = value.NewValueInterface(map[string]interface{}{}) - } else if wasList { - item = value.NewValueInterface([]interface{}{}) - } - } } newItems = append(newItems, item.Unstructured()) } } - // Preserve empty lists (non-nil) instead of converting to null when items were matched and removed - if len(newItems) > 0 || (hadMatches && !w.shouldExtract) { + if len(newItems) > 0 { w.out = newItems } return nil @@ -156,7 +141,6 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { } newMap := map[string]interface{}{} - hadMatches := false m.Iterate(func(k string, val value.Value) bool { pe := fieldpath.PathElement{FieldName: &k} path, _ := fieldpath.MakePath(pe) @@ -174,19 +158,7 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { return true } if subset := w.toRemove.WithPrefix(pe); !subset.Empty() { - hadMatches = true - wasMap := val.IsMap() - wasList := val.IsList() val = removeItemsWithSchema(val, subset, w.schema, fieldType, w.shouldExtract) - // If val returned null but we're removing items within the structure (not the field itself), - // preserve the empty container structure - if val.IsNull() && !w.shouldExtract { - if wasMap { - val = value.NewValueInterface(map[string]interface{}{}) - } else if wasList { - val = value.NewValueInterface([]interface{}{}) - } - } } else { // don't save values not on the path when we shouldExtract. if w.shouldExtract { @@ -196,8 +168,7 @@ func (w *removingWalker) doMap(t *schema.Map) ValidationErrors { newMap[k] = val.Unstructured() return true }) - // Preserve empty maps (non-nil) instead of converting to null when items were matched and removed - if len(newMap) > 0 || (hadMatches && !w.shouldExtract) { + if len(newMap) > 0 { w.out = newMap } return nil diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/value/reflectcache.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/value/reflectcache.go index 75b7085c3..f4f4c2b44 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/value/reflectcache.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/value/reflectcache.go @@ -39,14 +39,28 @@ type UnstructuredConverter interface { ToUnstructured() interface{} } +// UnstructuredConverterWithError is implemented instead of UnstructuredConverter by types for which +// some, but not all, values can be converted directly to unstructured. If a type implements both +// UnstructuredConverter and UnstructuredConverterWithError, its UnstructuredConverterWithError +// implementation takes precedence during conversion. +type UnstructuredConverterWithError interface { + json.Marshaler // require that json.Marshaler is implemented + + // ToUnstructured returns the unstructured representation, or a non-nil error if the value + // cannot be converted to unstructured. + ToUnstructuredWithError() (any, error) +} + // TypeReflectCacheEntry keeps data gathered using reflection about how a type is converted to/from unstructured. type TypeReflectCacheEntry struct { - isJsonMarshaler bool - ptrIsJsonMarshaler bool - isJsonUnmarshaler bool - ptrIsJsonUnmarshaler bool - isStringConvertable bool - ptrIsStringConvertable bool + isJsonMarshaler bool + ptrIsJsonMarshaler bool + isJsonUnmarshaler bool + ptrIsJsonUnmarshaler bool + isUnstructuredConverter bool + ptrIsUnstructuredConverter bool + isUnstructuredConverterWithError bool + ptrIsUnstructuredConverterWithError bool structFields map[string]*FieldCacheEntry orderedStructFields []*FieldCacheEntry @@ -93,9 +107,10 @@ func (f *FieldCacheEntry) GetFrom(structVal reflect.Value) reflect.Value { return structVal } -var marshalerType = reflect.TypeOf(new(json.Marshaler)).Elem() -var unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem() -var unstructuredConvertableType = reflect.TypeOf(new(UnstructuredConverter)).Elem() +var marshalerType = reflect.TypeFor[json.Marshaler]() +var unmarshalerType = reflect.TypeFor[json.Unmarshaler]() +var unstructuredConverterType = reflect.TypeFor[UnstructuredConverter]() +var unstructuredConverterWithErrorType = reflect.TypeFor[UnstructuredConverterWithError]() var defaultReflectCache = newReflectCache() // TypeReflectEntryOf returns the TypeReflectCacheEntry of the provided reflect.Type. @@ -122,11 +137,13 @@ func typeReflectEntryOf(cm reflectCacheMap, t reflect.Type, updates reflectCache return record } typeEntry := &TypeReflectCacheEntry{ - isJsonMarshaler: t.Implements(marshalerType), - ptrIsJsonMarshaler: reflect.PtrTo(t).Implements(marshalerType), - isJsonUnmarshaler: reflect.PtrTo(t).Implements(unmarshalerType), - isStringConvertable: t.Implements(unstructuredConvertableType), - ptrIsStringConvertable: reflect.PtrTo(t).Implements(unstructuredConvertableType), + isJsonMarshaler: t.Implements(marshalerType), + ptrIsJsonMarshaler: reflect.PointerTo(t).Implements(marshalerType), + isJsonUnmarshaler: reflect.PointerTo(t).Implements(unmarshalerType), + isUnstructuredConverter: t.Implements(unstructuredConverterType), + ptrIsUnstructuredConverter: reflect.PointerTo(t).Implements(unstructuredConverterType), + isUnstructuredConverterWithError: t.Implements(unstructuredConverterWithErrorType), + ptrIsUnstructuredConverterWithError: reflect.PointerTo(t).Implements(unstructuredConverterWithErrorType), } if t.Kind() == reflect.Struct { fieldEntries := map[string]*FieldCacheEntry{} @@ -190,7 +207,7 @@ func (e TypeReflectCacheEntry) OrderedFields() []*FieldCacheEntry { // CanConvertToUnstructured returns true if this TypeReflectCacheEntry can convert values of its type to unstructured. func (e TypeReflectCacheEntry) CanConvertToUnstructured() bool { - return e.isJsonMarshaler || e.ptrIsJsonMarshaler || e.isStringConvertable || e.ptrIsStringConvertable + return e.isJsonMarshaler || e.ptrIsJsonMarshaler || e.isUnstructuredConverter || e.ptrIsUnstructuredConverter || e.isUnstructuredConverterWithError || e.ptrIsUnstructuredConverterWithError } // ToUnstructured converts the provided value to unstructured and returns it. @@ -206,7 +223,7 @@ func (e TypeReflectCacheEntry) ToUnstructured(sv reflect.Value) (interface{}, er // Check if the object has a custom string converter and use it if available, since it is much more efficient // than round tripping through json. if converter, ok := e.getUnstructuredConverter(sv); ok { - return converter.ToUnstructured(), nil + return converter.ToUnstructuredWithError() } // Check if the object has a custom JSON marshaller/unmarshaller. if marshaler, ok := e.getJsonMarshaler(sv); ok { @@ -318,16 +335,26 @@ func (e TypeReflectCacheEntry) getJsonUnmarshaler(v reflect.Value) (json.Unmarsh return v.Addr().Interface().(json.Unmarshaler), true } -func (e TypeReflectCacheEntry) getUnstructuredConverter(v reflect.Value) (UnstructuredConverter, bool) { - if e.isStringConvertable { - return v.Interface().(UnstructuredConverter), true - } - if e.ptrIsStringConvertable { +type unstructuredConverterWithNilError struct { + UnstructuredConverter +} + +func (c unstructuredConverterWithNilError) ToUnstructuredWithError() (any, error) { + return c.UnstructuredConverter.ToUnstructured(), nil +} + +func (e TypeReflectCacheEntry) getUnstructuredConverter(v reflect.Value) (UnstructuredConverterWithError, bool) { + switch { + case e.isUnstructuredConverterWithError: + return v.Interface().(UnstructuredConverterWithError), true + case e.ptrIsUnstructuredConverterWithError && v.CanAddr(): // Check pointer receivers if v is not a pointer - if v.CanAddr() { - v = v.Addr() - return v.Interface().(UnstructuredConverter), true - } + return v.Addr().Interface().(UnstructuredConverterWithError), true + case e.isUnstructuredConverter: + return unstructuredConverterWithNilError{v.Interface().(UnstructuredConverter)}, true + case e.ptrIsUnstructuredConverter && v.CanAddr(): + // Check pointer receivers if v is not a pointer + return unstructuredConverterWithNilError{v.Addr().Interface().(UnstructuredConverter)}, true } return nil, false } diff --git a/vendor/sigs.k8s.io/structured-merge-diff/v6/value/value.go b/vendor/sigs.k8s.io/structured-merge-diff/v6/value/value.go index 140b99038..167848ebe 100644 --- a/vendor/sigs.k8s.io/structured-merge-diff/v6/value/value.go +++ b/vendor/sigs.k8s.io/structured-merge-diff/v6/value/value.go @@ -118,6 +118,11 @@ func readJSONIter(iter *jsoniter.Iterator) (Value, error) { if iter.Error != nil && iter.Error != io.EOF { return nil, iter.Error } + if iter.WhatIsNext(); iter.Error == nil { + // Piggy back on scanner aware error reporting here. + iter.ReportError("managedFields parsing", "unexpected trailing data after JSON object") + return nil, iter.Error + } return NewValueInterface(v), nil }