diff --git a/AGENTS.md b/AGENTS.md index 1db0b2eb4..af5d2e420 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -437,9 +437,10 @@ The `docs/` directory contains user-facing documentation: - `docs/prompts.md` – MCP Prompts configuration guide - `docs/logging.md` – MCP Logging guide (automatic K8s error logging, secret redaction) - `docs/OTEL.md` – OpenTelemetry observability setup -- `docs/metrics.md` – Metrics toolset (Prometheus / Alertmanager via obs-mcp) -- `docs/tracing.md` – Tracing toolset (Grafana Tempo via obs-mcp) -- `docs/otelcol.md` – OpenTelemetry Collector toolset (component discovery, schemas, and config validation via obs-mcp) +- `docs/observability/metrics.md` – Metrics toolset (Prometheus / Alertmanager via obs-mcp) +- `docs/observability/tracing.md` – Tracing toolset (Grafana Tempo via obs-mcp) +- `docs/observability/logs.md` – Logs toolset (Grafana Loki via obs-mcp) +- `docs/observability/otelcol.md` – OpenTelemetry Collector toolset (component discovery, schemas, and config validation via obs-mcp) - `docs/KIALI.md` – Kiali toolset configuration - `docs/getting-started-kubernetes.md` – Kubernetes ServiceAccount setup - `docs/getting-started-claude-code.md` – Claude Code CLI integration diff --git a/docs/README.md b/docs/README.md index 0c56aa63a..5dfcbb30c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,13 +22,13 @@ Choose the guide that matches your needs: ## Toolset Guides -- **[Metrics](./metrics.md)** - Prometheus and Alertmanager tools (`metrics` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) -- **[Tracing](./tracing.md)** - Grafana Tempo and TraceQL (`traces` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) -- **[OpenTelemetry Collector](./otelcol.md)** - Component discovery, schemas, and config validation (`otelcol` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) +- **[Metrics](./observability/metrics.md)** - Prometheus and Alertmanager tools (`metrics` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) +- **[Tracing](./observability/tracing.md)** - Grafana Tempo and TraceQL (`traces` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) +- **[Logs](./observability/logs.md)** - Grafana Loki and LogQL (`logs` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) +- **[OpenTelemetry Collector](./observability/otelcol.md)** - Component discovery, schemas, and config validation (`otelcol` toolset, via [obs-mcp](https://github.com/rhobs/obs-mcp)) - **[OADP](OADP.md)** - Tools for OpenShift API for Data Protection (Velero backups, restores, schedules) - **[Kiali](KIALI.md)** - Tools for Kiali ServiceMesh with Istio - **[KubeVirt](kubevirt.md)** - KubeVirt virtual machine management tools -- **[Observability](OBSERVABILITY.md)** - Tools for Prometheus metrics and Alertmanager alerts ## Feature Specifications diff --git a/docs/observability/logs.md b/docs/observability/logs.md new file mode 100644 index 000000000..38135cb96 --- /dev/null +++ b/docs/observability/logs.md @@ -0,0 +1,176 @@ +# Logs Toolset (`logs`) + +This toolset provides tools for querying [Grafana Loki](https://grafana.com/oss/loki/) using LogQL and the Loki HTTP API. +It is implemented by the [`rhobs/obs-mcp`](https://github.com/rhobs/obs-mcp) package and registered into the openshift-mcp-server as the `logs` toolset. + +For Prometheus and Alertmanager MCP tools, see the [metrics toolset guide](./metrics.md). +For Grafana Tempo and TraceQL (`traces` toolset), see the [tracing toolset guide](./tracing.md). +For OpenTelemetry Collector configuration assistance (`otelcol` toolset), see the [otelcol toolset guide](./otelcol.md). + +## Workflow + +1. Call **`loki_list_instances`** first to discover `LokiStack` instances, namespaces, multitenancy, and tenant names. +2. Use **`loki_label_names`** (and optionally **`loki_label_values`**) to learn which labels exist before writing LogQL queries. +3. Run **`loki_query_range`** with a LogQL query to retrieve matching log streams and lines. + +## Tools + +### loki_list_instances + +**Discovery entry point.** Lists LokiStack instances visible in the Kubernetes API. + +**Parameters:** none. + +**Output:** JSON per instance includes `lokiNamespace`, `lokiName`, `status`, and resolved `url`. Use `lokiNamespace`, `lokiName`, and `tenant` as parameters on other Loki tools. + +--- + +### loki_label_names + +List available Loki label names for a time range. Use this before writing LogQL queries to discover which labels are indexed. + +**Parameters:** +- `lokiNamespace` (string, optional) — Kubernetes namespace of the LokiStack (from `loki_list_instances`) +- `lokiName` (string, optional) — Name of the LokiStack (from `loki_list_instances`) +- `tenant` (string, optional) — Loki tenant ID; for LokiStack gateway modes (e.g. openshift-network) use `network` +- `start` (string, optional) — Start time (RFC3339, Unix timestamp, `NOW`, or relative like `NOW-1h`) +- `end` (string, optional) — End time (RFC3339, Unix timestamp, `NOW`, or relative) + +--- + +### loki_label_values + +List possible values for a Loki label key. Use this to build precise label matchers in LogQL. + +**Parameters:** +- `label` (string, required) — Label key to inspect (e.g. `namespace`, `pod`, `container`, `SrcK8S_Namespace`) +- `lokiNamespace`, `lokiName`, `tenant`, `start`, `end` — same as `loki_label_names` + +--- + +### loki_query_range + +Execute a Loki LogQL range query and return matching log streams and lines. + +**Parameters:** +- `query` (string, required) — LogQL query string (e.g. `{namespace="default"}`) +- `lokiNamespace` (string, optional) — Kubernetes namespace of the LokiStack +- `lokiName` (string, optional) — Name of the LokiStack +- `tenant` (string, optional) — Loki tenant ID +- `duration` (string, optional) — Lookback duration from now when start/end are omitted (e.g. `5m`, `1h`). Defaults to `15m` +- `start` (string, optional) — Start time (RFC3339, Unix, `NOW`, or relative) +- `end` (string, optional) — End time (RFC3339, Unix, `NOW`, or relative) +- `limit` (number, optional) — Maximum number of log lines to return. Defaults to 100, max 1000 +- `direction` (string, optional) — Search direction: `backward` (default) or `forward` + +--- + +## Enable the Toolset + +### Command line + +```bash +kubernetes-mcp-server --toolsets core,logs +``` + +### Configuration file (TOML) + +```toml +toolsets = ["core", "logs"] +``` + +### MCP client configuration + +```json +{ + "mcpServers": { + "kubernetes": { + "command": "npx", + "args": ["-y", "kubernetes-mcp-server@latest", "--toolsets", "core,logs"] + } + } +} +``` + +You can enable **`metrics`**, **`traces`**, and **`logs`** together (same obs-mcp dependency, different toolsets): + +```toml +toolsets = ["core", "metrics", "traces", "logs"] +``` + +--- + +## Configuration + +Optional settings use a **`[toolset_configs.logs]`** section (the key is the toolset name `logs`). + +```toml +[toolset_configs.logs] +# Where to read the bearer token from: "header" (default) or "kubeconfig". +# Set to "kubeconfig" when running locally (STDIO mode) so the token is read +# from your kubeconfig session (e.g. after `oc login`). +auth_mode = "kubeconfig" + +# URL of the Loki API endpoint. +# Optional — if unset, use LokiStack discovery (loki_list_instances + lokiNamespace/lokiName). +# Example for a direct Loki endpoint: +# loki_url = "https://logging-loki-gateway-http.openshift-logging.svc.cluster.local:8080" +loki_url = "" + +# Skip TLS certificate verification (development only). Default: false +insecure = false + +# Resolve Loki query URLs via OpenShift Routes instead of in-cluster Services. +# Default: false +useRoute = false +``` + +### Configuration reference + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `auth_mode` | string | `"header"` | Bearer token source: `"header"` or `"kubeconfig"` | +| `loki_url` | string | — | Loki API endpoint URL (optional; use LokiStack discovery if unset) | +| `insecure` | bool | `false` | Skip TLS certificate verification | +| `useRoute` | bool | `false` | Use OpenShift `Route` resources for LokiStack gateway URLs | + +--- + +## Authentication and TLS + +Bearer token behavior matches the [metrics toolset](./metrics.md) (**Authentication and TLS** section): `auth_mode` chooses header vs kubeconfig, and TLS uses kubeconfig CA data, OpenShift service CA when in-cluster, then the system trust store. Set `insecure = true` only when you cannot install the correct CA (not recommended in production). + +### Loki URL resolution + +When the `logs` toolset is enabled, the Loki URL is determined in this order: + +1. `loki_url` in the `[toolset_configs.logs]` config section (if set) +2. `LOKI_URL` environment variable +3. Default: `http://localhost:3100` (kubeconfig mode only) + +In `header` mode, you can either set `loki_url` **or** use LokiStack discovery (`loki_list_instances` + `lokiNamespace`/`lokiName` arguments on each tool call). + +--- + +## Instance discovery + +The server lists **`LokiStack`** objects cluster-wide and derives gateway base URLs from each resource. With **`useRoute = true`**, it prefers OpenShift `Route` hosts where available. + +Chosen instances are **validated** against this discovery list before any request is sent, so callers cannot point tools at arbitrary URLs. + +--- + +## Prerequisites + +- **Loki Operator** workloads in the cluster (`LokiStack` CRs) or a standalone Loki endpoint. +- **RBAC** on the MCP identity to **list** `LokiStack` objects cluster-wide. If **`useRoute`** is enabled, the server also **gets** `Route` resources in each Loki namespace to resolve external hosts. +- **Bearer token** with permission to reach the resolved Loki API (same patterns as the metrics toolset). + +--- + +## Related documentation + +- [Metrics toolset guide](./metrics.md) — Prometheus and Alertmanager (`metrics` toolset) +- [Tracing toolset guide](./tracing.md) — Grafana Tempo and TraceQL (`traces` toolset) +- [OpenTelemetry Collector toolset guide](./otelcol.md) — Component discovery, schemas, config validation (`otelcol` toolset) +- [OTEL.md](../OTEL.md) — OpenTelemetry export from this MCP server process (not the same as querying Loki in-cluster) diff --git a/docs/metrics.md b/docs/observability/metrics.md similarity index 99% rename from docs/metrics.md rename to docs/observability/metrics.md index 6ca184819..8ddf315b4 100644 --- a/docs/metrics.md +++ b/docs/observability/metrics.md @@ -4,6 +4,7 @@ This toolset provides tools for querying Prometheus/Thanos metrics and Alertmana It is implemented by the [`rhobs/obs-mcp`](https://github.com/rhobs/obs-mcp) package and registered into the openshift-mcp-server as the `metrics` toolset. +For Grafana Loki and LogQL (`logs` toolset), see the [logs toolset guide](./logs.md). For Grafana Tempo and TraceQL (`traces` toolset), see the [tracing toolset guide](./tracing.md). For OpenTelemetry Collector configuration assistance (`otelcol` toolset), see the [otelcol toolset guide](./otelcol.md). diff --git a/docs/otelcol.md b/docs/observability/otelcol.md similarity index 93% rename from docs/otelcol.md rename to docs/observability/otelcol.md index aed91e89a..56d64d440 100644 --- a/docs/otelcol.md +++ b/docs/observability/otelcol.md @@ -9,6 +9,7 @@ Component schemas are embedded in the binary (via `redhat-opentelemetry-collecto running Collector instance or cluster endpoint is required. For Prometheus and Alertmanager MCP tools, see the [metrics toolset guide](./metrics.md). +For Grafana Loki and LogQL (`logs` toolset), see the [logs toolset guide](./logs.md). For Grafana Tempo and TraceQL, see the [tracing toolset guide](./tracing.md). ## Workflow @@ -127,5 +128,6 @@ No Prometheus, Tempo, or Collector endpoint URLs are needed. ## Related documentation - [Metrics toolset guide](./metrics.md) — Prometheus and Alertmanager (`metrics` toolset) +- [Logs toolset guide](./logs.md) — Grafana Loki and LogQL (`logs` toolset) - [Tracing toolset guide](./tracing.md) — Grafana Tempo and TraceQL (`traces` toolset) -- [OTEL.md](OTEL.md) — OpenTelemetry export from this MCP server process (not the same as Collector config assistance) +- [OTEL.md](../OTEL.md) — OpenTelemetry export from this MCP server process (not the same as Collector config assistance) diff --git a/docs/tracing.md b/docs/observability/tracing.md similarity index 96% rename from docs/tracing.md rename to docs/observability/tracing.md index 219d61b18..6d0f04544 100644 --- a/docs/tracing.md +++ b/docs/observability/tracing.md @@ -4,6 +4,7 @@ This toolset provides tools for querying [Grafana Tempo](https://grafana.com/doc It is implemented by the [`rhobs/obs-mcp`](https://github.com/rhobs/obs-mcp) package and registered into the openshift-mcp-server as the `traces` toolset. For Prometheus and Alertmanager MCP tools, see the [metrics toolset guide](./metrics.md). +For Grafana Loki and LogQL (`logs` toolset), see the [logs toolset guide](./logs.md). For OpenTelemetry Collector configuration assistance (`otelcol` toolset), see the [otelcol toolset guide](./otelcol.md). ## Workflow @@ -163,4 +164,4 @@ Chosen instances are **validated** against this discovery list before any reques ## Related documentation - [Metrics toolset guide](./metrics.md) — Prometheus and Alertmanager (`metrics` toolset) -- [OTEL.md](OTEL.md) — OpenTelemetry export from this MCP server process (not the same as querying Tempo in-cluster) +- [OTEL.md](../OTEL.md) — OpenTelemetry export from this MCP server process (not the same as querying Tempo in-cluster) diff --git a/evals/tasks/observability/logs/loki-backend-reachability.yaml b/evals/tasks/observability/logs/loki-backend-reachability.yaml new file mode 100644 index 000000000..54d02fd06 --- /dev/null +++ b/evals/tasks/observability/logs/loki-backend-reachability.yaml @@ -0,0 +1,23 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: loki-backend-reachability + difficulty: easy + parallel: true + runs: 1 + labels: + category: logs + suite: observability + toolType: smoke-test + description: | + Smoke test that the agent can reach Loki via loki_list_instances and report + a discovered LokiStack. Run obs-mcp with --toolsets logs (or metrics,traces,logs). +spec: + prompt: + inline: | + Is the Loki backend reachable? List LokiStack instances and report the + name, namespace, and URL of any stack you find. + verify: + - llmJudge: + contains: "obs-mcp-loki" + reason: "Verify the agent discovered the obs-mcp-loki LokiStack from loki_list_instances" diff --git a/evals/tasks/observability/logs/loki-label-names.yaml b/evals/tasks/observability/logs/loki-label-names.yaml new file mode 100644 index 000000000..b62cc0074 --- /dev/null +++ b/evals/tasks/observability/logs/loki-label-names.yaml @@ -0,0 +1,26 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: loki-label-names + difficulty: medium + parallel: true + runs: 1 + labels: + category: logs + suite: observability + toolType: exploration + description: | + Tests discovery workflow: loki_list_instances then loki_label_names with tenant + network on the obs-mcp-loki stack (openshift-network mode). +spec: + prompt: + inline: | + For LokiStack obs-mcp-loki in namespace obs-mcp-loki, tenant network, what + label names are available for writing LogQL queries? + verify: + - llmJudge: + contains: "SrcK8S_Namespace" + reason: "NetObserv flow logs expose SrcK8S_Namespace as an indexed Loki label" + - llmJudge: + contains: "DstK8S_Namespace" + reason: "NetObserv flow logs expose DstK8S_Namespace as an indexed Loki label" diff --git a/evals/tasks/observability/logs/loki-label-values.yaml b/evals/tasks/observability/logs/loki-label-values.yaml new file mode 100644 index 000000000..23ad3dece --- /dev/null +++ b/evals/tasks/observability/logs/loki-label-values.yaml @@ -0,0 +1,22 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: loki-label-values + difficulty: medium + parallel: true + runs: 1 + labels: + category: logs + suite: observability + toolType: exploration + description: | + Tests loki_label_values for SrcK8S_Namespace on the network tenant. +spec: + prompt: + inline: | + For LokiStack obs-mcp-loki in namespace obs-mcp-loki with tenant network, + what values exist for the SrcK8S_Namespace label? + verify: + - llmJudge: + contains: "SrcK8S_Namespace" + reason: "Verify the agent queried the SrcK8S_Namespace label" diff --git a/evals/tasks/observability/logs/loki-list-instances.yaml b/evals/tasks/observability/logs/loki-list-instances.yaml new file mode 100644 index 000000000..be8f1399b --- /dev/null +++ b/evals/tasks/observability/logs/loki-list-instances.yaml @@ -0,0 +1,21 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: loki-list-instances + difficulty: easy + parallel: true + runs: 1 + labels: + category: logs + suite: observability + toolType: discovery + description: | + Tests that the agent calls loki_list_instances before other Loki tools. +spec: + prompt: + inline: | + Which LokiStack instances are available in this cluster? + verify: + - llmJudge: + contains: "obs-mcp-loki" + reason: "Verify the agent reported LokiStack instance details" diff --git a/evals/tasks/observability/logs/loki-query-network-flows.yaml b/evals/tasks/observability/logs/loki-query-network-flows.yaml new file mode 100644 index 000000000..04c53c2e0 --- /dev/null +++ b/evals/tasks/observability/logs/loki-query-network-flows.yaml @@ -0,0 +1,27 @@ +kind: Task +apiVersion: mcpchecker/v1alpha2 +metadata: + name: loki-query-network-flows + difficulty: medium + parallel: true + runs: 1 + labels: + category: logs + suite: observability + toolType: query + description: | + Tests loki_query_range with NetObserv flow log labels (SrcK8S_Namespace / + DstK8S_Namespace) and tenant network—not kubernetes_namespace_name. +spec: + prompt: + inline: | + Query NetObserv network flow logs from the last hour where the source or + destination namespace is obs-mcp-loki. Use LokiStack obs-mcp-loki in namespace + obs-mcp-loki with tenant network. + verify: + - llmJudge: + contains: "SrcK8S_Namespace" + reason: "Verify the agent used obs-mcp-loki indexed namespace labels in LogQL" + - llmJudge: + contains: "network" + reason: "Verify the agent used tenant network for the openshift-network LokiStack" diff --git a/go.mod b/go.mod index 99fda08c2..16d0c66cc 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/miekg/dns v1.1.72 github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/prometheus/client_golang v1.23.2 - github.com/rhobs/obs-mcp v0.3.0 + github.com/rhobs/obs-mcp v0.4.0 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -38,16 +38,16 @@ require ( google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.21.0 - k8s.io/api v0.36.1 + k8s.io/api v0.36.2 k8s.io/apiextensions-apiserver v0.36.1 - k8s.io/apimachinery v0.36.1 - k8s.io/cli-runtime v0.36.1 - k8s.io/client-go v0.36.1 + k8s.io/apimachinery v0.36.2 + 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/kubectl v0.36.1 - k8s.io/metrics v0.36.1 - k8s.io/streaming v0.36.1 + 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 sigs.k8s.io/controller-runtime v0.24.1 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.24.1 @@ -90,24 +90,24 @@ require ( github.com/go-openapi/errors v0.22.8 // indirect github.com/go-openapi/jsonpointer v0.23.1 // indirect github.com/go-openapi/jsonreference v0.21.6 // indirect - github.com/go-openapi/loads v0.23.4 // indirect + github.com/go-openapi/loads v0.24.0 // indirect github.com/go-openapi/runtime v0.32.3 // indirect github.com/go-openapi/runtime/server-middleware v0.32.3 // indirect - github.com/go-openapi/spec v0.22.5 // indirect + github.com/go-openapi/spec v0.22.6 // indirect github.com/go-openapi/strfmt v0.26.3 // indirect - github.com/go-openapi/swag v0.26.0 // indirect - github.com/go-openapi/swag/cmdutils v0.26.0 // indirect - github.com/go-openapi/swag/conv v0.26.0 // indirect - github.com/go-openapi/swag/fileutils v0.26.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-openapi/swag/jsonutils v0.26.0 // indirect - github.com/go-openapi/swag/loading v0.26.0 // indirect - github.com/go-openapi/swag/mangling v0.26.0 // indirect - github.com/go-openapi/swag/netutils v0.26.0 // indirect - github.com/go-openapi/swag/stringutils v0.26.0 // indirect - github.com/go-openapi/swag/typeutils v0.26.0 // indirect - github.com/go-openapi/swag/yamlutils v0.26.0 // indirect - github.com/go-openapi/validate v0.25.3 // indirect + github.com/go-openapi/swag v0.26.1 // indirect + github.com/go-openapi/swag/cmdutils v0.26.1 // indirect + github.com/go-openapi/swag/conv v0.26.1 // indirect + github.com/go-openapi/swag/fileutils v0.26.1 // indirect + github.com/go-openapi/swag/jsonname v0.26.1 // indirect + github.com/go-openapi/swag/jsonutils v0.26.1 // indirect + github.com/go-openapi/swag/loading v0.26.1 // indirect + github.com/go-openapi/swag/mangling v0.26.1 // indirect + github.com/go-openapi/swag/netutils v0.26.1 // indirect + github.com/go-openapi/swag/stringutils v0.26.1 // indirect + github.com/go-openapi/swag/typeutils v0.26.1 // indirect + github.com/go-openapi/swag/yamlutils v0.26.1 // indirect + github.com/go-openapi/validate v0.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect @@ -146,15 +146,15 @@ require ( github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260603165435-b2d908d32435 // indirect + github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260611132535-04e24ebf54ab // indirect github.com/pavolloffay/opentelemetry-mcp-server/modules/collectorschema v0.0.0-20260520093054-4540dfe82192 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/philippgille/chromem-go v0.7.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/alertmanager v0.32.1 // indirect + github.com/prometheus/alertmanager v0.33.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.68.1 // indirect + github.com/prometheus/common v0.69.0 // indirect github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/prometheus v0.312.0 // indirect @@ -178,13 +178,13 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.53.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect @@ -193,7 +193,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/apiserver v0.36.1 // indirect - k8s.io/component-base v0.36.1 // indirect + k8s.io/component-base v0.36.2 // indirect knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/go.sum b/go.sum index 40d82e0fa..a6f8902bf 100644 --- a/go.sum +++ b/go.sum @@ -171,48 +171,48 @@ github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0r github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y= github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY= -github.com/go-openapi/loads v0.23.4 h1:UMC8JClHQeASS+bh1Uc8ShGG6IrKt1kbM2DgFhx/vF0= -github.com/go-openapi/loads v0.23.4/go.mod h1:oXw5oD+IGqI5BdfQgN7y9OXR8JhsAfEDpwWKxpGzeno= +github.com/go-openapi/loads v0.24.0 h1:4LLorXRPTzIN9V6ngMUZbAscsBOUBk3Oa8cClu/bFrQ= +github.com/go-openapi/loads v0.24.0/go.mod h1:xQMgX+hw5xRAhGrcDXxeMw78IFqUpIzhleu3HqPhyF4= github.com/go-openapi/runtime v0.32.3 h1:J7Ycy5DJmhhP1By3NifhRUjnkXTrk21qbeqSULjwX8U= github.com/go-openapi/runtime v0.32.3/go.mod h1:/WTQi0fa5DiGnnCXQKsTkSm15OzJp8Uz3H2t+67TBr4= github.com/go-openapi/runtime/server-middleware v0.32.3 h1:Y/6h9ix9NCoMG04XazRwX6eA3alh4+JZ6qXdar5yd24= github.com/go-openapi/runtime/server-middleware v0.32.3/go.mod h1:fYPep4GdTwg/XqZUjR40uIM/8C12Ba5M+MrGCiwpTHo= -github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo= -github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74= +github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic= +github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM= github.com/go-openapi/strfmt v0.26.3 h1:rzmslHarJgBbf2qfGge+X3htclQfmXqBZMm0Too0HhU= github.com/go-openapi/strfmt v0.26.3/go.mod h1:a5nsUw0oRpQzZeOwx8bi6cKbzFZslpbCKt1LEot+KnQ= -github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= -github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= -github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= -github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= -github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= -github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= -github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= -github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= -github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= -github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= -github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= -github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= -github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= -github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= -github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= -github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= -github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= -github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= -github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= -github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= -github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= +github.com/go-openapi/swag v0.26.1 h1:l5sVEyVpwj+DDYeZyo7wQI/Ebn/mKYIyGB/pFwAfGoQ= +github.com/go-openapi/swag v0.26.1/go.mod h1:yNY38BbIVthxbkDtq1UHBCGasBqjakW3lCR6ANzdBEw= +github.com/go-openapi/swag/cmdutils v0.26.1 h1:f2iE1ijYaJ3nuu5PaEMx3zpEhzhZFgivCJObWEObLIQ= +github.com/go-openapi/swag/cmdutils v0.26.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg= +github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A= +github.com/go-openapi/swag/fileutils v0.26.1 h1:K1XCM2CGhfNsc6YDt6v7Q5+1e59rftYWdcu/isZhvFw= +github.com/go-openapi/swag/fileutils v0.26.1/go.mod h1:mYUgxQAKX4ShS3qvvySx+/9yrlUnDhjiD1CalaQl8lQ= +github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE= +github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc= +github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY= +github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc= +github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w= +github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E= +github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM= +github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM= +github.com/go-openapi/swag/netutils v0.26.1 h1:BNctoc39WTAUMxyAs355fExOPzMZtPbZ0ZZ1Am2FR5M= +github.com/go-openapi/swag/netutils v0.26.1/go.mod h1:y02vByhZhQPAVwOX+0KipXFZ/hUbk6G/Enhf5rGaOkQ= +github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU= +github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk= +github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU= +github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o= +github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk= +github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M= github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8= github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk= github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo= github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/go-openapi/validate v0.25.3 h1:4nzAIavcJ7WveHK2+V1UAkZK3kWcjzxZCzjfZAfavKs= -github.com/go-openapi/validate v0.25.3/go.mod h1:GemfuGMyYpIaBoKpX3z8sLywrmxpzWVOoJ7R0VeAVuk= +github.com/go-openapi/validate v0.26.0 h1:dxWzQ3F+vb1SajqUxHjwb5T4mTpSHmdrtv5Bi7+ZNhw= +github.com/go-openapi/validate v0.26.0/go.mod h1:b4o00uq7fJeJA+wWhVFCJpKTctzeFwzZImGGmHsl2JA= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -344,8 +344,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 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-20260603165435-b2d908d32435 h1:QxOm58H4ez7Loxcx0PXYcmux+2nXQjym43Ym9Riu5G4= -github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260603165435-b2d908d32435/go.mod h1:ThsbZTM538g95wb3Aj/+XHfFfpW/YBHdO5o6W0f+IRw= +github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260611132535-04e24ebf54ab h1:4HWk+v8GBIJyPLP4mRGfvncjmDbtqxK8gfvAo76CDQw= +github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260611132535-04e24ebf54ab/go.mod h1:ThsbZTM538g95wb3Aj/+XHfFfpW/YBHdO5o6W0f+IRw= github.com/pavolloffay/opentelemetry-mcp-server/modules/collectorschema v0.0.0-20260520093054-4540dfe82192 h1:BSUqid8PYcMHLM1mvYdsHgO0vSaj5YL346Dw1fU9IGA= github.com/pavolloffay/opentelemetry-mcp-server/modules/collectorschema v0.0.0-20260520093054-4540dfe82192/go.mod h1:8O9OWWUERoegxTzJcN0c/613/qzTBkoDESFJiE/dLxg= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -364,16 +364,16 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/alertmanager v0.32.1 h1:BQ3jHXNq2A7VSD9Kh0Qx+kXbifNbHSDuKVbMmdRHHJ0= -github.com/prometheus/alertmanager v0.32.1/go.mod h1:0Dy9faTtMgpVYxJVxV0o65elTxHnSRCF/7gy5BKGZiE= +github.com/prometheus/alertmanager v0.33.0 h1:AAVa3wpCsaDxisTUUPXx+1qhnA2mx0f8Cc+smpAtN7w= +github.com/prometheus/alertmanager v0.33.0/go.mod h1:V06Uc8EZ5X5wLOJRGhtXx+EE2LgrinFIADbKWMVm1RY= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_golang/exp v0.0.0-20260518105423-c9d5bc4c50a9 h1:e33IfrrwrJkylWwAGcQ2jMvbWVv13lv0suTXjGNeiqY= github.com/prometheus/client_golang/exp v0.0.0-20260518105423-c9d5bc4c50a9/go.mod h1:vW/EVguzbNw6xMRmozJQWbY60/+Zsg0TgVJOSXGx2iI= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= @@ -388,8 +388,8 @@ github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/rhobs/obs-mcp v0.3.0 h1:1E/ZesxcDZYuf4gfBJAyS2nOnK+DfMJZiRAoroHbzHM= -github.com/rhobs/obs-mcp v0.3.0/go.mod h1:INO5QXD4/OzdtVNivOrpeEHV+x+qNSEYmy0RNZiehpc= +github.com/rhobs/obs-mcp v0.4.0 h1:yZvBBge2Qs5NP4gKP6QBYUTZdU4ZS9NyBg40wB5Q/IM= +github.com/rhobs/obs-mcp v0.4.0/go.mod h1:8Kz/5Mh7prWu9k0bTsGnoipDPo6skiFoQe7kxV1tWDQ= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -503,26 +503,26 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= @@ -555,30 +555,30 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= helm.sh/helm/v3 v3.21.0 h1:9TRbaXQH+BIKLLDYlu++JsyWodS5kBBOLF7C7HY5+cs= helm.sh/helm/v3 v3.21.0/go.mod h1:5IvU6Ae6ruB/vasVHhnC1IU5RvqFM349vLYS1BiHqeY= -k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= -k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= -k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= -k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= k8s.io/apiserver v0.36.1 h1:iMS5V+rPUertv5P9RaqJgmHHTuh4quWpoxchvMUY+JY= k8s.io/apiserver v0.36.1/go.mod h1:Cby1PbLWztu0GDOxoO6iFOyyqIsziHNEW+w9zVQ22Kw= -k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM= -k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw= -k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= -k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= -k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= -k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= +k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM= +k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +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/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= -k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= -k8s.io/metrics v0.36.1 h1:MQPb+G4RhrKEpt8NETPssbW8QgGUc4Jbqu1jx+kPqGk= -k8s.io/metrics v0.36.1/go.mod h1:xqS8XcWLjDzo6E7DJm/GfjKpRKdN5/MtJAQFuV6nLUc= -k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4= -k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +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= knative.dev/pkg v0.0.0-20260318013857-98d5a706d4fd h1:yeh+smYaouOwhkyCPj+AYACt1MeD+EI4mXSzSbmtj10= diff --git a/vendor/github.com/go-openapi/loads/.gitignore b/vendor/github.com/go-openapi/loads/.gitignore index d8f4186fe..fbb78de2c 100644 --- a/vendor/github.com/go-openapi/loads/.gitignore +++ b/vendor/github.com/go-openapi/loads/.gitignore @@ -3,3 +3,4 @@ .idea .env .mcp.json +.worktrees diff --git a/vendor/github.com/go-openapi/loads/.golangci.yml b/vendor/github.com/go-openapi/loads/.golangci.yml index 83968f3fa..272b14e54 100644 --- a/vendor/github.com/go-openapi/loads/.golangci.yml +++ b/vendor/github.com/go-openapi/loads/.golangci.yml @@ -7,6 +7,8 @@ linters: - gochecknoglobals # on this repo, it is hard to refactor without globals/inits and no breaking change - gochecknoinits - godox + - gomodguard + - gomodguard_v2 - exhaustruct - nlreturn - nonamedreturns diff --git a/vendor/github.com/go-openapi/loads/CONTRIBUTORS.md b/vendor/github.com/go-openapi/loads/CONTRIBUTORS.md index d9481e787..6ab26b8dd 100644 --- a/vendor/github.com/go-openapi/loads/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/loads/CONTRIBUTORS.md @@ -4,11 +4,11 @@ | Total Contributors | Total Contributions | | --- | --- | -| 14 | 130 | +| 14 | 133 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | -| @fredbi | 52 | | +| @fredbi | 55 | | | @casualjim | 48 | | | @youyuanwu | 6 | | | @vburenin | 4 | | diff --git a/vendor/github.com/go-openapi/loads/README.md b/vendor/github.com/go-openapi/loads/README.md index a67a1546e..293f79bb6 100644 --- a/vendor/github.com/go-openapi/loads/README.md +++ b/vendor/github.com/go-openapi/loads/README.md @@ -20,12 +20,9 @@ Supports JSON and YAML documents. * **2025-12-19** : new community chat on discord * a new discord community channel is available to be notified of changes and support users - * our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31** You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url] -Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] - ## Status API is stable. @@ -58,6 +55,41 @@ go get github.com/go-openapi/loads See also the provided [examples](https://pkg.go.dev/github.com/go-openapi/loads#pkg-examples). +## Security + +This library does not enforce a security policy of its own: it reads whatever the configured +loader is allowed to read. + +This is deliberate — like `go-openapi/swag/loading`, it is a base utility, +and sanitizing or containing untrusted input is the caller's responsibility, +just as sanitizing a file name before passing it to `os.ReadFile` is not that function's job. + +When a spec — its path or its `$ref` contents — may come from an untrusted source, confine +loading explicitly (e.g. `loading.WithRoot` for local files and a restricted +`loading.WithHTTPClient` for remote URLs, passed via `loads.WithLoadingOptions`). + +For the common case, the pre-baked `loads.SpecRestricted` / `loads.JSONSpecRestricted` loaders +bundle a trusted root with a network-restricted client (`loads.RestrictedHTTPClient`) and apply +the confinement to `$ref` resolution as well: + +```go +doc, err := loads.SpecRestricted(path, trustedRoot) +``` + +To harden the package-level default in one call — so even callers that rely on the global +loader (including cross-package `$ref` resolution via `spec.PathLoader`) are confined, with no +unconfined fallback left — use `loads.SetRestrictedLoaders` at startup: + +```go +loads.SetRestrictedLoaders(trustedRoot) +``` + +Note that `loads.AddLoader` only *prepends* to the default chain, leaving the unconfined loader +reachable; use `loads.SetLoaders` / `loads.SetRestrictedLoaders` to replace it. + +See the [Security section of the package documentation][security-doc] for the threat model and +runnable examples. For the project's vulnerability reporting policy, see [SECURITY.md](./SECURITY.md). + ## Change log See @@ -69,9 +101,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). ## Other documentation * [All-time contributors](./CONTRIBUTORS.md) -* [Contributing guidelines](.github/CONTRIBUTING.md) -* [Maintainers documentation](docs/MAINTAINERS.md) -* [Code style](docs/STYLE.md) +* [Contributing guidelines][contributing-doc-site] +* [Maintainers documentation][maintainers-doc-site] +* [Code style][style-doc-site] ## Cutting a new release @@ -102,9 +134,6 @@ Maintainers can cut a new release by either: [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/loads [godoc-url]: http://pkg.go.dev/github.com/go-openapi/loads -[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png -[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM -[slack-url]: https://goswagger.slack.com/archives/C04R30YMU [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 @@ -116,3 +145,9 @@ Maintainers can cut a new release by either: [goversion-url]: https://github.com/go-openapi/loads/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/loads [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/loads/latest + +[security-doc]: https://pkg.go.dev/github.com/go-openapi/loads#hdr-Security + +[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html +[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html +[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html diff --git a/vendor/github.com/go-openapi/loads/doc.go b/vendor/github.com/go-openapi/loads/doc.go index 67a5e2f8d..0fafe4f46 100644 --- a/vendor/github.com/go-openapi/loads/doc.go +++ b/vendor/github.com/go-openapi/loads/doc.go @@ -6,4 +6,72 @@ // It is used by other go-openapi packages to load and run analysis on local or remote spec documents. // // Loaders support JSON and YAML documents. +// +// # Security +// +// This package does not enforce a security policy of its own: like the underlying +// [github.com/go-openapi/swag/loading] utilities, it reads whatever the configured loader is +// allowed to read. +// +// When a spec — its path or its contents — may derive from untrusted input, the caller must confine loading explicitly. +// +// This is a deliberate design choice. +// Both this package and the [github.com/go-openapi/swag/loading] utilities are base building blocks: +// deciding which sources are legitimate, and containing access to them, +// requires application context that a general-purpose loader does not have. +// +// Just as sanitizing a file name before handing it to [os.ReadFile] is the caller's +// responsibility and not that function's, sanitizing and containing the path and references +// resolved here is the responsibility of the code that may feed them untrusted input. +// +// There are two distinct attack surfaces: +// +// - The path passed to [Spec], [JSONSpec], or [Embedded]. By default a local path is read +// with no confinement, so a caller-controlled path (including an absolute path or a +// "file:///etc/passwd" URI) may read any file the process can access. A remote path is +// fetched with [net/http.DefaultClient], which follows redirects and performs no +// destination filtering, so a caller-controlled URL may reach internal services or cloud +// metadata endpoints (server-side request forgery). +// +// - The contents of the spec, when references are resolved. [Document.Expanded] follows the +// "$ref" pointers found inside the document by calling the same loader recursively. A spec +// obtained even from a trusted path can therefore drive arbitrary local reads +// ("$ref": "file:///etc/passwd") or SSRF ("$ref": "http://169.254.169.254/...") through +// its own contents. This amplification is specific to reference resolution and does not +// exist in the raw loading utilities. +// +// Mitigation. Pass [github.com/go-openapi/swag/loading] options through [WithLoadingOptions]; +// they are attached to the document's loader and so apply both to the initial load and to +// every "$ref" resolved during expansion: +// +// - [github.com/go-openapi/swag/loading.WithRoot] confines local reads to a trusted +// directory, rejecting absolute paths, ".." traversal, and symlinks that escape it. Prefer +// it over a [github.com/go-openapi/swag/loading.WithFS] built from [os.DirFS], which does +// not block symlink escapes. +// +// - [github.com/go-openapi/swag/loading.WithHTTPClient] allows to supply a restricted HTTP client. +// Enforce the network policy at dial time (a [net.Dialer] Control hook), so it also covers +// redirects and DNS rebinding, which a URL-string allowlist cannot. See the example on +// [Spec]. +// +// Pre-baked loaders. When the opinionated defaults fit, [SpecRestricted], [JSONSpecRestricted] +// and [JSONDocRestricted] bundle a trusted root with a network-restricted client +// ([RestrictedHTTPClient]), and apply the confinement to "$ref" resolution as well — so the +// common case needs no manual wiring. To harden the global default in one call (so even callers +// that rely on the package-level loader are confined), use [SetRestrictedLoaders]. Reach for the +// options above when you need a custom policy; [IsForbiddenAddress] exposes the default network +// policy so you can reuse it as the base of your own HTTP client. +// +// Caveats: +// +// - The package-level default loader (also installed as [github.com/go-openapi/spec.PathLoader]) +// carries no loading options and is therefore unconfined. It is used as a fallback when +// expansion runs without a document loader, and by other go-openapi packages that resolve +// references on their own. [AddLoader] does not fix this — it only prepends, leaving the +// unconfined fallback reachable. Either build a confined loader per call, or replace the +// global default outright with [SetLoaders] / [SetRestrictedLoaders]. +// +// - A custom loader installed via [WithDocLoader] or [AddLoader] only honors these +// protections if its loading function actually applies the [github.com/go-openapi/swag/loading] +// options it is given. package loads diff --git a/vendor/github.com/go-openapi/loads/errors.go b/vendor/github.com/go-openapi/loads/errors.go index 14a8186b6..e94f038f9 100644 --- a/vendor/github.com/go-openapi/loads/errors.go +++ b/vendor/github.com/go-openapi/loads/errors.go @@ -15,4 +15,8 @@ const ( // ErrNoLoader indicates that no configured loader matched the input. ErrNoLoader loaderError = "no loader matched" + + // ErrForbiddenAddress is returned by [RestrictedHTTPClient] when a connection is attempted + // to a non-public address (loopback, private, link-local, or unspecified). + ErrForbiddenAddress loaderError = "blocked dial to a non-public address" ) diff --git a/vendor/github.com/go-openapi/loads/loaders.go b/vendor/github.com/go-openapi/loads/loaders.go index ac8adfe8b..f8a2a9438 100644 --- a/vendor/github.com/go-openapi/loads/loaders.go +++ b/vendor/github.com/go-openapi/loads/loaders.go @@ -21,6 +21,15 @@ import ( var loaders *loader func init() { + loaders = defaultLoaders() + + // sets the global default loader for go-openapi/spec + spec.PathLoader = loaders.Load +} + +// defaultLoaders builds the built-in loader chain: a YAML matcher first, with a JSON loader as +// the catch-all fallback. +func defaultLoaders() *loader { jsonLoader := &loader{ DocLoaderWithMatch: DocLoaderWithMatch{ Match: func(_ string) bool { @@ -30,15 +39,35 @@ func init() { }, } - loaders = jsonLoader.WithHead(&loader{ + return jsonLoader.WithHead(&loader{ DocLoaderWithMatch: DocLoaderWithMatch{ Match: loading.YAMLMatcher, Fn: loading.YAMLDoc, }, }) +} - // sets the global default loader for go-openapi/spec - spec.PathLoader = loaders.Load +// buildLoaderChain links a list of [DocLoaderWithMatch] into a loader chain, preserving order. +// Entries with a nil Fn are skipped. Returns nil when no usable loader is provided. +func buildLoaderChain(ldrs ...DocLoaderWithMatch) *loader { + var final, prev *loader + for _, ldr := range ldrs { + if ldr.Fn == nil { + continue + } + + node := &loader{DocLoaderWithMatch: ldr} + if prev == nil { + final = node + prev = node + + continue + } + + prev = prev.WithNext(node) + } + + return final } // DocLoader represents a doc loader type. @@ -141,6 +170,17 @@ func JSONDoc(path string, opts ...loading.Option) (json.RawMessage, error) { // // This function updates the default loader used by [github.com/go-openapi/spec]. // Since this sets package level globals, you shouldn't call this concurrently. +// +// # Security +// +// AddLoader only *prepends* to the default chain: the previous loaders — including the +// unconfined JSON fallback — remain reachable, both here and via cross-package "$ref" +// resolution. It is therefore the wrong tool for hardening the global default. To replace the +// chain entirely (leaving no unconfined fallback) use [SetLoaders], or [SetRestrictedLoaders] +// for a one-call confined setup. For a single load, prefer a confined per-call loader via +// [WithLoadingOptions] or [WithDocLoaderMatches]. A custom loader registered here only honors +// the protections if its loading function applies the [github.com/go-openapi/swag/loading] +// options it is given. See the package documentation on Security. func AddLoader(predicate DocMatcher, load DocLoader) { loaders = loaders.WithHead(&loader{ DocLoaderWithMatch: DocLoaderWithMatch{ @@ -152,3 +192,36 @@ func AddLoader(predicate DocMatcher, load DocLoader) { // sets the global default loader for go-openapi/spec spec.PathLoader = loaders.Load } + +// SetLoaders replaces the package-level default loader chain with the given loaders, tried in +// order, and re-points [github.com/go-openapi/spec.PathLoader] at it. +// +// Unlike [AddLoader], nothing of the previous default survives — so when the replacement is +// confined, no unconfined fallback remains for any caller relying on the global default +// (including cross-package "$ref" resolution). An entry with a nil Match is a catch-all; you +// are responsible for providing a suitable fallback. Calling SetLoaders with no usable loader +// restores the built-in default (a YAML matcher with a JSON fallback). +// +// # Concurrency +// +// This sets package-level globals and the [github.com/go-openapi/spec] global loader. It is +// not safe to call concurrently with other loads or with [AddLoader]; configure it once at +// startup, before serving. +// +// # Security +// +// This is the way to harden the global default in one place. For a ready-made confined setup, +// see [SetRestrictedLoaders]. As with [AddLoader], a custom loader only honors the protections +// if its loading function applies the [github.com/go-openapi/swag/loading] options it is given. +// See the package documentation on Security. +func SetLoaders(ldrs ...DocLoaderWithMatch) { + chain := buildLoaderChain(ldrs...) + if chain == nil { + chain = defaultLoaders() + } + + loaders = chain + + // sets the global default loader for go-openapi/spec + spec.PathLoader = loaders.Load +} diff --git a/vendor/github.com/go-openapi/loads/options.go b/vendor/github.com/go-openapi/loads/options.go index 045ece5e0..fec20520b 100644 --- a/vendor/github.com/go-openapi/loads/options.go +++ b/vendor/github.com/go-openapi/loads/options.go @@ -51,25 +51,16 @@ func WithDocLoader(l DocLoader) LoaderOption { // Loaders are executed in the order of provided [DocLoaderWithMatch] 'es. func WithDocLoaderMatches(l ...DocLoaderWithMatch) LoaderOption { return func(opt *options) { - var final, prev *loader - for _, ldr := range l { - if ldr.Fn == nil { - continue - } - - if prev == nil { - final = &loader{DocLoaderWithMatch: ldr} - prev = final - continue - } - - prev = prev.WithNext(&loader{DocLoaderWithMatch: ldr}) - } - opt.loader = final + opt.loader = buildLoaderChain(l...) } } // WithLoadingOptions adds some [loading.Option] to be added when calling a registered loader. +// +// The options are attached to the document's loader, so they apply both to the initial load +// and to every "$ref" resolved during [Document.Expanded]. This is the recommended place to +// confine loading of untrusted input, for example with [loading.WithRoot] (local) and +// [loading.WithHTTPClient] (remote). See the package documentation on Security. func WithLoadingOptions(loadingOptions ...loading.Option) LoaderOption { return func(opt *options) { opt.loadingOptions = loadingOptions diff --git a/vendor/github.com/go-openapi/loads/restricted.go b/vendor/github.com/go-openapi/loads/restricted.go new file mode 100644 index 000000000..022a9a857 --- /dev/null +++ b/vendor/github.com/go-openapi/loads/restricted.go @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package loads + +import ( + "encoding/json" + "net" + "net/http" + "net/netip" + "syscall" + "time" + + "github.com/go-openapi/swag/loading" +) + +const ( + // numConfinementOptions is the count of loading options appended to enforce confinement + // (WithRoot + WithHTTPClient), used to size the bundled option slice. + numConfinementOptions = 2 + + defaultTLSHandshakeTimeout = 10 * time.Second +) + +// RestrictedHTTPClient returns an [http.Client] that refuses, at dial time, to connect to +// loopback, private, link-local (including cloud-metadata endpoints such as 169.254.169.254), +// or unspecified addresses. A blocked connection fails with an error wrapping +// [ErrForbiddenAddress]. +// +// The check runs in the dialer Control hook, after DNS resolution and before connect, so it +// also covers HTTP redirects and DNS rebinding — which a URL-string allowlist cannot. The +// client does not honor proxy environment variables, so the guard always inspects the real +// destination rather than a proxy address. +// +// This is the network half of the restricted loaders ([JSONDocRestricted], +// [JSONSpecRestricted], [SpecRestricted]). It may also be used directly with +// [github.com/go-openapi/swag/loading.WithHTTPClient]. +// +// The policy is opinionated and deliberately simple. For a different one (a custom allow/deny +// list, an explicit proxy, mutual TLS, ...), build your own client and pass it with +// [github.com/go-openapi/swag/loading.WithHTTPClient]. To keep the default address policy as a +// base, reuse [IsForbiddenAddress] in your own dialer Control hook — see the package examples +// for the pattern. +func RestrictedHTTPClient() *http.Client { + control := func(_, address string, _ syscall.RawConn) error { + host, _, err := net.SplitHostPort(address) + if err != nil { + return err + } + addr, err := netip.ParseAddr(host) + if err != nil { + return err + } + if IsForbiddenAddress(addr) { + return ErrForbiddenAddress + } + + return nil + } + + return &http.Client{ + Transport: &http.Transport{ + Proxy: nil, // dial the real destination so the guard inspects it + DialContext: (&net.Dialer{Control: control}).DialContext, + ForceAttemptHTTP2: true, + TLSHandshakeTimeout: defaultTLSHandshakeTimeout, + }, + } +} + +// IsForbiddenAddress reports whether addr is one that [RestrictedHTTPClient] refuses to dial: +// a loopback, private, link-local (including cloud-metadata endpoints such as 169.254.169.254), +// or unspecified address. IPv4-mapped IPv6 addresses are unmapped before the check. +// +// It is exported so callers can reuse or extend the default policy when building their own +// dialer Control hook, for example to also reject a CGNAT range or to carve out a single +// trusted internal host: +// +// control := func(_, address string, _ syscall.RawConn) error { +// host, _, err := net.SplitHostPort(address) +// if err != nil { +// return err +// } +// addr, err := netip.ParseAddr(host) +// if err != nil { +// return err +// } +// if loads.IsForbiddenAddress(addr) && host != allowedInternalHost { +// return loads.ErrForbiddenAddress +// } +// return nil +// } +func IsForbiddenAddress(addr netip.Addr) bool { + a := addr.Unmap() + + return a.IsLoopback() || a.IsPrivate() || a.IsLinkLocalUnicast() || a.IsUnspecified() +} + +// restrictedLoadingOptions bundles caller-supplied options with the confinement options, +// appended last so that local rooting and the restricted client always take precedence +// (the loading options are last-wins). +func restrictedLoadingOptions(root string, extra []loading.Option) []loading.Option { + out := make([]loading.Option, 0, len(extra)+numConfinementOptions) + out = append(out, extra...) + out = append(out, loading.WithRoot(root), loading.WithHTTPClient(RestrictedHTTPClient())) + + return out +} + +// JSONDocRestricted returns a JSON [DocLoader] that confines local reads to root (via +// [github.com/go-openapi/swag/loading.WithRoot]) and restricts remote fetches with +// [RestrictedHTTPClient]. +// +// The returned loader may be registered with [WithDocLoader] or [AddLoader]. The confinement +// always takes precedence over any option passed here or at call time, so a caller cannot +// loosen it through [WithLoadingOptions]. +// +// Like [JSONDoc], it loads JSON only: it does not convert YAML. For specs whose references may +// point at YAML documents, prefer [SpecRestricted], which keeps the default JSON/YAML chain. +func JSONDocRestricted(root string, opts ...loading.Option) DocLoader { + // one restricted client, reused for every path and $ref + return restrictedDocLoader(JSONDoc, restrictedLoadingOptions(root, opts)) +} + +// restrictedDocLoader wraps a [DocLoader] so that the confinement options in base are always +// applied, appended after any call-time options so they take precedence (loading options are +// last-wins). +func restrictedDocLoader(fn DocLoader, base []loading.Option) DocLoader { + return func(path string, callOpts ...loading.Option) (json.RawMessage, error) { + if len(callOpts) == 0 { + return fn(path, base...) + } + + all := make([]loading.Option, 0, len(callOpts)+len(base)) + all = append(all, callOpts...) + all = append(all, base...) // confinement (tail of base) still wins + + return fn(path, all...) + } +} + +// JSONSpecRestricted loads a JSON spec like [JSONSpec], but confines local reads to root and +// restricts remote fetches with [RestrictedHTTPClient]. +// +// The confinement is attached to the document's loader, so it also applies to every "$ref" +// resolved by [Document.Expanded]. Extra [github.com/go-openapi/swag/loading] options (custom +// headers, basic auth, timeout, ...) may be supplied; the confinement always wins over them. +func JSONSpecRestricted(path, root string, opts ...loading.Option) (*Document, error) { + return JSONSpec(path, WithLoadingOptions(restrictedLoadingOptions(root, opts)...)) +} + +// SpecRestricted loads a spec like [Spec] — with JSON/YAML auto-detection — but confines local +// reads to root and restricts remote fetches with [RestrictedHTTPClient]. +// +// The confinement is attached to the document's loader, so it also applies to every "$ref" +// resolved by [Document.Expanded]. Extra [github.com/go-openapi/swag/loading] options (custom +// headers, basic auth, timeout, ...) may be supplied; the confinement always wins over them. +func SpecRestricted(path, root string, opts ...loading.Option) (*Document, error) { + return Spec(path, WithLoadingOptions(restrictedLoadingOptions(root, opts)...)) +} + +// SetRestrictedLoaders hardens the package-level default in a single call: it installs a +// confined JSON/YAML loader chain — local reads rooted at root, remote fetches through +// [RestrictedHTTPClient] — as the global default and as +// [github.com/go-openapi/spec.PathLoader]. +// +// After this call, every load that relies on the package default ([Spec], [JSONSpec], and any +// cross-package "$ref" resolution) is confined, with no unconfined fallback left behind. It is +// the global counterpart of [SpecRestricted]; a single restricted client is shared across the +// chain. Extra [github.com/go-openapi/swag/loading] options may be supplied; the confinement +// always wins over them. +// +// # Concurrency +// +// Like [SetLoaders], this mutates package-level and [github.com/go-openapi/spec] globals and is +// not safe to call concurrently. Configure it once at startup, before serving. To revert, call +// [SetLoaders] with no arguments. +func SetRestrictedLoaders(root string, opts ...loading.Option) { + base := restrictedLoadingOptions(root, opts) // one restricted client shared by the whole chain + + SetLoaders( + NewDocLoaderWithMatch(restrictedDocLoader(loading.YAMLDoc, base), loading.YAMLMatcher), + NewDocLoaderWithMatch(restrictedDocLoader(JSONDoc, base), nil), // nil matcher: JSON catch-all fallback + ) +} diff --git a/vendor/github.com/go-openapi/loads/spec.go b/vendor/github.com/go-openapi/loads/spec.go index 606a01d8e..40eaff2c7 100644 --- a/vendor/github.com/go-openapi/loads/spec.go +++ b/vendor/github.com/go-openapi/loads/spec.go @@ -77,6 +77,14 @@ func Embedded(orig, flat json.RawMessage, opts ...LoaderOption) (*Document, erro // Spec loads a new spec document from a local or remote path. // // By default it uses a JSON or YAML loader, with auto-detection based on the resource extension. +// +// Security: by default the path is read with no confinement (local) and fetched with +// [net/http.DefaultClient] (remote), and any "$ref" later resolved by [Document.Expanded] is +// loaded the same way. When the path or the spec contents may derive from untrusted input, +// confine loading with [WithLoadingOptions] (for example +// [github.com/go-openapi/swag/loading.WithRoot] and +// [github.com/go-openapi/swag/loading.WithHTTPClient]). See the package documentation on +// Security. func Spec(path string, opts ...LoaderOption) (*Document, error) { ldr := loaderFromOptions(opts) @@ -157,6 +165,14 @@ func trimData(in json.RawMessage) (json.RawMessage, error) { } // Expanded expands the $ref fields in the spec [Document] and returns a new expanded [Document]. +// +// Security: expansion resolves every "$ref" by calling the document's loader recursively, so +// the spec contents drive further loads. A spec from an untrusted source can thus trigger +// arbitrary local reads or SSRF through its references. The loader carries the +// [github.com/go-openapi/swag/loading] options supplied via [WithLoadingOptions] at load time; +// configure confinement there so it applies to expansion as well. When no document loader is +// set, expansion falls back to the unconfined package-level loader. See the package +// documentation on Security. func (d *Document) Expanded(options ...*spec.ExpandOptions) (*Document, error) { swspec := new(spec.Swagger) if err := json.Unmarshal(d.raw, swspec); err != nil { diff --git a/vendor/github.com/go-openapi/spec/.golangci.yml b/vendor/github.com/go-openapi/spec/.golangci.yml index dc7c96053..9d2733176 100644 --- a/vendor/github.com/go-openapi/spec/.golangci.yml +++ b/vendor/github.com/go-openapi/spec/.golangci.yml @@ -4,7 +4,10 @@ linters: disable: - depguard - funlen + - goconst - godox + - gomodguard + - gomodguard_v2 - exhaustruct - nlreturn - nonamedreturns diff --git a/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md b/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md index 0f533c016..2fd257bbe 100644 --- a/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/spec/CONTRIBUTORS.md @@ -4,12 +4,12 @@ | Total Contributors | Total Contributions | | --- | --- | -| 38 | 396 | +| 38 | 398 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | | @casualjim | 191 | | -| @fredbi | 94 | | +| @fredbi | 96 | | | @pytlesk4 | 26 | | | @kul-amr | 10 | | | @keramix | 10 | | diff --git a/vendor/github.com/go-openapi/spec/README.md b/vendor/github.com/go-openapi/spec/README.md index 405002b81..7c96eb9a5 100644 --- a/vendor/github.com/go-openapi/spec/README.md +++ b/vendor/github.com/go-openapi/spec/README.md @@ -18,12 +18,9 @@ The object model for OpenAPI v2 specification documents. * **2025-12-19** : new community chat on discord * a new discord community channel is available to be notified of changes and support users - * our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31** You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url] -Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] - ## Status API is stable. @@ -95,9 +92,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). ## Other documentation * [All-time contributors](./CONTRIBUTORS.md) -* [Contributing guidelines](.github/CONTRIBUTING.md) -* [Maintainers documentation](docs/MAINTAINERS.md) -* [Code style](docs/STYLE.md) +* [Contributing guidelines][contributing-doc-site] +* [Maintainers documentation][maintainers-doc-site] +* [Code style][style-doc-site] ## Cutting a new release @@ -132,9 +129,6 @@ Maintainers can cut a new release by either: [doc-url]: https://goswagger.io/go-openapi [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/spec [godoc-url]: http://pkg.go.dev/github.com/go-openapi/spec -[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png -[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM -[slack-url]: https://goswagger.slack.com/archives/C04R30YMU [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 @@ -146,3 +140,7 @@ Maintainers can cut a new release by either: [goversion-url]: https://github.com/go-openapi/spec/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/spec [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/spec/latest + +[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html +[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html +[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html diff --git a/vendor/github.com/go-openapi/spec/header.go b/vendor/github.com/go-openapi/spec/header.go index 599ba2c5d..f656e0789 100644 --- a/vendor/github.com/go-openapi/spec/header.go +++ b/vendor/github.com/go-openapi/spec/header.go @@ -150,7 +150,11 @@ func (h Header) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return jsonutils.ConcatJSON(b1, b2, b3), nil + b4, err := json.Marshal(h.VendorExtensible) + if err != nil { + return nil, err + } + return jsonutils.ConcatJSON(b1, b2, b3, b4), nil } // UnmarshalJSON unmarshals this header from JSON. diff --git a/vendor/github.com/go-openapi/spec/schema_loader.go b/vendor/github.com/go-openapi/spec/schema_loader.go index 0894c932c..1e346069e 100644 --- a/vendor/github.com/go-openapi/spec/schema_loader.go +++ b/vendor/github.com/go-openapi/spec/schema_loader.go @@ -117,7 +117,7 @@ func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error { tgt := reflect.ValueOf(target) - if tgt.Kind() != reflect.Ptr { + if tgt.Kind() != reflect.Pointer { return ErrResolveRefNeedsAPointer } diff --git a/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md b/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md index 286878acf..ef1a73529 100644 --- a/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/swag/CONTRIBUTORS.md @@ -4,11 +4,11 @@ | Total Contributors | Total Contributions | | --- | --- | -| 24 | 242 | +| 24 | 246 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | -| @fredbi | 112 | | +| @fredbi | 116 | | | @casualjim | 98 | | | @alexandear | 4 | | | @orisano | 3 | | diff --git a/vendor/github.com/go-openapi/swag/README.md b/vendor/github.com/go-openapi/swag/README.md index 64f667103..ddbd8735c 100644 --- a/vendor/github.com/go-openapi/swag/README.md +++ b/vendor/github.com/go-openapi/swag/README.md @@ -34,12 +34,9 @@ You may also use it standalone for your projects. * **2025-12-19** : new community chat on discord * a new discord community channel is available to be notified of changes and support users - * our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31** You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url] -Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] - ## Status API is stable. @@ -171,9 +168,9 @@ on top of which it has been built. ## Other documentation * [All-time contributors](./CONTRIBUTORS.md) -* [Contributing guidelines](.github/CONTRIBUTING.md) -* [Maintainers documentation](docs/MAINTAINERS.md) -* [Code style](docs/STYLE.md) +* [Contributing guidelines][contributing-doc-site] +* [Maintainers documentation][maintainers-doc-site] +* [Code style][style-doc-site] ## Cutting a new release @@ -208,9 +205,6 @@ Maintainers can cut a new release by either: [doc-url]: https://goswagger.io/go-openapi [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/swag [godoc-url]: http://pkg.go.dev/github.com/go-openapi/swag -[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png -[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM -[slack-url]: https://goswagger.slack.com/archives/C04R30YMU [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 @@ -222,3 +216,7 @@ Maintainers can cut a new release by either: [goversion-url]: https://github.com/go-openapi/swag/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/swag [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/swag/latest + +[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html +[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html +[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html diff --git a/vendor/github.com/go-openapi/swag/loading/doc.go b/vendor/github.com/go-openapi/swag/loading/doc.go index 8cf7bcb8b..112c49968 100644 --- a/vendor/github.com/go-openapi/swag/loading/doc.go +++ b/vendor/github.com/go-openapi/swag/loading/doc.go @@ -2,4 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 // Package loading provides tools to load a file from http or from a local file system. +// +// # Security +// +// By default, the local loader reads any path the process can access, including absolute +// paths and "file://" URIs (for example "file:///etc/passwd"). Applications that pass +// untrusted input to [LoadFromFileOrHTTP], [JSONDoc] (or to downstream consumers such as +// go-openapi/loads) must confine local loading to a trusted directory. +// +// Use [WithRoot] to do so: it resolves every requested path relative to a chosen directory +// and rejects anything that escapes it, including via symlink. It is built on [os.Root] +// and is therefore safer than passing an [os.DirFS] to [WithFS], which does not block +// symlink escapes. +// +// Remote loading uses a standard [net/http] client. +// By default it follows redirects and performs no destination filtering — exactly like [net/http.DefaultClient]. +// +// A caller-controlled URL may therefore reach internal services or cloud metadata endpoints +// (server-side request forgery). +// +// This package does not, and should not, embed a network policy: +// when the URL may derive from untrusted input, supply a restricted client with +// [WithHTTPClient] whose transport rejects unwanted destinations at dial time — which also +// covers redirects and DNS rebinding. +// See the example on [LoadFromFileOrHTTP]. package loading diff --git a/vendor/github.com/go-openapi/swag/loading/loading.go b/vendor/github.com/go-openapi/swag/loading/loading.go index 269fb74d1..0b38ac1e3 100644 --- a/vendor/github.com/go-openapi/swag/loading/loading.go +++ b/vendor/github.com/go-openapi/swag/loading/loading.go @@ -17,7 +17,11 @@ import ( "strings" ) -// LoadFromFileOrHTTP loads the bytes from a file or a remote http server based on the path passed in +// LoadFromFileOrHTTP loads the bytes from a file or a remote http server based on the path passed in. +// +// Security: by default a local path is read with no confinement, so a caller-controlled path +// (including a "file://" URI or an absolute path) may read any file the process can access. +// When the path may derive from untrusted input, confine local loading with [WithRoot]. func LoadFromFileOrHTTP(pth string, opts ...Option) ([]byte, error) { o := optionsWithDefaults(opts) return LoadStrategy(pth, o.ReadFileFunc(), loadHTTPBytes(opts...), opts...)(pth) @@ -54,11 +58,14 @@ func LoadFromFileOrHTTP(pth string, opts ...Option) ([]byte, error) { // - `file:///c:/folder/file` becomes `C:\folder\file` // - `file://c:/folder/file` is tolerated (without leading `/`) and becomes `c:\folder\file` func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts ...Option) func(string) ([]byte, error) { - if strings.HasPrefix(pth, "http") { + if hasHTTPScheme(pth) { return remote } o := optionsWithDefaults(opts) _, isEmbedFS := o.fs.(embed.FS) + // any loader backed by an fs.FS or an os.Root consumes forward-slash paths on every + // platform, so it must not go through the windows-native file:// preprocessing below. + isFSBacked := o.fs != nil || o.root != "" return func(p string) ([]byte, error) { upth, err := url.PathUnescape(p) @@ -67,14 +74,20 @@ func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts . } cpth, hasPrefix := strings.CutPrefix(upth, "file://") - if !hasPrefix || isEmbedFS || runtime.GOOS != "windows" { + if !hasPrefix || isFSBacked || runtime.GOOS != "windows" { // crude processing: trim the file:// prefix. This leaves full URIs with a host with a (mostly) unexpected result // regular file path provided: just normalize slashes if isEmbedFS { - // on windows, we need to slash the path if FS is an embed FS. + // embed.FS always uses "/" as separator, even on windows, and rejects leading "./" or "/". return local(strings.TrimLeft(filepath.ToSlash(cpth), "./")) // remove invalid leading characters for embed FS } + if isFSBacked { + // other fs.FS (e.g. os.DirFS) and os.Root loaders also use "/" on every platform. + // Escaping paths (absolute, "..", escaping symlinks) are rejected by the loader, not rewritten here. + return local(filepath.ToSlash(cpth)) + } + return local(filepath.FromSlash(cpth)) } @@ -113,6 +126,21 @@ func LoadStrategy(pth string, local, remote func(string) ([]byte, error), opts . } } +// hasHTTPScheme reports whether pth is an absolute URL with an http or https scheme, +// selecting the remote loader. The comparison is case-insensitive, as URL schemes are. +// +// Requiring the "://" separator (rather than a bare "http" prefix) avoids misrouting a +// local file whose name merely starts with "http" (e.g. "httpbin.json") to the remote loader. +func hasHTTPScheme(pth string) bool { + for _, scheme := range [...]string{"http://", "https://"} { + if len(pth) >= len(scheme) && strings.EqualFold(pth[:len(scheme)], scheme) { + return true + } + } + + return false +} + func loadHTTPBytes(opts ...Option) func(path string) ([]byte, error) { o := optionsWithDefaults(opts) diff --git a/vendor/github.com/go-openapi/swag/loading/options.go b/vendor/github.com/go-openapi/swag/loading/options.go index 6674ac69e..2c1282317 100644 --- a/vendor/github.com/go-openapi/swag/loading/options.go +++ b/vendor/github.com/go-openapi/swag/loading/options.go @@ -4,6 +4,7 @@ package loading import ( + "errors" "io/fs" "net/http" "os" @@ -23,7 +24,8 @@ type ( } fileOptions struct { - fs fs.ReadFileFS + fs fs.ReadFileFS + root string // when non-empty, local reads are confined to this directory via os.Root } options struct { @@ -33,6 +35,20 @@ type ( ) func (fo fileOptions) ReadFileFunc() func(string) ([]byte, error) { + if fo.root != "" { + root := fo.root + + return func(name string) ([]byte, error) { + r, err := os.OpenRoot(root) + if err != nil { + return nil, errors.Join(err, ErrLoader) + } + defer func() { _ = r.Close() }() + + return r.ReadFile(name) + } + } + if fo.fs == nil { return os.ReadFile } @@ -87,8 +103,15 @@ func WithHTTPClient(client *http.Client) Option { // By default, the file system is the one provided by the os package. // // For example, this may be set to consume from an embedded file system, or a rooted FS. +// +// WithFS and [WithRoot] are mutually exclusive: the last one applied wins. +// +// Security note: a file system built from [os.DirFS] confines paths but does NOT protect +// against symlinks that escape the root. To load from a directory derived from untrusted +// input, prefer [WithRoot], which is symlink-escape resistant. func WithFS(filesystem fs.FS) Option { return func(o *options) { + o.root = "" // last-wins vs WithRoot if rfs, ok := filesystem.(fs.ReadFileFS); ok { o.fs = rfs @@ -98,6 +121,28 @@ func WithFS(filesystem fs.FS) Option { } } +// WithRoot confines local file loading to dir. +// +// Every requested path is resolved relative to dir, and any path that would escape dir — +// whether through an absolute path, ".." traversal, or a symlink pointing outside dir — is +// rejected. This is built on [os.Root] and is therefore resistant to the symlink escapes +// that a plain [os.DirFS] does not prevent. +// +// WithRoot is the recommended option when loading specs from a location derived from +// untrusted input. It applies to local loading only and has no effect on remote +// (http/https) loading. WithRoot and [WithFS] are mutually exclusive: the last one applied +// wins. +// +// Note: [os.Root] confines path resolution but does not, by itself, protect against +// traversal of mount/bind boundaries, /proc special files, or device files. Point WithRoot +// at a directory that holds only the documents you intend to expose. +func WithRoot(dir string) Option { + return func(o *options) { + o.root = dir + o.fs = nil // last-wins vs WithFS + } +} + type readFileFS struct { fs.FS } diff --git a/vendor/github.com/go-openapi/validate/CONTRIBUTORS.md b/vendor/github.com/go-openapi/validate/CONTRIBUTORS.md index 46da8797d..0f5497c8b 100644 --- a/vendor/github.com/go-openapi/validate/CONTRIBUTORS.md +++ b/vendor/github.com/go-openapi/validate/CONTRIBUTORS.md @@ -4,12 +4,12 @@ | Total Contributors | Total Contributions | | --- | --- | -| 31 | 302 | +| 31 | 305 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | | @casualjim | 169 | | -| @fredbi | 65 | | +| @fredbi | 68 | | | @sttts | 11 | | | @youyuanwu | 9 | | | @keramix | 8 | | diff --git a/vendor/github.com/go-openapi/validate/README.md b/vendor/github.com/go-openapi/validate/README.md index 17bd03b60..b814a2ef1 100644 --- a/vendor/github.com/go-openapi/validate/README.md +++ b/vendor/github.com/go-openapi/validate/README.md @@ -18,12 +18,9 @@ A validator for OpenAPI v2 specifications and JSON schema draft 4. * **2025-12-19** : new community chat on discord * a new discord community channel is available to be notified of changes and support users - * our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31** You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url] -Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] - ## Status API is stable. @@ -75,9 +72,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). ## Other documentation * [All-time contributors](./CONTRIBUTORS.md) -* [Contributing guidelines](.github/CONTRIBUTING.md) -* [Maintainers documentation](docs/MAINTAINERS.md) -* [Code style](docs/STYLE.md) +* [Contributing guidelines][contributing-doc-site] +* [Maintainers documentation][maintainers-doc-site] +* [Code style][style-doc-site] ## Cutting a new release @@ -108,9 +105,6 @@ Maintainers can cut a new release by either: [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/validate [godoc-url]: http://pkg.go.dev/github.com/go-openapi/validate -[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png -[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM -[slack-url]: https://goswagger.slack.com/archives/C04R30YMU [discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue [discord-url]: https://discord.gg/FfnFYaC3k5 @@ -122,3 +116,7 @@ Maintainers can cut a new release by either: [goversion-url]: https://github.com/go-openapi/validate/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/validate [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/validate/latest + +[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html +[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html +[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html diff --git a/vendor/github.com/go-openapi/validate/spec.go b/vendor/github.com/go-openapi/validate/spec.go index b85432f92..0849e47ea 100644 --- a/vendor/github.com/go-openapi/validate/spec.go +++ b/vendor/github.com/go-openapi/validate/spec.go @@ -146,7 +146,8 @@ func (s *SpecValidator) Validate(data any) (*Result, *Result) { errs.Merge(s.validateNonEmptyPathParamNames()) // errs.Merge(s.validateRefNoSibling()) // warning only - errs.Merge(s.validateReferenced()) // warning only + errs.Merge(s.validateReferenced()) // warning only + errs.Merge(s.validateDubiousRefs()) // warning only return errs, warnings } @@ -553,8 +554,12 @@ DEFINITIONS: if schema.Required != nil { // Safeguard for _, pn := range schema.Required { red := s.validateRequiredProperties(pn, d, &schema) //#nosec + // NOTE: capture validity before merging: Merge may redeem `red` to the + // pool (wantsRedeemOnMerge), after which reading it races with a concurrent + // BorrowResult().cleared() in another goroutine sharing the global pool. + isValid := red.IsValid() res.Merge(red) - if !red.IsValid() && !s.Options.ContinueOnErrors { + if !isValid && !s.Options.ContinueOnErrors { break DEFINITIONS // there is an error, let's stop that bleeding } } diff --git a/vendor/github.com/go-openapi/validate/spec_messages.go b/vendor/github.com/go-openapi/validate/spec_messages.go index 42ce36028..eeb8a8695 100644 --- a/vendor/github.com/go-openapi/validate/spec_messages.go +++ b/vendor/github.com/go-openapi/validate/spec_messages.go @@ -177,6 +177,18 @@ const ( // UnusedResponseWarning ... UnusedResponseWarning = "response %q is not used anywhere" + // DubiousAbsoluteRefWarning flags a $ref pointing to an absolute local file location that escapes the + // spec's base path. Absolute local references are legitimate when they stay beneath the base path + // (flattening/expansion introduces such anchors for cyclical $refs), but an absolute reference that + // escapes the base path - or a file:// reference in a spec with no known base - may indicate an + // unsafe or adversarial spec. + DubiousAbsoluteRefWarning = "$ref %q points to an absolute or local file location that escapes the spec's base path: this may be unsafe with adversarial specs" + + // DubiousMultipleHostsWarning flags a spec whose remote $refs resolve to several distinct hosts. + // A single consistent remote host is common and legitimate; references spread across multiple hosts + // may indicate an unsafe or adversarial spec. + DubiousMultipleHostsWarning = "$ref values point to %d distinct remote hosts (%s): a spec referencing multiple hosts may be unsafe" + InvalidObject = "expected an object in %q.%s" ) @@ -404,3 +416,11 @@ func someParametersBrokenMsg(path, method, operationID string) errors.Error { func refShouldNotHaveSiblingsMsg(path, operationID string) errors.Error { return errors.New(errors.CompositeErrorCode, RefShouldNotHaveSiblingsWarning, operationID, path) } + +func dubiousAbsoluteRefMsg(ref string) errors.Error { + return errors.New(errors.CompositeErrorCode, DubiousAbsoluteRefWarning, ref) +} + +func dubiousMultipleHostsMsg(count int, hosts string) errors.Error { + return errors.New(errors.CompositeErrorCode, DubiousMultipleHostsWarning, count, hosts) +} diff --git a/vendor/github.com/go-openapi/validate/spec_ref_warnings.go b/vendor/github.com/go-openapi/validate/spec_ref_warnings.go new file mode 100644 index 000000000..49c72314c --- /dev/null +++ b/vendor/github.com/go-openapi/validate/spec_ref_warnings.go @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "net/url" + "path" + "sort" + "strings" + + "github.com/go-openapi/spec" +) + +// minDistinctHostsToWarn is the number of distinct remote hosts among $refs at or above which +// Rule 2 emits a host-spread warning. A single consistent remote host is legitimate. +const minDistinctHostsToWarn = 2 + +// validateDubiousRefs emits warnings (never errors) when $ref locations match patterns +// that may indicate an unsafe or adversarial spec. It inspects refs as authored, on the +// UNEXPANDED spec, so it must run before expansion flattens them away. +// +// Two rules are applied over s.analyzer.AllRefs(): +// +// - Rule 1 (absolute local escape): a $ref pointing to an absolute local file location +// (file:// scheme, a Unix absolute path, or a Windows drive path such as C:\) is dubious +// UNLESS it stays beneath the spec's base path. Absolute refs beneath the base are +// legitimate: flattening/expansion in go-openapi/spec and analysis introduces absolute +// anchors to resolve cyclical $refs. Relative and fragment-only refs are always exempt. +// +// - Rule 2 (host spread): when remote (http/https, or protocol-relative) refs resolve to +// two or more distinct hosts, a single aggregate warning lists them. A single consistent +// remote host is common and legitimate, so it is not flagged. +// +// All findings are warnings: they do not affect validity (see Result.IsValid). +func (s *SpecValidator) validateDubiousRefs() *Result { + res := pools.poolOfResults.BorrowResult() + + baseDir, hasBase := s.localBaseDir() + + remoteHosts := make(map[string]struct{}) + for _, r := range s.analyzer.AllRefs() { + u := r.GetURL() + if u == nil { // Safeguard: a valid spec always yields parseable refs + continue + } + + // Rule 1: absolute local reference escaping the base path. + if refPath, isLocalAbs := absoluteLocalRefPath(r, u); isLocalAbs { + if !hasBase || !isBeneathBase(refPath, baseDir) { + res.AddWarnings(dubiousAbsoluteRefMsg(r.String())) + } + continue + } + + // Rule 2: gather remote hosts (http/https and protocol-relative //host/...). + if host := remoteRefHost(u); host != "" { + remoteHosts[host] = struct{}{} + } + } + + if len(remoteHosts) >= minDistinctHostsToWarn { + hosts := make([]string, 0, len(remoteHosts)) + for h := range remoteHosts { + hosts = append(hosts, h) + } + sort.Strings(hosts) + res.AddWarnings(dubiousMultipleHostsMsg(len(hosts), strings.Join(hosts, ", "))) + } + + return res +} + +// absoluteLocalRefPath reports whether r is an absolute LOCAL file reference and, if so, +// returns the cleaned path it points to (without scheme/fragment, drive letter lower-cased). +// +// Classification order matters (see the empirical jsonreference flag behavior): +// - file:// scheme is local, including UNC file://host/share (inherently dubious). +// - a non-empty Host with no file scheme means remote (http/https or protocol-relative +// //host/path) - NOT local; handled by Rule 2. This must be checked before the Unix +// branch, because protocol-relative refs also set HasFullFilePath. +// - len(u.Scheme) == 1 is a Windows drive path (C:\ or C:/), whose drive+path land in +// Scheme/Opaque/Path rather than Path. Checked before the Unix branch because C:/x also +// sets HasFullFilePath, and reconstructed from the authored ref string to keep the drive. +// - !r.HasFullURL && r.HasFullFilePath is a plain Unix absolute path (/abs/models.json). +// +// Relative (./x.json) and fragment-only (#/definitions/X) refs return false. +func absoluteLocalRefPath(r spec.Ref, u *url.URL) (string, bool) { + switch { + case r.HasFileScheme: + return fileRefPath(u), true + case u.Host != "": + // Remote (http/https) or protocol-relative //host/path: handled by Rule 2. + return "", false + case len(u.Scheme) == 1: + // Windows drive letter: reconstruct from the authored ref string. + return cleanRefPath(r.String()), true + case !r.HasFullURL && r.HasFullFilePath: + return cleanRefPath(u.Path), true + default: + return "", false + } +} + +// remoteRefHost returns the host of a remote reference (http/https), or of a protocol-relative +// reference (//host/path). It returns "" for local and fragment-only refs. file:// hosts (UNC) +// are deliberately excluded: those are handled as local-absolute refs by Rule 1. +func remoteRefHost(u *url.URL) string { + switch u.Scheme { + case "http", "https": + return u.Host + case "": + // Protocol-relative //host/path: empty scheme but a host is present. + return u.Host + default: + return "" + } +} + +// localBaseDir returns the directory of the spec file, slash-normalized, when the spec was +// loaded from a local path. It returns ok=false when the base is unknown (in-memory spec) or +// remote (http/https), in which case absolute-local refs cannot be proven beneath a base and +// are treated as dubious. +func (s *SpecValidator) localBaseDir() (string, bool) { + specPath := s.spec.SpecFilePath() + if specPath == "" { + return "", false + } + + // Strip a file:// scheme if present; reject remote bases. + if u, err := url.Parse(specPath); err == nil && u.Scheme != "" { + switch { + case u.Scheme == "file": + specPath = u.Path + case len(u.Scheme) == 1: // Windows drive letter, treat as local + // keep specPath as-is (authored path) + default: // http, https, ... : no local base + return "", false + } + } + + return path.Dir(cleanRefPath(specPath)), true +} + +// isBeneathBase reports whether the cleaned target path is located within baseDir, i.e. it does +// not escape baseDir via "..". Comparison is purely lexical on cleaned paths, which is sufficient +// (and cross-platform safe) for a non-fatal warning. Both sides are expected to already be +// cleanRefPath-normalized (slashes, drive-letter case). +func isBeneathBase(target, baseDir string) bool { + if baseDir == "" { + return false + } + if target == baseDir { + return true + } + if !strings.HasSuffix(baseDir, "/") { + baseDir += "/" + } + return strings.HasPrefix(target, baseDir) +} + +// fileRefPath extracts the local path a file:// reference points to, accounting for the way +// Windows file URLs parse: +// - file:///abs/x -> /abs/x (empty host) +// - file:///C:/dir/x -> /c:/dir/x (empty host; drive sits in the path) +// - file://D:/a/x -> d:/a/x (drive letter lands in Host, rejoin it) +// - file://host/share -> /host/share/x (real UNC host kept visible so it cannot match a +// local base and stays flagged as dubious) +func fileRefPath(u *url.URL) string { + switch { + case u.Host == "": + return cleanRefPath(u.Path) + case isDriveHost(u.Host): + // Windows path authored as file://D:/... : the drive landed in Host (e.g. "d:"). + return cleanRefPath(u.Host + u.Path) + default: + // Real remote/UNC host: keep it in the path so it never matches a local base. + return cleanRefPath("//" + u.Host + u.Path) + } +} + +// isDriveHost reports whether a URL host is actually a Windows drive letter (e.g. "d:"), which +// happens when a Windows path is authored as a two-slash file URL: file://D:/path. +func isDriveHost(host string) bool { + h := strings.TrimSuffix(host, ":") + if len(h) != 1 { + return false + } + c := h[0] + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// cleanRefPath normalizes a ref or base path for lexical comparison: backslashes to forward +// slashes, path.Clean, and a lower-cased leading Windows drive letter (matching the behavior of +// go-openapi/spec's normalizer). Plain Unix paths are unaffected, preserving case-sensitivity. +func cleanRefPath(p string) string { + p = path.Clean(strings.ReplaceAll(p, `\`, `/`)) + switch { + case len(p) >= 2 && p[1] == ':': + // drive-letter form: C:/dir -> c:/dir + p = strings.ToLower(p[:1]) + p[1:] + case len(p) >= 3 && p[0] == '/' && p[2] == ':': + // slash-prefixed drive form from canonical file:// URLs: /C:/dir -> c:/dir. + // The leading slash is dropped so this matches the base path derived from + // SpecFilePath (which has no leading slash), and the bare-drive form. + p = strings.ToLower(p[1:2]) + p[2:] + } + return p +} diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/client/alert/get_alerts_parameters.go b/vendor/github.com/prometheus/alertmanager/api/v2/client/alert/get_alerts_parameters.go index c574dec42..512cedd65 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/client/alert/get_alerts_parameters.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/client/alert/get_alerts_parameters.go @@ -104,6 +104,12 @@ type GetAlertsParams struct { */ Receiver *string + /* ReceiverMatchers. + + A matcher expression to filter by receiver labels. For example `owner="my-team"`. Can be repeated to apply multiple matchers. + */ + ReceiverMatchers []string + /* Silenced. Include silenced alerts in results. If false, excludes silenced alerts. Note that true (default) shows both silenced and non-silenced alerts. @@ -237,6 +243,17 @@ func (o *GetAlertsParams) SetReceiver(receiver *string) { o.Receiver = receiver } +// WithReceiverMatchers adds the receiverMatchers to the get alerts params +func (o *GetAlertsParams) WithReceiverMatchers(receiverMatchers []string) *GetAlertsParams { + o.SetReceiverMatchers(receiverMatchers) + return o +} + +// SetReceiverMatchers adds the receiverMatchers to the get alerts params +func (o *GetAlertsParams) SetReceiverMatchers(receiverMatchers []string) { + o.ReceiverMatchers = receiverMatchers +} + // WithSilenced adds the silenced to the get alerts params func (o *GetAlertsParams) WithSilenced(silenced *bool) *GetAlertsParams { o.SetSilenced(silenced) @@ -329,6 +346,17 @@ func (o *GetAlertsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Reg } } + if o.ReceiverMatchers != nil { + + // binding items for receiver_matchers + joinedReceiverMatchers := o.bindParamReceiverMatchers(reg) + + // query array param receiver_matchers + if err := r.SetQueryParam("receiver_matchers", joinedReceiverMatchers...); err != nil { + return err + } + } + if o.Silenced != nil { // query param silenced @@ -385,3 +413,20 @@ func (o *GetAlertsParams) bindParamFilter(formats strfmt.Registry) []string { return filterIS } + +// bindParamGetAlerts binds the parameter receiver_matchers +func (o *GetAlertsParams) bindParamReceiverMatchers(formats strfmt.Registry) []string { + receiverMatchersIR := o.ReceiverMatchers + + var receiverMatchersIC []string + for _, receiverMatchersIIR := range receiverMatchersIR { // explode []string + + receiverMatchersIIV := receiverMatchersIIR // string as string + receiverMatchersIC = append(receiverMatchersIC, receiverMatchersIIV) + } + + // items.CollectionFormat: "multi" + receiverMatchersIS := swag.JoinByFormat(receiverMatchersIC, "multi") + + return receiverMatchersIS +} diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/client/alertgroup/get_alert_groups_parameters.go b/vendor/github.com/prometheus/alertmanager/api/v2/client/alertgroup/get_alert_groups_parameters.go index 06bd5b7d4..280137fe2 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/client/alertgroup/get_alert_groups_parameters.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/client/alertgroup/get_alert_groups_parameters.go @@ -112,6 +112,12 @@ type GetAlertGroupsParams struct { */ Receiver *string + /* ReceiverMatchers. + + A matcher expression to filter by receiver labels. For example `owner="my-team"`. Can be repeated to apply multiple matchers. + */ + ReceiverMatchers []string + /* Silenced. Include silenced alerts within the returned groups. If false, excludes silenced alerts from groups. Note that true (default) shows both silenced and non-silenced alerts. @@ -248,6 +254,17 @@ func (o *GetAlertGroupsParams) SetReceiver(receiver *string) { o.Receiver = receiver } +// WithReceiverMatchers adds the receiverMatchers to the get alert groups params +func (o *GetAlertGroupsParams) WithReceiverMatchers(receiverMatchers []string) *GetAlertGroupsParams { + o.SetReceiverMatchers(receiverMatchers) + return o +} + +// SetReceiverMatchers adds the receiverMatchers to the get alert groups params +func (o *GetAlertGroupsParams) SetReceiverMatchers(receiverMatchers []string) { + o.ReceiverMatchers = receiverMatchers +} + // WithSilenced adds the silenced to the get alert groups params func (o *GetAlertGroupsParams) WithSilenced(silenced *bool) *GetAlertGroupsParams { o.SetSilenced(silenced) @@ -346,6 +363,17 @@ func (o *GetAlertGroupsParams) WriteToRequest(r runtime.ClientRequest, reg strfm } } + if o.ReceiverMatchers != nil { + + // binding items for receiver_matchers + joinedReceiverMatchers := o.bindParamReceiverMatchers(reg) + + // query array param receiver_matchers + if err := r.SetQueryParam("receiver_matchers", joinedReceiverMatchers...); err != nil { + return err + } + } + if o.Silenced != nil { // query param silenced @@ -385,3 +413,20 @@ func (o *GetAlertGroupsParams) bindParamFilter(formats strfmt.Registry) []string return filterIS } + +// bindParamGetAlertGroups binds the parameter receiver_matchers +func (o *GetAlertGroupsParams) bindParamReceiverMatchers(formats strfmt.Registry) []string { + receiverMatchersIR := o.ReceiverMatchers + + var receiverMatchersIC []string + for _, receiverMatchersIIR := range receiverMatchersIR { // explode []string + + receiverMatchersIIV := receiverMatchersIIR // string as string + receiverMatchersIC = append(receiverMatchersIC, receiverMatchersIIV) + } + + // items.CollectionFormat: "multi" + receiverMatchersIS := swag.JoinByFormat(receiverMatchersIC, "multi") + + return receiverMatchersIS +} diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_parameters.go b/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_parameters.go index b468b3c7e..7fadf197f 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_parameters.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_parameters.go @@ -28,6 +28,7 @@ import ( "github.com/go-openapi/runtime" cr "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" ) // NewGetReceiversParams creates a new GetReceiversParams object, @@ -74,6 +75,13 @@ GetReceiversParams contains all the parameters to send to the API endpoint Typically these are written to a http.Request. */ type GetReceiversParams struct { + + /* ReceiverMatchers. + + A matcher expression to filter by receiver labels. For example `owner="my-team"`. Can be repeated to apply multiple matchers. + */ + ReceiverMatchers []string + timeout time.Duration Context context.Context HTTPClient *http.Client @@ -127,6 +135,17 @@ func (o *GetReceiversParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithReceiverMatchers adds the receiverMatchers to the get receivers params +func (o *GetReceiversParams) WithReceiverMatchers(receiverMatchers []string) *GetReceiversParams { + o.SetReceiverMatchers(receiverMatchers) + return o +} + +// SetReceiverMatchers adds the receiverMatchers to the get receivers params +func (o *GetReceiversParams) SetReceiverMatchers(receiverMatchers []string) { + o.ReceiverMatchers = receiverMatchers +} + // WriteToRequest writes these params to a swagger request func (o *GetReceiversParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { @@ -135,8 +154,36 @@ func (o *GetReceiversParams) WriteToRequest(r runtime.ClientRequest, reg strfmt. } var res []error + if o.ReceiverMatchers != nil { + + // binding items for receiver_matchers + joinedReceiverMatchers := o.bindParamReceiverMatchers(reg) + + // query array param receiver_matchers + if err := r.SetQueryParam("receiver_matchers", joinedReceiverMatchers...); err != nil { + return err + } + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } return nil } + +// bindParamGetReceivers binds the parameter receiver_matchers +func (o *GetReceiversParams) bindParamReceiverMatchers(formats strfmt.Registry) []string { + receiverMatchersIR := o.ReceiverMatchers + + var receiverMatchersIC []string + for _, receiverMatchersIIR := range receiverMatchersIR { // explode []string + + receiverMatchersIIV := receiverMatchersIIR // string as string + receiverMatchersIC = append(receiverMatchersIC, receiverMatchersIIV) + } + + // items.CollectionFormat: "multi" + receiverMatchersIS := swag.JoinByFormat(receiverMatchersIC, "multi") + + return receiverMatchersIS +} diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_responses.go b/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_responses.go index dd3850577..519fcfd36 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_responses.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/client/receiver/get_receivers_responses.go @@ -45,6 +45,12 @@ func (o *GetReceiversReader) ReadResponse(response runtime.ClientResponse, consu return nil, err } return result, nil + case 400: + result := NewGetReceiversBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result default: return nil, runtime.NewAPIError("[GET /receivers] getReceivers", response, response.Code()) } @@ -117,3 +123,71 @@ func (o *GetReceiversOK) readResponse(response runtime.ClientResponse, consumer return nil } + +// NewGetReceiversBadRequest creates a GetReceiversBadRequest with default headers values +func NewGetReceiversBadRequest() *GetReceiversBadRequest { + return &GetReceiversBadRequest{} +} + +/* +GetReceiversBadRequest describes a response with status code 400, with default header values. + +Bad request +*/ +type GetReceiversBadRequest struct { + Payload string +} + +// IsSuccess returns true when this get receivers bad request response has a 2xx status code +func (o *GetReceiversBadRequest) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this get receivers bad request response has a 3xx status code +func (o *GetReceiversBadRequest) IsRedirect() bool { + return false +} + +// IsClientError returns true when this get receivers bad request response has a 4xx status code +func (o *GetReceiversBadRequest) IsClientError() bool { + return true +} + +// IsServerError returns true when this get receivers bad request response has a 5xx status code +func (o *GetReceiversBadRequest) IsServerError() bool { + return false +} + +// IsCode returns true when this get receivers bad request response a status code equal to that given +func (o *GetReceiversBadRequest) IsCode(code int) bool { + return code == 400 +} + +// Code gets the status code for the get receivers bad request response +func (o *GetReceiversBadRequest) Code() int { + return 400 +} + +func (o *GetReceiversBadRequest) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /receivers][%d] getReceiversBadRequest %s", 400, payload) +} + +func (o *GetReceiversBadRequest) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[GET /receivers][%d] getReceiversBadRequest %s", 400, payload) +} + +func (o *GetReceiversBadRequest) GetPayload() string { + return o.Payload +} + +func (o *GetReceiversBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/models/alert_group.go b/vendor/github.com/prometheus/alertmanager/api/v2/models/alert_group.go index f5899195a..6cad0ab2e 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/models/alert_group.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/models/alert_group.go @@ -45,7 +45,7 @@ type AlertGroup struct { // receiver // Required: true - Receiver *Receiver `json:"receiver"` + Receiver *ReceiverReference `json:"receiver"` } // Validate validates this alert group diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/models/gettable_alert.go b/vendor/github.com/prometheus/alertmanager/api/v2/models/gettable_alert.go index 4091b569c..8ca83284e 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/models/gettable_alert.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/models/gettable_alert.go @@ -50,7 +50,7 @@ type GettableAlert struct { // receivers // Required: true - Receivers []*Receiver `json:"receivers"` + Receivers []*ReceiverReference `json:"receivers"` // starts at // Required: true @@ -79,7 +79,7 @@ func (m *GettableAlert) UnmarshalJSON(raw []byte) error { Fingerprint *string `json:"fingerprint"` - Receivers []*Receiver `json:"receivers"` + Receivers []*ReceiverReference `json:"receivers"` StartsAt *strfmt.DateTime `json:"startsAt"` @@ -126,7 +126,7 @@ func (m GettableAlert) MarshalJSON() ([]byte, error) { Fingerprint *string `json:"fingerprint"` - Receivers []*Receiver `json:"receivers"` + Receivers []*ReceiverReference `json:"receivers"` StartsAt *strfmt.DateTime `json:"startsAt"` diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver.go b/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver.go index 8e1bf9ee4..6487936c2 100644 --- a/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver.go +++ b/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver.go @@ -21,6 +21,7 @@ package models import ( "context" + stderrors "errors" "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" @@ -33,6 +34,9 @@ import ( // swagger:model receiver type Receiver struct { + // labels + Labels LabelSet `json:"labels,omitempty"` + // name // Required: true Name *string `json:"name"` @@ -42,6 +46,10 @@ type Receiver struct { func (m *Receiver) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateLabels(formats); err != nil { + res = append(res, err) + } + if err := m.validateName(formats); err != nil { res = append(res, err) } @@ -52,6 +60,29 @@ func (m *Receiver) Validate(formats strfmt.Registry) error { return nil } +func (m *Receiver) validateLabels(formats strfmt.Registry) error { + if swag.IsZero(m.Labels) { // not required + return nil + } + + if m.Labels != nil { + if err := m.Labels.Validate(formats); err != nil { + ve := new(errors.Validation) + if stderrors.As(err, &ve) { + return ve.ValidateName("labels") + } + ce := new(errors.CompositeError) + if stderrors.As(err, &ce) { + return ce.ValidateName("labels") + } + + return err + } + } + + return nil +} + func (m *Receiver) validateName(formats strfmt.Registry) error { if err := validate.Required("name", "body", m.Name); err != nil { @@ -61,8 +92,39 @@ func (m *Receiver) validateName(formats strfmt.Registry) error { return nil } -// ContextValidate validates this receiver based on context it is used +// ContextValidate validate this receiver based on the context it is used func (m *Receiver) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateLabels(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Receiver) contextValidateLabels(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.Labels) { // not required + return nil + } + + if err := m.Labels.ContextValidate(ctx, formats); err != nil { + ve := new(errors.Validation) + if stderrors.As(err, &ve) { + return ve.ValidateName("labels") + } + ce := new(errors.CompositeError) + if stderrors.As(err, &ce) { + return ce.ValidateName("labels") + } + + return err + } + return nil } diff --git a/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver_reference.go b/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver_reference.go new file mode 100644 index 000000000..817c63fd3 --- /dev/null +++ b/vendor/github.com/prometheus/alertmanager/api/v2/models/receiver_reference.go @@ -0,0 +1,85 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// Copyright Prometheus Team +// 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. +// + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ReceiverReference receiver reference +// +// swagger:model receiverReference +type ReceiverReference struct { + + // name + // Required: true + Name *string `json:"name"` +} + +// Validate validates this receiver reference +func (m *ReceiverReference) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateName(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ReceiverReference) validateName(formats strfmt.Registry) error { + + if err := validate.Required("name", "body", m.Name); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this receiver reference based on context it is used +func (m *ReceiverReference) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ReceiverReference) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ReceiverReference) UnmarshalBinary(b []byte) error { + var res ReceiverReference + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/vendor/github.com/prometheus/common/config/config.go b/vendor/github.com/prometheus/common/config/config.go index ff54cdd82..d0040763e 100644 --- a/vendor/github.com/prometheus/common/config/config.go +++ b/vendor/github.com/prometheus/common/config/config.go @@ -33,7 +33,7 @@ type Secret string var MarshalSecretValue = false // MarshalYAML implements the yaml.Marshaler interface for Secrets. -func (s Secret) MarshalYAML() (interface{}, error) { +func (s Secret) MarshalYAML() (any, error) { if MarshalSecretValue { return string(s), nil } @@ -44,7 +44,7 @@ func (s Secret) MarshalYAML() (interface{}, error) { } // UnmarshalYAML implements the yaml.Unmarshaler interface for Secrets. -func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (s *Secret) UnmarshalYAML(unmarshal func(any) error) error { type plain Secret return unmarshal((*plain)(s)) } diff --git a/vendor/github.com/prometheus/common/config/http_config.go b/vendor/github.com/prometheus/common/config/http_config.go index 7089fc75a..88457375f 100644 --- a/vendor/github.com/prometheus/common/config/http_config.go +++ b/vendor/github.com/prometheus/common/config/http_config.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "net" "net/http" "net/url" @@ -76,7 +77,7 @@ var TLSVersions = map[string]TLSVersion{ "TLS10": (TLSVersion)(tls.VersionTLS10), } -func (tv *TLSVersion) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (tv *TLSVersion) UnmarshalYAML(unmarshal func(any) error) error { var s string err := unmarshal(&s) if err != nil { @@ -89,7 +90,7 @@ func (tv *TLSVersion) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("unknown TLS version: %s", s) } -func (tv TLSVersion) MarshalYAML() (interface{}, error) { +func (tv TLSVersion) MarshalYAML() (any, error) { for s, v := range TLSVersions { if tv == v { return s, nil @@ -178,7 +179,7 @@ type URL struct { } // UnmarshalYAML implements the yaml.Unmarshaler interface for URLs. -func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (u *URL) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err @@ -193,7 +194,7 @@ func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error { } // MarshalYAML implements the yaml.Marshaler interface for URLs. -func (u URL) MarshalYAML() (interface{}, error) { +func (u URL) MarshalYAML() (any, error) { if u.URL != nil { return u.Redacted(), nil } @@ -269,16 +270,16 @@ type OAuth2 struct { Audience string `yaml:"audience,omitempty" json:"audience,omitempty"` // Claims is a map of claims to be added to the JWT token. Only used if // GrantType is set to "urn:ietf:params:oauth:grant-type:jwt-bearer". - Claims map[string]interface{} `yaml:"claims,omitempty" json:"claims,omitempty"` - Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` - TokenURL string `yaml:"token_url,omitempty" json:"token_url,omitempty"` - EndpointParams map[string]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"` - TLSConfig TLSConfig `yaml:"tls_config,omitempty"` + Claims map[string]any `yaml:"claims,omitempty" json:"claims,omitempty"` + Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` + TokenURL string `yaml:"token_url,omitempty" json:"token_url,omitempty"` + EndpointParams map[string]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"` + TLSConfig TLSConfig `yaml:"tls_config,omitempty"` ProxyConfig `yaml:",inline"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (o *OAuth2) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (o *OAuth2) UnmarshalYAML(unmarshal func(any) error) error { type plain OAuth2 if err := unmarshal((*plain)(o)); err != nil { return err @@ -324,7 +325,7 @@ func LoadHTTPConfigFile(filename string) (*HTTPClientConfig, []byte, error) { if err != nil { return nil, nil, err } - cfg.SetDirectory(filepath.Dir(filepath.Dir(filename))) + cfg.SetDirectory(filepath.Dir(filename)) return cfg, content, nil } @@ -463,7 +464,7 @@ func (c *HTTPClientConfig) Validate() error { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (c *HTTPClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *HTTPClientConfig) UnmarshalYAML(unmarshal func(any) error) error { type plain HTTPClientConfig *c = DefaultHTTPClientConfig if err := unmarshal((*plain)(c)); err != nil { @@ -483,7 +484,7 @@ func (c *HTTPClientConfig) UnmarshalJSON(data []byte) error { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (a *BasicAuth) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (a *BasicAuth) UnmarshalYAML(unmarshal func(any) error) error { type plain BasicAuth return unmarshal((*plain)(a)) } @@ -721,6 +722,14 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon } if cfg.HTTPHeaders != nil { + // Strip sensitive headers added by headersRoundTripper on cross-host + // redirects before they reach the transport. Only needed when + // redirects are actually followed; when FollowRedirects is false + // CheckRedirect returns ErrUseLastResponse immediately so there are + // no subsequent requests. + if cfg.FollowRedirects { + rt = &sensitiveHeadersStripRT{next: rt} + } rt = NewHeadersRoundTripper(cfg.HTTPHeaders, rt) } @@ -862,7 +871,7 @@ func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials Se } func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if len(req.Header.Get("Authorization")) != 0 { + if len(req.Header.Get("Authorization")) != 0 || isCrossHostRedirect(req) { return rt.rt.RoundTrip(req) } @@ -900,7 +909,7 @@ func NewBasicAuthRoundTripper(username, password SecretReader, rt http.RoundTrip } func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if len(req.Header.Get("Authorization")) != 0 { + if len(req.Header.Get("Authorization")) != 0 || isCrossHostRedirect(req) { return rt.rt.RoundTrip(req) } var username string @@ -1059,6 +1068,18 @@ func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, clientCred } func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if isCrossHostRedirect(req) { + // Bypass the OAuth2 transport so no token is attached. Read Base under + // the lock to avoid a data race with concurrent reconfigurations. + rt.mtx.RLock() + base := rt.lastRT.Base + rt.mtx.RUnlock() + if base == nil { + base = http.DefaultTransport + } + return base.RoundTrip(req) + } + var ( secret string needsInit bool @@ -1123,6 +1144,98 @@ func mapToValues(m map[string]string) url.Values { return v } +// isCrossHostRedirect reports whether req is a redirect that has left the +// original request's host at any point in the chain. It detects this by walking +// the req.Response chain (which Go's HTTP client populates on every redirect +// hop) to find the original request's hostname, then checking every hop in the +// chain against it. +// +// The decision is sticky, mirroring net/http: once any hop leaves the original +// host's domain, credentials and sensitive headers stay stripped for the rest +// of the chain, even if a later hop redirects back to the original host. +// +// This works regardless of whether the caller uses NewClientFromConfig or a +// custom http.Client built from NewRoundTripperFromConfigWithContext directly. +func isCrossHostRedirect(req *http.Request) bool { + if req.Response == nil { + return false + } + originalHost := strings.ToLower(originalRequestHost(req)) + for r := req; r.Response != nil && r.Response.Request != nil; r = r.Response.Request { + if !isDomainOrSubdomain(strings.ToLower(r.URL.Hostname()), originalHost) { + return true + } + } + return false +} + +func originalRequestHost(req *http.Request) string { + r := req + for r.Response != nil && r.Response.Request != nil { + r = r.Response.Request + } + return r.URL.Hostname() +} + +// sensitiveHeadersOnRedirect lists the headers that must not be forwarded when +// following a redirect to a different host. The first four entries match the +// list stripped by makeHeadersCopier in net/http/client.go; we additionally +// strip the Proxy-* headers, which net/http does not, to avoid leaking proxy +// credentials to an untrusted host. +var sensitiveHeadersOnRedirect = map[string]struct{}{ + "Authorization": {}, + // "Www-Authenticate" is the canonical form produced by + // textproto.CanonicalMIMEHeaderKey; it is not a typo of "WWW-Authenticate". + "Www-Authenticate": {}, + "Cookie": {}, + "Cookie2": {}, + "Proxy-Authorization": {}, + "Proxy-Authenticate": {}, +} + +// sensitiveHeadersStripRT strips sensitive headers from requests marked as +// cross-host redirects before passing them to the underlying transport. +type sensitiveHeadersStripRT struct { + next http.RoundTripper +} + +func (rt *sensitiveHeadersStripRT) RoundTrip(req *http.Request) (*http.Response, error) { + if isCrossHostRedirect(req) { + req = cloneRequest(req) + for h := range sensitiveHeadersOnRedirect { + req.Header.Del(h) + } + } + return rt.next.RoundTrip(req) +} + +func (rt *sensitiveHeadersStripRT) CloseIdleConnections() { + if ci, ok := rt.next.(closeIdler); ok { + ci.CloseIdleConnections() + } +} + +// isDomainOrSubdomain reports whether sub is a subdomain (or exact match) of +// parent. It mirrors isDomainOrSubdomain from net/http/client.go. +func isDomainOrSubdomain(sub, parent string) bool { + if parent == "" { + return false + } + if sub == parent { + return true + } + // A colon means sub is an IPv6 address; a percent sign introduces an IPv6 + // zone ID. Neither can be a hostname, and both could otherwise pass the + // suffix check below (e.g. "::1%.www.example.com" ends with "example.com"). + if strings.ContainsAny(sub, ":%") { + return false + } + if !strings.HasSuffix(sub, parent) { + return false + } + return sub[len(sub)-len(parent)-1] == '.' +} + // cloneRequest returns a clone of the provided *http.Request. // The clone is a shallow copy of the struct and its Header map. func cloneRequest(r *http.Request) *http.Request { @@ -1130,10 +1243,7 @@ func cloneRequest(r *http.Request) *http.Request { r2 := new(http.Request) *r2 = *r // Deep copy of the Header. - r2.Header = make(http.Header) - for k, s := range r.Header { - r2.Header[k] = s - } + maps.Copy(r.Header, r2.Header) return r2 } @@ -1256,7 +1366,7 @@ func (c *TLSConfig) SetDirectory(dir string) { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (c *TLSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *TLSConfig) UnmarshalYAML(unmarshal func(any) error) error { type plain TLSConfig if err := unmarshal((*plain)(c)); err != nil { return err diff --git a/vendor/github.com/prometheus/common/config/oauth_assertion.go b/vendor/github.com/prometheus/common/config/oauth_assertion.go index bf4bcb949..ba5ffb4dc 100644 --- a/vendor/github.com/prometheus/common/config/oauth_assertion.go +++ b/vendor/github.com/prometheus/common/config/oauth_assertion.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "io" + "maps" "net/http" "net/url" "strings" @@ -133,9 +134,7 @@ func (js jwtSource) Token() (*oauth2.Token, error) { claims["scope"] = scopes } - for k, v := range js.conf.PrivateClaims { - claims[k] = v - } + maps.Copy(claims, js.conf.PrivateClaims) assertion := jwt.NewWithClaims(js.conf.SigningAlgorithm, claims) if js.conf.PrivateKeyID != "" { diff --git a/vendor/github.com/prometheus/common/expfmt/expfmt.go b/vendor/github.com/prometheus/common/expfmt/expfmt.go index 4e4c13e72..10bf35708 100644 --- a/vendor/github.com/prometheus/common/expfmt/expfmt.go +++ b/vendor/github.com/prometheus/common/expfmt/expfmt.go @@ -122,7 +122,7 @@ func NewOpenMetricsFormat(version string) (Format, error) { // removed. func (f Format) WithEscapingScheme(s model.EscapingScheme) Format { var terms []string - for _, p := range strings.Split(string(f), ";") { + for p := range strings.SplitSeq(string(f), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { trimmed := strings.TrimSpace(p) @@ -194,7 +194,7 @@ func (f Format) FormatType() FormatType { // "escaping" term exists, that will be used. Otherwise, the global default will // be returned. func (f Format) ToEscapingScheme() model.EscapingScheme { - for _, p := range strings.Split(string(f), ";") { + for p := range strings.SplitSeq(string(f), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { continue diff --git a/vendor/github.com/prometheus/common/expfmt/text_create.go b/vendor/github.com/prometheus/common/expfmt/text_create.go index 6b8978145..f4074ae9a 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_create.go +++ b/vendor/github.com/prometheus/common/expfmt/text_create.go @@ -42,12 +42,12 @@ const ( var ( bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return bufio.NewWriter(io.Discard) }, } numBufPool = sync.Pool{ - New: func() interface{} { + New: func() any { b := make([]byte, 0, initialNumBufSize) return &b }, diff --git a/vendor/github.com/prometheus/common/expfmt/text_parse.go b/vendor/github.com/prometheus/common/expfmt/text_parse.go index 00c8841a1..4ce1f40b8 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_parse.go +++ b/vendor/github.com/prometheus/common/expfmt/text_parse.go @@ -339,6 +339,16 @@ func (p *TextParser) startLabelName() stateFn { return nil // Unexpected end of input. } if p.currentByte == '}' { + if p.currentMF == nil { + // The closing brace was reached before any metric name was read, + // e.g. for the input "{}". There is no metric to attach labels to, + // so this is a malformed exposition. This mirrors the guard in + // startLabelValue. currentMF (not currentMetric) is checked because + // reset only clears currentMF between parses. + p.parseError("invalid metric name") + p.currentLabelPairs = nil + return nil + } p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...) p.currentLabelPairs = nil if p.skipBlankTab(); p.err != nil { diff --git a/vendor/github.com/prometheus/common/model/labels.go b/vendor/github.com/prometheus/common/model/labels.go index dfeb34be5..29688a13c 100644 --- a/vendor/github.com/prometheus/common/model/labels.go +++ b/vendor/github.com/prometheus/common/model/labels.go @@ -124,7 +124,7 @@ func (ln LabelName) IsValidLegacy() bool { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (ln *LabelName) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err diff --git a/vendor/github.com/prometheus/common/model/labelset.go b/vendor/github.com/prometheus/common/model/labelset.go index 9de47b256..6010b26a8 100644 --- a/vendor/github.com/prometheus/common/model/labelset.go +++ b/vendor/github.com/prometheus/common/model/labelset.go @@ -16,6 +16,7 @@ package model import ( "encoding/json" "fmt" + "maps" "sort" ) @@ -107,9 +108,7 @@ func (ls LabelSet) Before(o LabelSet) bool { // Clone returns a copy of the label set. func (ls LabelSet) Clone() LabelSet { lsn := make(LabelSet, len(ls)) - for ln, lv := range ls { - lsn[ln] = lv - } + maps.Copy(lsn, ls) return lsn } @@ -117,13 +116,9 @@ func (ls LabelSet) Clone() LabelSet { func (ls LabelSet) Merge(other LabelSet) LabelSet { result := make(LabelSet, len(ls)) - for k, v := range ls { - result[k] = v - } + maps.Copy(result, ls) - for k, v := range other { - result[k] = v - } + maps.Copy(result, other) return result } diff --git a/vendor/github.com/prometheus/common/model/metric.go b/vendor/github.com/prometheus/common/model/metric.go index 429a0dab1..2fe461511 100644 --- a/vendor/github.com/prometheus/common/model/metric.go +++ b/vendor/github.com/prometheus/common/model/metric.go @@ -17,6 +17,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "regexp" "sort" "strconv" @@ -258,9 +259,7 @@ func (m Metric) Before(o Metric) bool { // Clone returns a copy of the Metric. func (m Metric) Clone() Metric { clone := make(Metric, len(m)) - for k, v := range m { - clone[k] = v - } + maps.Copy(clone, m) return clone } diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go index 1730b0fdc..0854753f4 100644 --- a/vendor/github.com/prometheus/common/model/time.go +++ b/vendor/github.com/prometheus/common/model/time.go @@ -123,44 +123,38 @@ func (t Time) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface. func (t *Time) UnmarshalJSON(b []byte) error { - p := strings.Split(string(b), ".") - switch len(p) { - case 1: - v, err := strconv.ParseInt(p[0], 10, 64) + base, frac, found := strings.Cut(string(b), ".") + if !found { + v, err := strconv.ParseInt(base, 10, 64) if err != nil { return err } *t = Time(v * second) - - case 2: - v, err := strconv.ParseInt(p[0], 10, 64) + } else { + v, err := strconv.ParseInt(base, 10, 64) if err != nil { return err } - v *= second - prec := dotPrecision - len(p[1]) + prec := dotPrecision - len(frac) if prec < 0 { - p[1] = p[1][:dotPrecision] - } else if prec > 0 { - p[1] += strings.Repeat("0", prec) + frac = frac[:dotPrecision] } - - va, err := strconv.ParseInt(p[1], 10, 32) + va, err := strconv.ParseInt(frac, 10, 32) if err != nil { return err } - - // If the value was something like -0.1 the negative is lost in the - // parsing because of the leading zero, this ensures that we capture it. - if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 { - *t = Time(v+va) * -1 - } else { - *t = Time(v + va) + switch prec { + case 1: + va *= 10 + case 2: + va *= 100 } - default: - return fmt.Errorf("invalid time %q", string(b)) + if len(base) > 0 && base[0] == '-' { + va = -va + } + *t = Time(v*second + va) } return nil } @@ -340,12 +334,12 @@ func (d *Duration) UnmarshalText(text []byte) error { } // MarshalYAML implements the yaml.Marshaler interface. -func (d Duration) MarshalYAML() (interface{}, error) { +func (d Duration) MarshalYAML() (any, error) { return d.String(), nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err diff --git a/vendor/github.com/prometheus/common/model/value.go b/vendor/github.com/prometheus/common/model/value.go index a9995a37e..8dffd9c4a 100644 --- a/vendor/github.com/prometheus/common/model/value.go +++ b/vendor/github.com/prometheus/common/model/value.go @@ -259,13 +259,13 @@ func (s Scalar) String() string { // MarshalJSON implements json.Marshaler. func (s Scalar) MarshalJSON() ([]byte, error) { v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64) - return json.Marshal([...]interface{}{s.Timestamp, v}) + return json.Marshal([...]any{s.Timestamp, v}) } // UnmarshalJSON implements json.Unmarshaler. func (s *Scalar) UnmarshalJSON(b []byte) error { var f string - v := [...]interface{}{&s.Timestamp, &f} + v := [...]any{&s.Timestamp, &f} if err := json.Unmarshal(b, &v); err != nil { return err @@ -291,12 +291,12 @@ func (s *String) String() string { // MarshalJSON implements json.Marshaler. func (s String) MarshalJSON() ([]byte, error) { - return json.Marshal([]interface{}{s.Timestamp, s.Value}) + return json.Marshal([]any{s.Timestamp, s.Value}) } // UnmarshalJSON implements json.Unmarshaler. func (s *String) UnmarshalJSON(b []byte) error { - v := [...]interface{}{&s.Timestamp, &s.Value} + v := [...]any{&s.Timestamp, &s.Value} return json.Unmarshal(b, &v) } diff --git a/vendor/github.com/prometheus/common/model/value_float.go b/vendor/github.com/prometheus/common/model/value_float.go index 6bfc757d1..b7d93615e 100644 --- a/vendor/github.com/prometheus/common/model/value_float.go +++ b/vendor/github.com/prometheus/common/model/value_float.go @@ -79,7 +79,7 @@ func (s SamplePair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil + return fmt.Appendf(nil, "[%s,%s]", t, v), nil } // UnmarshalJSON implements json.Unmarshaler. diff --git a/vendor/github.com/prometheus/common/model/value_histogram.go b/vendor/github.com/prometheus/common/model/value_histogram.go index 91ce5b7a4..f27856ccc 100644 --- a/vendor/github.com/prometheus/common/model/value_histogram.go +++ b/vendor/github.com/prometheus/common/model/value_histogram.go @@ -67,11 +67,11 @@ func (s HistogramBucket) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil + return fmt.Appendf(nil, "[%s,%s,%s,%s]", b, l, u, c), nil } func (s *HistogramBucket) UnmarshalJSON(buf []byte) error { - tmp := []interface{}{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} + tmp := []any{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err @@ -152,11 +152,11 @@ func (s SampleHistogramPair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil + return fmt.Appendf(nil, "[%s,%s]", t, v), nil } func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error { - tmp := []interface{}{&s.Timestamp, &s.Histogram} + tmp := []any{&s.Timestamp, &s.Histogram} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/auth/auth.go b/vendor/github.com/rhobs/obs-mcp/pkg/auth/auth.go new file mode 100644 index 000000000..cb422d0c7 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/auth/auth.go @@ -0,0 +1,152 @@ +package auth + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "log/slog" + "net/http" + "os" + "strings" + + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" + promapi "github.com/prometheus/client_golang/api" + promcfg "github.com/prometheus/common/config" + "k8s.io/client-go/rest" +) + +// AuthMode defines the authentication mode +type AuthMode string + +const ( + // AuthModeKubeConfig reads the bearer token from the kubeconfig or from the mounted service account token file. + AuthModeKubeConfig AuthMode = "kubeconfig" + // AuthModeServiceAccount reads the bearer token from the mounted service account token file. + AuthModeServiceAccount AuthMode = "serviceaccount" + // AuthModeHeader reads the bearer token from the incoming request's authorization header. + // The caller must store the token in the context. + AuthModeHeader AuthMode = "header" +) + +const ( + serviceCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt" +) + +// ParseAuthMode validates and converts a string to AuthMode +func ParseAuthMode(mode string) (AuthMode, error) { + switch mode { + case string(AuthModeKubeConfig): + return AuthModeKubeConfig, nil + case string(AuthModeServiceAccount): + return AuthModeServiceAccount, nil + case string(AuthModeHeader): + return AuthModeHeader, nil + default: + return "", fmt.Errorf("invalid auth mode: %s (valid options: kubeconfig, serviceaccount, header)", mode) + } +} + +// BuildRoundTripper creates an http.RoundTripper using the configured auth mode. +func BuildRoundTripper(ctx context.Context, restConfig *rest.Config, authMode AuthMode, useTLS, insecure bool) (http.RoundTripper, error) { + if restConfig == nil { + return nil, fmt.Errorf("no REST config available") + } + + // Do not use rest.TransportFor() in kubeconfig mode, because rest.TransportFor() inherits + // AccessControlRoundTripper from kubernetes-mcp-server, which provides access control for + // the Kubernetes HTTP API and misinterprets the Prometheus HTTP API endpoints + // (e.g. /api/v1/label) as Kubernetes endpoints. + token, err := readToken(ctx, restConfig, authMode) + if err != nil { + return nil, err + } + + return createRoundTripperWithToken(restConfig, token, useTLS, insecure) +} + +func createRoundTripperWithToken(restConfig *rest.Config, token string, useTLS, insecure bool) (http.RoundTripper, error) { + defaultRt, ok := promapi.DefaultRoundTripper.(*http.Transport) + if !ok { + return nil, fmt.Errorf("unexpected RoundTripper type: %T, expected *http.Transport", promapi.DefaultRoundTripper) + } + rt := defaultRt.Clone() + + if !useTLS { + slog.Warn("Connecting without TLS") + return rt, nil + } + + if insecure { + rt.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, + } + } else { + certs, err := createCertPoolFromRESTConfig(restConfig) + if err != nil { + return nil, err + } + rt.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: certs, + } + } + + if token != "" { + return promcfg.NewAuthorizationCredentialsRoundTripper( + "Bearer", promcfg.NewInlineSecret(token), rt), nil + } + + return rt, nil +} + +// createCertPoolFromRESTConfig creates a cert pool from Kubernetes REST config. +func createCertPoolFromRESTConfig(restConfig *rest.Config) (*x509.CertPool, error) { + var certPool *x509.CertPool + + // Start with system cert pool if available + if systemPool, err := x509.SystemCertPool(); err == nil && systemPool != nil { + certPool = systemPool + } else { + certPool = x509.NewCertPool() + } + + // Try to append cluster CA from REST config + var caLoaded bool + + // First, try CAData + if len(restConfig.CAData) > 0 { + if ok := certPool.AppendCertsFromPEM(restConfig.CAData); ok { + caLoaded = true + slog.Debug("Loaded cluster CA from REST config CAData") + } else { + slog.Warn("Failed to parse CA certificates from REST config CAData") + } + } + + // If CAData wasn't available, try serviceCAFile + if !caLoaded { + caPEM, err := os.ReadFile(serviceCAFile) + if err != nil { + slog.Warn("Failed to read CA file", "file", serviceCAFile, "error", err) + } else { + if ok := certPool.AppendCertsFromPEM(caPEM); ok { + slog.Debug("Loaded cluster CA from file", "file", serviceCAFile) + } else { + slog.Warn("Failed to parse CA certificates from file", "file", serviceCAFile) + } + } + } + + return certPool, nil +} + +func ContextWithAuthFromRequest(ctx context.Context, r *http.Request) context.Context { + authHeader := r.Header.Get(string(kubernetes.OAuthAuthorizationHeader)) + parts := strings.Fields(authHeader) + if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") { + ctx = context.WithValue(ctx, kubernetes.OAuthAuthorizationHeader, "Bearer "+parts[1]) + } + return ctx +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/auth/token.go b/vendor/github.com/rhobs/obs-mcp/pkg/auth/token.go new file mode 100644 index 000000000..c5d605cbf --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/auth/token.go @@ -0,0 +1,80 @@ +package auth + +import ( + "context" + "fmt" + "log/slog" + "os" + "strings" + + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" + "k8s.io/client-go/rest" +) + +const ( + serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" +) + +func readToken(ctx context.Context, restConfig *rest.Config, authMode AuthMode) (string, error) { + switch authMode { + case AuthModeKubeConfig: + return readTokenFromRestConfig(restConfig) + + case AuthModeServiceAccount: + token, err := readTokenFromServiceAccountTokenFile() + if err != nil { + return "", fmt.Errorf("failed to read service account token: %w", err) + } + return string(token), nil + + case AuthModeHeader: + // Read token from context. + // The caller is responsible for putting the token from the request header into the context. + token := readTokenFromContext(ctx) + if token == "" { + slog.Warn("no bearer token found in request context authorization header") + } + return token, nil + + default: + return "", fmt.Errorf("unsupported auth mode: %s", authMode) + } +} + +// readTokenFromRestConfig extracts the bearer token from Kubernetes REST config. +func readTokenFromRestConfig(restConfig *rest.Config) (string, error) { + if restConfig == nil { + return "", fmt.Errorf("no REST config available") + } + + if restConfig.BearerToken != "" { + return restConfig.BearerToken, nil + } + + if restConfig.BearerTokenFile != "" { + token, err := os.ReadFile(restConfig.BearerTokenFile) + if err != nil { + return "", err + } + return strings.TrimSpace(string(token)), nil + } + + return "", nil +} + +func readTokenFromServiceAccountTokenFile() ([]byte, error) { + return os.ReadFile(serviceAccountTokenPath) +} + +// readTokenFromContext reads a token from the context and strips the Bearer prefix +func readTokenFromContext(ctx context.Context) string { + authHeader, ok := ctx.Value(kubernetes.OAuthAuthorizationHeader).(string) + if !ok { + return "" + } + parts := strings.Fields(authHeader) + if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") { + return parts[1] + } + return strings.TrimSpace(authHeader) +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/common.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/common.go new file mode 100644 index 000000000..e965cea0b --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/common.go @@ -0,0 +1,71 @@ +package logs + +import ( + "context" + "encoding/json" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/modelcontextprotocol/go-sdk/mcp" + "k8s.io/client-go/dynamic" + + "github.com/rhobs/obs-mcp/pkg/logs/loki" +) + +// ToolParams is a subset of api.ToolHandlerParams and contains only fields required by logs tool handlers. +type ToolParams struct { + context context.Context + arguments map[string]any + dynamicClient dynamic.Interface + config *Config + newLokiLoader func(url, tenant string) (loki.Loader, error) +} + +// ToolHandler is the signature shared by all logs tool handlers. +type ToolHandler[T any] func(params ToolParams) (T, error) + +func ToMCPHandler[T any]( + newLokiLoader func(ctx context.Context, url, tenant string) (loki.Loader, error), + dynamicClient dynamic.Interface, + config *Config, + handler ToolHandler[T], +) mcp.ToolHandlerFor[map[string]any, T] { + return func(ctx context.Context, request *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, T, error) { + output, err := handler(ToolParams{ + context: ctx, + arguments: args, + dynamicClient: dynamicClient, + config: config, + newLokiLoader: func(url, tenant string) (loki.Loader, error) { + return newLokiLoader(ctx, url, tenant) + }, + }) + return nil, output, err + } +} + +func ToServerHandler[T any]( + newLokiLoader func(params api.ToolHandlerParams, url, tenant string) (loki.Loader, error), + handler ToolHandler[T], +) api.ToolHandlerFunc { + return func(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + config := GetConfig(params) + output, err := handler(ToolParams{ + context: params.Context, + arguments: params.GetArguments(), + dynamicClient: params.DynamicClient(), + config: config, + newLokiLoader: func(url, tenant string) (loki.Loader, error) { + return newLokiLoader(params, url, tenant) + }, + }) + if err != nil { + return api.NewToolCallResult("", err), nil + } + + jsonBytes, err := json.Marshal(output) + if err != nil { + return nil, err + } + return api.NewToolCallResult(string(jsonBytes), nil), nil + } +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/config.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/config.go new file mode 100644 index 000000000..431d86633 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/config.go @@ -0,0 +1,67 @@ +package logs + +import ( + "context" + "fmt" + + "github.com/BurntSushi/toml" + "github.com/containers/kubernetes-mcp-server/pkg/api" + serverconfig "github.com/containers/kubernetes-mcp-server/pkg/config" + + "github.com/rhobs/obs-mcp/pkg/auth" +) + +func init() { + serverconfig.RegisterToolsetConfig(ToolsetName, logsToolsetParser) +} + +type Config struct { + // AuthMode controls where the bearer token is obtained for authenticating against Loki endpoints. + // Valid values: "header" (default), "kubeconfig". + AuthMode auth.AuthMode `toml:"auth_mode,omitempty"` + + // LokiURL is the URL of the Loki API endpoint. + LokiURL string `toml:"loki_url,omitempty"` + + // Insecure controls whether to skip TLS certificate verification. + Insecure bool `toml:"insecure,omitempty"` + + // UseRoute controls whether to use OpenShift Routes for discovering LokiStack endpoints. + UseRoute bool `toml:"useRoute,omitempty"` +} + +var _ api.ExtendedConfig = (*Config)(nil) + +var DefaultConfig = &Config{} + +func (c *Config) Validate() error { + if c.AuthMode != "" && c.AuthMode != auth.AuthModeHeader && c.AuthMode != auth.AuthModeKubeConfig { + return fmt.Errorf("invalid auth_mode: %q (valid options: %q, %q)", c.AuthMode, auth.AuthModeHeader, auth.AuthModeKubeConfig) + } + return nil +} + +func (c *Config) GetAuthMode() auth.AuthMode { + if c.AuthMode == "" { + return auth.AuthModeHeader + } + return c.AuthMode +} + +func logsToolsetParser(_ context.Context, primitive toml.Primitive, md toml.MetaData) (api.ExtendedConfig, error) { + var cfg Config + if err := md.PrimitiveDecode(primitive, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func GetConfig(params api.ToolHandlerParams) *Config { + if cfg, ok := params.GetToolsetConfig(ToolsetName); ok { + if logsCfg, ok := cfg.(*Config); ok { + return logsCfg + } + } + + return DefaultConfig +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/definitions.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/definitions.go new file mode 100644 index 000000000..0a25b3d6c --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/definitions.go @@ -0,0 +1,158 @@ +package logs + +import "github.com/rhobs/obs-mcp/pkg/tools" + +var ( + lokiNamespaceParameter = tools.ParamDef{ + Name: "lokiNamespace", + Type: tools.ParamTypeString, + Description: "Kubernetes namespace of the LokiStack. Use loki_list_instances to discover valid values.", + Required: false, + } + lokiNameParameter = tools.ParamDef{ + Name: "lokiName", + Type: tools.ParamTypeString, + Description: "Name of the LokiStack. Use loki_list_instances to discover valid values.", + Required: false, + } + tenantParameter = tools.ParamDef{ + Name: "tenant", + Type: tools.ParamTypeString, + Description: "Loki tenant ID (X-Scope-OrgID). For LokiStack gateway modes (e.g. openshift-network) this selects the `/api/logs/v1/` path; use `network` for openshift-network.", + Required: false, + } +) + +var ( + ListInstancesTool = tools.ToolDef[ListInstancesOutput]{ + Name: "loki_list_instances", + Description: `List LokiStack instances available in the Kubernetes cluster. +Call this first when using Loki Operator managed stacks so you can pass lokiNamespace and lokiName to other Loki tools.`, + Title: "List LokiStack Instances", + ReadOnly: true, + Destructive: false, + Idempotent: true, + OpenWorld: true, + } + + LabelNamesTool = tools.ToolDef[LabelNamesOutput]{ + Name: "loki_label_names", + Description: lokiLabelNamesPrompt, + Title: "List Loki Label Names", + ReadOnly: true, + Destructive: false, + Idempotent: true, + OpenWorld: true, + Params: []tools.ParamDef{ + lokiNamespaceParameter, + lokiNameParameter, + tenantParameter, + { + Name: "start", + Type: tools.ParamTypeString, + Description: "Start time as RFC3339, Unix timestamp, NOW, or NOW-relative expression (optional).", + Required: false, + }, + { + Name: "end", + Type: tools.ParamTypeString, + Description: "End time as RFC3339, Unix timestamp, NOW, or NOW-relative expression (optional).", + Required: false, + }, + }, + } + + LabelValuesTool = tools.ToolDef[LabelValuesOutput]{ + Name: "loki_label_values", + Description: lokiLabelValuesPrompt, + Title: "List Loki Label Values", + ReadOnly: true, + Destructive: false, + Idempotent: true, + OpenWorld: true, + Params: []tools.ParamDef{ + lokiNamespaceParameter, + lokiNameParameter, + tenantParameter, + { + Name: "label", + Type: tools.ParamTypeString, + Description: "Label key to inspect (for example namespace, pod, container).", + Required: true, + }, + { + Name: "start", + Type: tools.ParamTypeString, + Description: "Start time as RFC3339, Unix timestamp, NOW, or NOW-relative expression (optional).", + Required: false, + }, + { + Name: "end", + Type: tools.ParamTypeString, + Description: "End time as RFC3339, Unix timestamp, NOW, or NOW-relative expression (optional).", + Required: false, + }, + }, + } + + QueryRangeTool = tools.ToolDef[QueryRangeOutput]{ + Name: "loki_query_range", + Description: lokiQueryRangePrompt, + Title: "Execute Loki Range Query", + ReadOnly: true, + Destructive: false, + Idempotent: true, + OpenWorld: true, + Params: []tools.ParamDef{ + lokiNamespaceParameter, + lokiNameParameter, + tenantParameter, + { + Name: "query", + Type: tools.ParamTypeString, + Description: "LogQL query string.", + Required: true, + }, + { + Name: "start", + Type: tools.ParamTypeString, + Description: "Start time as RFC3339, Unix timestamp, NOW, or NOW-relative expression (optional).", + Required: false, + }, + { + Name: "end", + Type: tools.ParamTypeString, + Description: "End time as RFC3339, Unix timestamp, NOW, or NOW-relative expression (optional).", + Required: false, + }, + { + Name: "duration", + Type: tools.ParamTypeString, + Description: "Lookback duration from now when start/end are omitted (for example 5m, 1h). Defaults to 15m.", + Required: false, + Pattern: `^\d+[smhdwy]$`, + }, + { + Name: "limit", + Type: tools.ParamTypeNumber, + Description: "Maximum number of log lines to return. Defaults to 100, max 1000.", + Required: false, + }, + { + Name: "direction", + Type: tools.ParamTypeString, + Description: "Search direction: backward (default) or forward.", + Required: false, + }, + }, + } +) + +func AllTools() []tools.ToolDefInterface { + return []tools.ToolDefInterface{ + ListInstancesTool, + LabelNamesTool, + LabelValuesTool, + QueryRangeTool, + } +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/discovery/discovery.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/discovery/discovery.go new file mode 100644 index 000000000..6e0a7a8f5 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/discovery/discovery.go @@ -0,0 +1,143 @@ +package discovery + +import ( + "context" + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" +) + +type LokiInstance struct { + Namespace string `json:"lokiNamespace"` + Name string `json:"lokiName"` + Status string `json:"status"` + baseURL string +} + +func ListInstances(ctx context.Context, k8sClient dynamic.Interface, useRoute bool) ([]LokiInstance, error) { + if k8sClient == nil { + return nil, fmt.Errorf("kubernetes dynamic client is not available") + } + + list, err := k8sClient.Resource(lokiStackGVR).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list LokiStacks: %w", err) + } + + instances := make([]LokiInstance, 0, len(list.Items)) + for _, item := range list.Items { + var stack LokiStack + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(item.Object, &stack); err != nil { + return nil, fmt.Errorf("failed to parse LokiStack: %w", err) + } + + tenantsMode := "" + if stack.Spec.Tenants != nil { + tenantsMode = stack.Spec.Tenants.Mode + } + baseURL, err := resolveBaseURL(ctx, k8sClient, useRoute, stack.Namespace, stack.Name, tenantsMode) + if err != nil { + return nil, err + } + instances = append(instances, LokiInstance{ + Namespace: stack.Namespace, + Name: stack.Name, + Status: getStatusFromConditions(stack.Status.Conditions), + baseURL: baseURL, + }) + } + + return instances, nil +} + +func FindInstanceByName(instances []LokiInstance, namespace, name string) (LokiInstance, error) { + for _, instance := range instances { + if instance.Namespace == namespace && instance.Name == name { + return instance, nil + } + } + return LokiInstance{}, fmt.Errorf("LokiStack %s/%s not found", namespace, name) +} + +func resolveBaseURL(ctx context.Context, k8sClient dynamic.Interface, useRoute bool, namespace, stackName, tenantsMode string) (string, error) { + gatewaySvcName := fmt.Sprintf("%s-gateway-http", stackName) + if useRoute { + routeHost, err := resolveRouteHost(ctx, k8sClient, namespace, stackName, gatewaySvcName) + if err != nil { + return "", err + } + return fmt.Sprintf("https://%s/api/logs/v1", routeHost), nil + } + + // TODO: revisit better ways to determine the target protocol. + // - cross-check with tracing approach. + if strings.HasPrefix(tenantsMode, "openshift-") { + return fmt.Sprintf("https://%s.%s.svc:8080/api/logs/v1", gatewaySvcName, namespace), nil + } + // For static mode where no gateway api is present. + return fmt.Sprintf("http://%s.%s.svc:8080", gatewaySvcName, namespace), nil +} + +func resolveRouteHost(ctx context.Context, k8sClient dynamic.Interface, namespace, stackName, gatewaySvcName string) (string, error) { + for _, routeName := range []string{stackName, gatewaySvcName} { + host, err := getRouteHost(ctx, k8sClient, namespace, routeName) + if err == nil { + return host, nil + } + if !apierrors.IsNotFound(err) { + return "", err + } + } + + list, err := k8sClient.Resource(routeGVR).Namespace(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return "", fmt.Errorf("failed to list routes in %s: %w", namespace, err) + } + for _, item := range list.Items { + toName, found, err := unstructured.NestedString(item.Object, "spec", "to", "name") + if err != nil || !found || toName != gatewaySvcName { + continue + } + host, found, err := unstructured.NestedString(item.Object, "spec", "host") + if err != nil || !found || host == "" { + continue + } + return host, nil + } + + return "", fmt.Errorf("no route found for gateway service %s/%s", namespace, gatewaySvcName) +} + +func getRouteHost(ctx context.Context, k8sClient dynamic.Interface, namespace, routeName string) (string, error) { + unstructuredRoute, err := k8sClient.Resource(routeGVR).Namespace(namespace).Get(ctx, routeName, metav1.GetOptions{}) + if err != nil { + return "", err + } + + var route Route + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredRoute.Object, &route); err != nil { + return "", fmt.Errorf("failed to parse route %s/%s: %w", namespace, routeName, err) + } + if route.Spec.Host == "" { + return "", fmt.Errorf("route %s/%s has no host", namespace, routeName) + } + return route.Spec.Host, nil +} + +func getStatusFromConditions(conditions []metav1.Condition) string { + for _, cond := range conditions { + if cond.Status == metav1.ConditionTrue { + return cond.Type + } + } + return "" +} + +func (l *LokiInstance) GetURL() string { + return l.baseURL +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/discovery/types.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/discovery/types.go new file mode 100644 index 000000000..907f11e8a --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/discovery/types.go @@ -0,0 +1,50 @@ +package discovery + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + lokiStackGVR = schema.GroupVersionResource{ + Group: "loki.grafana.com", + Version: "v1", + Resource: "lokistacks", + } + routeGVR = schema.GroupVersionResource{ + Group: "route.openshift.io", + Version: "v1", + Resource: "routes", + } +) + +// LokiStack represents the LokiStack CR. +type LokiStack struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec LokiStackSpec `json:"spec"` + Status LokiStackStatus `json:"status"` +} + +type LokiStackSpec struct { + Tenants *LokiStackTenants `json:"tenants,omitempty"` +} + +type LokiStackTenants struct { + Mode string `json:"mode,omitempty"` +} + +type LokiStackStatus struct { + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// Route represents the OpenShift Route CR. +type Route struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + Spec RouteSpec `json:"spec"` +} + +type RouteSpec struct { + Host string `json:"host,omitempty"` +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/handlers.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/handlers.go new file mode 100644 index 000000000..aec1a529d --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/handlers.go @@ -0,0 +1,250 @@ +package logs + +import ( + "fmt" + "time" + + "github.com/prometheus/common/model" + + "github.com/rhobs/obs-mcp/pkg/logs/discovery" + "github.com/rhobs/obs-mcp/pkg/logs/loki" + "github.com/rhobs/obs-mcp/pkg/prometheus" + "github.com/rhobs/obs-mcp/pkg/tools" +) + +const ( + defaultQueryLookback = 15 * time.Minute + defaultQueryLimit = 100 + maxQueryLimit = 1000 +) + +func LabelNamesHandler(params ToolParams) (LabelNamesOutput, error) { + client, err := params.getLokiClient() + if err != nil { + return LabelNamesOutput{}, err + } + + input := buildLabelNamesInput(params.arguments) + start, end, err := parseDefaultTimeRange(input.Start, input.End) + if err != nil { + return LabelNamesOutput{}, err + } + + labels, err := client.LabelNames(params.context, start, end) + if err != nil { + return LabelNamesOutput{}, fmt.Errorf("failed to list Loki label names: %w", err) + } + return LabelNamesOutput{Labels: labels}, nil +} + +func LabelValuesHandler(params ToolParams) (LabelValuesOutput, error) { + client, err := params.getLokiClient() + if err != nil { + return LabelValuesOutput{}, err + } + + input := buildLabelValuesInput(params.arguments) + if input.Label == "" { + return LabelValuesOutput{}, fmt.Errorf("label parameter is required and must be a string") + } + + start, end, err := parseDefaultTimeRange(input.Start, input.End) + if err != nil { + return LabelValuesOutput{}, err + } + + values, err := client.LabelValues(params.context, input.Label, start, end) + if err != nil { + return LabelValuesOutput{}, fmt.Errorf("failed to list Loki label values: %w", err) + } + return LabelValuesOutput{Values: values}, nil +} + +func QueryRangeHandler(params ToolParams) (QueryRangeOutput, error) { + client, err := params.getLokiClient() + if err != nil { + return QueryRangeOutput{}, err + } + + input := buildQueryRangeInput(params.arguments) + if input.Query == "" { + return QueryRangeOutput{}, fmt.Errorf("query parameter is required and must be a string") + } + + start, end, err := parseQueryTimeRange(input) + if err != nil { + return QueryRangeOutput{}, err + } + + direction := input.Direction + if direction == "" { + direction = "backward" + } + if direction != "backward" && direction != "forward" { + return QueryRangeOutput{}, fmt.Errorf("direction must be either backward or forward") + } + + limit := input.Limit + if limit <= 0 { + limit = defaultQueryLimit + } + if limit > maxQueryLimit { + limit = maxQueryLimit + } + + result, err := client.QueryRange(params.context, loki.QueryRangeInput{ + Query: input.Query, + Start: start, + End: end, + Limit: limit, + Direction: direction, + }) + if err != nil { + return QueryRangeOutput{}, fmt.Errorf("failed to execute Loki query_range: %w", err) + } + + return QueryRangeOutput{ + ResultType: result.ResultType, + Streams: result.Streams, + }, nil +} + +func ListInstancesHandler(params ToolParams) (ListInstancesOutput, error) { + instances, err := discovery.ListInstances(params.context, params.dynamicClient, params.useRoute()) + if err != nil { + return ListInstancesOutput{}, err + } + + output := make([]LokiInstance, 0, len(instances)) + for _, instance := range instances { + output = append(output, LokiInstance{ + LokiNamespace: instance.Namespace, + LokiName: instance.Name, + Status: instance.Status, + URL: instance.GetURL(), + }) + } + return ListInstancesOutput{Instances: output}, nil +} + +func buildLabelNamesInput(args map[string]any) LabelNamesInput { + return LabelNamesInput{ + LokiNamespace: tools.GetString(args, "lokiNamespace", ""), + LokiName: tools.GetString(args, "lokiName", ""), + Tenant: tools.GetString(args, "tenant", ""), + Start: tools.GetString(args, "start", ""), + End: tools.GetString(args, "end", ""), + } +} + +func buildLabelValuesInput(args map[string]any) LabelValuesInput { + return LabelValuesInput{ + LokiNamespace: tools.GetString(args, "lokiNamespace", ""), + LokiName: tools.GetString(args, "lokiName", ""), + Tenant: tools.GetString(args, "tenant", ""), + Label: tools.GetString(args, "label", ""), + Start: tools.GetString(args, "start", ""), + End: tools.GetString(args, "end", ""), + } +} + +func buildQueryRangeInput(args map[string]any) QueryRangeInput { + return QueryRangeInput{ + LokiNamespace: tools.GetString(args, "lokiNamespace", ""), + LokiName: tools.GetString(args, "lokiName", ""), + Tenant: tools.GetString(args, "tenant", ""), + Query: tools.GetString(args, "query", ""), + Start: tools.GetString(args, "start", ""), + End: tools.GetString(args, "end", ""), + Duration: tools.GetString(args, "duration", ""), + Direction: tools.GetString(args, "direction", ""), + Limit: tools.GetInt(args, "limit", defaultQueryLimit), + } +} + +func parseDefaultTimeRange(start, end string) (startTime, endTime time.Time, err error) { + if start == "" && end == "" { + endTime = time.Now() + startTime = endTime.Add(-defaultQueryLookback) + return startTime, endTime, nil + } + if (start == "") != (end == "") { + return time.Time{}, time.Time{}, fmt.Errorf("both start and end must be provided together") + } + + startTime, err = prometheus.ParseTimestamp(start) + if err != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid start time format: %w", err) + } + endTime, err = prometheus.ParseTimestamp(end) + if err != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid end time format: %w", err) + } + if startTime.After(endTime) { + return time.Time{}, time.Time{}, fmt.Errorf("start must be before or equal to end") + } + return startTime, endTime, nil +} + +func parseQueryTimeRange(input QueryRangeInput) (start, end time.Time, err error) { + if input.Start != "" || input.End != "" { + return parseDefaultTimeRange(input.Start, input.End) + } + + duration := defaultQueryLookback + if input.Duration != "" { + d, parseErr := model.ParseDuration(input.Duration) + if parseErr != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid duration format: %w", parseErr) + } + duration = time.Duration(d) + if duration <= 0 { + return time.Time{}, time.Time{}, fmt.Errorf("duration must be positive") + } + } + + end = time.Now() + start = end.Add(-duration) + return start, end, nil +} + +func (params ToolParams) getLokiClient() (loki.Loader, error) { + url, err := params.resolveLokiURL() + if err != nil { + return nil, err + } + tenant := tools.GetString(params.arguments, "tenant", "") + return params.newLokiLoader(url, tenant) +} + +func (params ToolParams) resolveLokiURL() (string, error) { + if params.config != nil && params.config.LokiURL != "" { + return params.config.LokiURL, nil + } + + namespace := tools.GetString(params.arguments, "lokiNamespace", "") + name := tools.GetString(params.arguments, "lokiName", "") + if namespace != "" || name != "" { + if namespace == "" || name == "" { + return "", fmt.Errorf("both lokiNamespace and lokiName must be provided together") + } + instances, err := discovery.ListInstances(params.context, params.dynamicClient, params.useRoute()) + if err != nil { + return "", err + } + instance, err := discovery.FindInstanceByName(instances, namespace, name) + if err != nil { + return "", err + } + return instance.GetURL(), nil + } + + return "", fmt.Errorf("loki URL not configured; set loki_url/--loki-url/LOKI_URL or provide lokiNamespace and lokiName") +} + +func (params ToolParams) useRoute() bool { + if params.config == nil { + return false + } + return params.config.UseRoute +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/loki/loader.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/loki/loader.go new file mode 100644 index 000000000..cea0374c3 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/loki/loader.go @@ -0,0 +1,240 @@ +package loki + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + promapi "github.com/prometheus/client_golang/api" +) + +const ( + labelsEndpoint = "/loki/api/v1/labels" + labelValuesEndpoint = "/loki/api/v1/label/%s/values" + queryRangeEndpoint = "/loki/api/v1/query_range" + + requestTimeout = 60 * time.Second + defaultLimit = 100 + maxErrBodyBytes = 64 * 1024 +) + +// Loader defines the interface for querying Loki. +type Loader interface { + LabelNames(ctx context.Context, start, end time.Time) ([]string, error) + LabelValues(ctx context.Context, label string, start, end time.Time) ([]string, error) + QueryRange(ctx context.Context, input QueryRangeInput) (QueryRangeResult, error) +} + +// QueryRangeInput is the query payload for Loki /query_range. +type QueryRangeInput struct { + Query string + Start time.Time + End time.Time + Limit int + Direction string +} + +// Entry is a log line with timestamp. +type Entry struct { + Timestamp string `json:"timestamp"` + Line string `json:"line"` +} + +// Stream is a Loki stream with labels and entries. +type Stream struct { + Labels map[string]string `json:"labels"` + Entries []Entry `json:"entries"` +} + +// QueryRangeResult is the normalized Loki query_range result. +type QueryRangeResult struct { + ResultType string `json:"resultType"` + Streams []Stream `json:"streams"` +} + +// RealLoader is a real Loki loader implementation. +type RealLoader struct { + baseURL string + tenant string + client *http.Client +} + +var _ Loader = (*RealLoader)(nil) + +func NewLoader(apiConfig promapi.Config, tenant string) (*RealLoader, error) { + if strings.TrimSpace(apiConfig.Address) == "" { + return nil, fmt.Errorf("loki URL is required") + } + + baseURL := strings.TrimSuffix(apiConfig.Address, "/") + httpClient := &http.Client{ + Timeout: requestTimeout, + } + if apiConfig.RoundTripper != nil { + httpClient.Transport = apiConfig.RoundTripper + } + + return &RealLoader{ + baseURL: baseURL, + tenant: strings.TrimSpace(tenant), + client: httpClient, + }, nil +} + +func (l *RealLoader) LabelNames(ctx context.Context, start, end time.Time) ([]string, error) { + endpoint := labelsEndpoint + params := buildTimeParams(start, end) + + var response struct { + Status string `json:"status"` + Data []string `json:"data"` + Error string `json:"error"` + } + if err := l.getJSON(ctx, endpoint, params, &response); err != nil { + return nil, err + } + if response.Status != "success" { + return nil, fmt.Errorf("loki labels request failed: %s", response.Error) + } + return response.Data, nil +} + +func (l *RealLoader) LabelValues(ctx context.Context, label string, start, end time.Time) ([]string, error) { + if strings.TrimSpace(label) == "" { + return nil, fmt.Errorf("label is required") + } + endpoint := fmt.Sprintf(labelValuesEndpoint, url.PathEscape(label)) + params := buildTimeParams(start, end) + + var response struct { + Status string `json:"status"` + Data []string `json:"data"` + Error string `json:"error"` + } + if err := l.getJSON(ctx, endpoint, params, &response); err != nil { + return nil, err + } + if response.Status != "success" { + return nil, fmt.Errorf("loki label values request failed: %s", response.Error) + } + return response.Data, nil +} + +func (l *RealLoader) QueryRange(ctx context.Context, input QueryRangeInput) (QueryRangeResult, error) { + params := buildTimeParams(input.Start, input.End) + params.Set("query", input.Query) + params.Set("direction", input.Direction) + params.Set("limit", strconv.Itoa(input.Limit)) + + var response struct { + Status string `json:"status"` + Data struct { + ResultType string `json:"resultType"` + Result []struct { + Stream map[string]string `json:"stream"` + Values [][]string `json:"values"` + } `json:"result"` + } `json:"data"` + Error string `json:"error"` + } + if err := l.getJSON(ctx, queryRangeEndpoint, params, &response); err != nil { + return QueryRangeResult{}, err + } + if response.Status != "success" { + return QueryRangeResult{}, fmt.Errorf("loki query_range request failed: %s", response.Error) + } + + streams := make([]Stream, 0, len(response.Data.Result)) + for _, result := range response.Data.Result { + entries := make([]Entry, 0, len(result.Values)) + for _, raw := range result.Values { + if len(raw) < 2 { + continue + } + entries = append(entries, Entry{ + Timestamp: raw[0], + Line: raw[1], + }) + } + streams = append(streams, Stream{ + Labels: result.Stream, + Entries: entries, + }) + } + + return QueryRangeResult{ + ResultType: response.Data.ResultType, + Streams: streams, + }, nil +} + +func buildTimeParams(start, end time.Time) url.Values { + params := url.Values{} + if !start.IsZero() { + params.Set("start", strconv.FormatInt(start.UnixNano(), 10)) + } + if !end.IsZero() { + params.Set("end", strconv.FormatInt(end.UnixNano(), 10)) + } + return params +} + +func (l *RealLoader) requestURL(endpoint string) string { + // Gateway URL ending with /api/logs/v1 — insert tenant in path + // (used by OpenShift Loki gateway with tenant-based path routing) + if strings.HasSuffix(l.baseURL, "/api/logs/v1") && l.tenant != "" { + return l.baseURL + "/" + url.PathEscape(l.tenant) + endpoint + } + // URL already includes /api/logs/v1/, or standard Loki URL + // (for standard Loki, tenant is sent via X-Scope-OrgID header) + return l.baseURL + endpoint +} + +func (l *RealLoader) getJSON(ctx context.Context, endpoint string, params url.Values, output any) error { + u := l.requestURL(endpoint) + if len(params) > 0 { + u += "?" + params.Encode() + } + + start := time.Now() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) + if err != nil { + return fmt.Errorf("failed to build request: %w", err) + } + if l.tenant != "" { + req.Header.Set("X-Scope-OrgID", l.tenant) + } + + resp, err := l.client.Do(req) + if err != nil { + return fmt.Errorf("loki request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + slog.Debug("Backend call completed", + "backend", "loki", + "endpoint", endpoint, + "status_code", resp.StatusCode, + "duration_ms", time.Since(start).Milliseconds(), + ) + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + body, err := io.ReadAll(io.LimitReader(resp.Body, maxErrBodyBytes)) + if err != nil { + return fmt.Errorf("loki request failed with status %d: failed to read response body: %w", resp.StatusCode, err) + } + return fmt.Errorf("loki request failed with status %d: %s", resp.StatusCode, string(body)) + } + + if err := json.NewDecoder(resp.Body).Decode(output); err != nil { + return fmt.Errorf("failed to decode loki response: %w", err) + } + return nil +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/prompt.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/prompt.go new file mode 100644 index 000000000..4fad68f32 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/prompt.go @@ -0,0 +1,21 @@ +package logs + +const ServerPrompt = ` +You have access to Loki log tools. + +Recommended workflow: +1. Discover LokiStack instances first with loki_list_instances when Loki Operator is installed. +2. Discover labels with loki_label_names and loki_label_values. +3. Build narrow LogQL queries with explicit label matchers. +4. Use short time windows and small limits first, then expand only if needed. + +Avoid broad queries without label matchers because they are expensive and noisy. +` + +const ( + lokiLabelNamesPrompt = `List available Loki label names for a time range. Use this before writing LogQL queries.` + lokiLabelValuesPrompt = `List possible values for a Loki label key. Use this to build precise label matchers in LogQL.` + lokiQueryRangePrompt = `Execute a Loki LogQL range query and return matching log streams and lines. + +Use precise label matchers and a short time window first.` +) diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/toolset.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/toolset.go new file mode 100644 index 000000000..1b49d4c4b --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/toolset.go @@ -0,0 +1,45 @@ +package logs + +import ( + "github.com/containers/kubernetes-mcp-server/pkg/api" + + "github.com/rhobs/obs-mcp/pkg/logs/loki" +) + +const ToolsetName = "logs" + +// Toolset implements the observability toolset for Loki. +type Toolset struct { + NewLokiLoader func(params api.ToolHandlerParams, url, tenant string) (loki.Loader, error) +} + +var _ api.Toolset = (*Toolset)(nil) + +func (t *Toolset) GetName() string { + return ToolsetName +} + +func (t *Toolset) GetDescription() string { + return "Toolset for querying Loki logs" +} + +func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { + return []api.ServerTool{ + ListInstancesTool.ToServerTool(ToServerHandler(t.NewLokiLoader, ListInstancesHandler)), + LabelNamesTool.ToServerTool(ToServerHandler(t.NewLokiLoader, LabelNamesHandler)), + LabelValuesTool.ToServerTool(ToServerHandler(t.NewLokiLoader, LabelValuesHandler)), + QueryRangeTool.ToServerTool(ToServerHandler(t.NewLokiLoader, QueryRangeHandler)), + } +} + +func (t *Toolset) GetPrompts() []api.ServerPrompt { + return nil +} + +func (t *Toolset) GetResources() []api.ServerResource { + return nil +} + +func (t *Toolset) GetResourceTemplates() []api.ServerResourceTemplate { + return nil +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/logs/types.go b/vendor/github.com/rhobs/obs-mcp/pkg/logs/types.go new file mode 100644 index 000000000..6f349d653 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/logs/types.go @@ -0,0 +1,56 @@ +package logs + +import "github.com/rhobs/obs-mcp/pkg/logs/loki" + +type LabelNamesInput struct { + LokiNamespace string `json:"lokiNamespace,omitempty"` + LokiName string `json:"lokiName,omitempty"` + Tenant string `json:"tenant,omitempty"` + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` +} + +type LabelNamesOutput struct { + Labels []string `json:"labels"` +} + +type LabelValuesInput struct { + LokiNamespace string `json:"lokiNamespace,omitempty"` + LokiName string `json:"lokiName,omitempty"` + Tenant string `json:"tenant,omitempty"` + Label string `json:"label"` + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` +} + +type LabelValuesOutput struct { + Values []string `json:"values"` +} + +type QueryRangeInput struct { + LokiNamespace string `json:"lokiNamespace,omitempty"` + LokiName string `json:"lokiName,omitempty"` + Tenant string `json:"tenant,omitempty"` + Query string `json:"query"` + Start string `json:"start,omitempty"` + End string `json:"end,omitempty"` + Duration string `json:"duration,omitempty"` + Limit int `json:"limit,omitempty"` + Direction string `json:"direction,omitempty"` +} + +type ListInstancesOutput struct { + Instances []LokiInstance `json:"instances"` +} + +type LokiInstance struct { + LokiNamespace string `json:"lokiNamespace"` + LokiName string `json:"lokiName"` + Status string `json:"status"` + URL string `json:"url"` +} + +type QueryRangeOutput struct { + ResultType string `json:"resultType"` + Streams []loki.Stream `json:"streams"` +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/config/config.go b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/config/config.go index c06361542..79153dab5 100644 --- a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/config/config.go +++ b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/config/config.go @@ -8,31 +8,19 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/api" serverconfig "github.com/containers/kubernetes-mcp-server/pkg/config" + "github.com/rhobs/obs-mcp/pkg/auth" "github.com/rhobs/obs-mcp/pkg/prometheus" ) const MetricsToolSetName = "metrics" -// AuthMode defines where the bearer token is obtained for authenticating -// against Prometheus and Alertmanager endpoints. -type AuthMode string - -const ( - // AuthModeHeader reads the bearer token from the request context (authorization header). - // This is the default. - AuthModeHeader AuthMode = "header" - - // AuthModeKubeConfig reads the bearer token from the kubeconfig/REST config only. - AuthModeKubeConfig AuthMode = "kubeconfig" -) - // Config holds obs-mcp toolset configuration type Config struct { // AuthMode controls where the bearer token is obtained for authenticating // against Prometheus and Alertmanager endpoints. // Valid values: "header" (default) - read from the request context authorization header, // "kubeconfig" - read from the kubeconfig/REST config. - AuthMode AuthMode `toml:"auth_mode,omitempty"` + AuthMode auth.AuthMode `toml:"auth_mode,omitempty"` // PrometheusURL is the URL of the Prometheus/Thanos Querier endpoint. // This field is required. Example: "https://thanos-querier-openshift-monitoring.apps.example.com" PrometheusURL string `toml:"prometheus_url,omitempty"` @@ -72,8 +60,8 @@ var _ api.ExtendedConfig = (*Config)(nil) // Validate checks that the configuration values are valid. func (c *Config) Validate() error { - if c.AuthMode != "" && c.AuthMode != AuthModeHeader && c.AuthMode != AuthModeKubeConfig { - return fmt.Errorf("invalid auth_mode: %q (valid options: %q, %q)", c.AuthMode, AuthModeHeader, AuthModeKubeConfig) + if c.AuthMode != "" && c.AuthMode != auth.AuthModeHeader && c.AuthMode != auth.AuthModeKubeConfig { + return fmt.Errorf("invalid auth_mode: %q (valid options: %q, %q)", c.AuthMode, auth.AuthModeHeader, auth.AuthModeKubeConfig) } if _, err := c.GetGuardrails(); err != nil { @@ -84,9 +72,9 @@ func (c *Config) Validate() error { } // GetAuthMode returns the configured token source, defaulting to TokenSourceHeader. -func (c *Config) GetAuthMode() AuthMode { +func (c *Config) GetAuthMode() auth.AuthMode { if c.AuthMode == "" { - return AuthModeHeader + return auth.AuthModeHeader } return c.AuthMode } diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/loki_loader.go b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/loki_loader.go new file mode 100644 index 000000000..af471c729 --- /dev/null +++ b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/loki_loader.go @@ -0,0 +1,35 @@ +package tools + +import ( + "fmt" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + + "github.com/rhobs/obs-mcp/pkg/logs" + "github.com/rhobs/obs-mcp/pkg/logs/loki" +) + +const defaultLokiURL = "http://localhost:3100" + +// NewLokiLoader creates a Loki loader using the logs toolset configuration. +func NewLokiLoader(params api.ToolHandlerParams, url, tenant string) (loki.Loader, error) { + cfg := logs.GetConfig(params) + lokiURL := url + if lokiURL == "" { + lokiURL = cfg.LokiURL + } + if lokiURL == "" { + lokiURL = defaultLokiURL + } + + apiConfig, err := buildAPIConfig(params, lokiURL, cfg.Insecure, cfg.GetAuthMode()) + if err != nil { + return nil, fmt.Errorf("failed to create API config: %w", err) + } + + loader, err := loki.NewLoader(apiConfig, tenant) + if err != nil { + return nil, fmt.Errorf("failed to create Loki loader: %w", err) + } + return loader, nil +} diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/prometheus_client.go b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/prometheus_client.go index 0173039aa..758e98feb 100644 --- a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/prometheus_client.go +++ b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/tools/prometheus_client.go @@ -1,28 +1,21 @@ package tools import ( - "crypto/tls" - "crypto/x509" "fmt" "log/slog" - "net/http" - "os" "strings" "github.com/containers/kubernetes-mcp-server/pkg/api" - "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" promapi "github.com/prometheus/client_golang/api" - promcfg "github.com/prometheus/common/config" - "k8s.io/client-go/rest" "github.com/rhobs/obs-mcp/pkg/alertmanager" + "github.com/rhobs/obs-mcp/pkg/auth" "github.com/rhobs/obs-mcp/pkg/prometheus" toolsetconfig "github.com/rhobs/obs-mcp/pkg/toolset/config" ) const ( defaultPrometheusURL = "http://localhost:9090" - serviceCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt" ) // getConfig retrieves the obs-mcp toolset configuration from params. @@ -68,153 +61,18 @@ func getPromClient(params api.ToolHandlerParams) (prometheus.Loader, error) { return promClient, nil } -func resolveToken(params api.ToolHandlerParams, restConfig *rest.Config, authMode toolsetconfig.AuthMode) (string, error) { - slog.Info("Obtaining authentication token", "authMode", authMode) - switch authMode { //nolint:exhaustive // the default auth_mode value is header - case toolsetconfig.AuthModeKubeConfig: - return extractBearerToken(restConfig), nil - default: - token := readTokenFromCtx(params) - if token == "" { - return "", fmt.Errorf("no bearer token found in request context authorization header") - } - return token, nil - } -} - // buildAPIConfig creates a Prometheus API config using the configured auth mode. -func buildAPIConfig(params api.ToolHandlerParams, prometheusURL string, insecure bool, authMode toolsetconfig.AuthMode) (promapi.Config, error) { - restConfig := params.RESTConfig() - if restConfig == nil { - return promapi.Config{}, fmt.Errorf("no REST config available") - } - - token, err := resolveToken(params, restConfig, authMode) +func buildAPIConfig(params api.ToolHandlerParams, prometheusURL string, insecure bool, authMode auth.AuthMode) (promapi.Config, error) { + tls := strings.HasPrefix(prometheusURL, "https://") + rt, err := auth.BuildRoundTripper(params.Context, params.RESTConfig(), authMode, tls, insecure) if err != nil { - return promapi.Config{}, err - } - - return createAPIConfigWithToken(restConfig, prometheusURL, token, insecure) -} - -// createAPIConfigWithToken creates a Prometheus API config with bearer token authentication. -// This follows the pattern from obs-mcp/pkg/mcp/auth.go to avoid using rest.TransportFor() -// which would inherit the AccessControlRoundTripper. -func createAPIConfigWithToken(restConfig *rest.Config, prometheusURL, token string, insecure bool) (promapi.Config, error) { - apiConfig := promapi.Config{ - Address: prometheusURL, - } - - useTLS := strings.HasPrefix(prometheusURL, "https://") - if useTLS { - defaultRt, ok := promapi.DefaultRoundTripper.(*http.Transport) - if !ok { - return promapi.Config{}, fmt.Errorf("unexpected RoundTripper type: %T, expected *http.Transport", promapi.DefaultRoundTripper) - } - - if insecure { - defaultRt.TLSClientConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: true, - } - } else { - // Build cert pool from REST config - certs, err := createCertPoolFromRESTConfig(restConfig) - if err != nil { - return promapi.Config{}, err - } - defaultRt.TLSClientConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - RootCAs: certs, - } - } - - if token != "" { - apiConfig.RoundTripper = promcfg.NewAuthorizationCredentialsRoundTripper( - "Bearer", promcfg.NewInlineSecret(token), defaultRt) - } else { - apiConfig.RoundTripper = defaultRt - } - } else { - slog.Warn("Connecting to Prometheus without TLS") + return promapi.Config{}, fmt.Errorf("failed to create round tripper: %w", err) } - return apiConfig, nil -} - -// createCertPoolFromRESTConfig creates a cert pool from Kubernetes REST config. -func createCertPoolFromRESTConfig(restConfig *rest.Config) (*x509.CertPool, error) { - var certPool *x509.CertPool - - // Start with system cert pool if available - if systemPool, err := x509.SystemCertPool(); err == nil && systemPool != nil { - certPool = systemPool - } else { - certPool = x509.NewCertPool() - } - - // Try to append cluster CA from REST config - var caLoaded bool - - // First, try CAData - if len(restConfig.CAData) > 0 { - if ok := certPool.AppendCertsFromPEM(restConfig.CAData); ok { - caLoaded = true - slog.Debug("Loaded cluster CA from REST config CAData") - } else { - slog.Warn("Failed to parse CA certificates from REST config CAData") - } - } - - // If CAData wasn't available, try serviceCAFile - if !caLoaded { - caPEM, err := os.ReadFile(serviceCAFile) - if err != nil { - slog.Warn("Failed to read CA file", "file", serviceCAFile, "error", err) - } else { - if ok := certPool.AppendCertsFromPEM(caPEM); ok { - slog.Debug("Loaded cluster CA from file", "file", serviceCAFile) - } else { - slog.Warn("Failed to parse CA certificates from file", "file", serviceCAFile) - } - } - } - - return certPool, nil -} - -// extractBearerToken extracts the bearer token from Kubernetes REST config. -func extractBearerToken(restConfig *rest.Config) string { - if restConfig == nil { - return "" - } - - if restConfig.BearerToken != "" { - return restConfig.BearerToken - } - - if restConfig.BearerTokenFile != "" { - token, err := os.ReadFile(restConfig.BearerTokenFile) - if err != nil { - slog.Warn("Failed to read token file", "file", restConfig.BearerTokenFile, "error", err) - return "" - } - return strings.TrimSpace(string(token)) - } - - return "" -} - -func readTokenFromCtx(params api.ToolHandlerParams) string { - authHeader, ok := params.Value(kubernetes.OAuthAuthorizationHeader).(string) - if !ok { - return "" - } - parts := strings.Fields(authHeader) - if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") { - return parts[1] - } - return strings.TrimSpace(authHeader) + return promapi.Config{ + Address: prometheusURL, + RoundTripper: rt, + }, nil } // getAlertmanagerClient creates an Alertmanager client using the toolset configuration. @@ -226,17 +84,7 @@ func getAlertmanagerClient(params api.ToolHandlerParams) (alertmanager.Loader, e return nil, fmt.Errorf("alertmanager_url not configured") } - restConfig := params.RESTConfig() - if restConfig == nil { - return nil, fmt.Errorf("no REST config available") - } - - token, err := resolveToken(params, restConfig, cfg.GetAuthMode()) - if err != nil { - return nil, err - } - - apiConfig, err := createAPIConfigWithToken(restConfig, alertmanagerURL, token, cfg.Insecure) + apiConfig, err := buildAPIConfig(params, alertmanagerURL, cfg.Insecure, cfg.GetAuthMode()) if err != nil { return nil, fmt.Errorf("failed to create API config: %w", err) } diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/toolset.go b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/toolset.go index 23bc945e3..4b1c2df6f 100644 --- a/vendor/github.com/rhobs/obs-mcp/pkg/toolset/toolset.go +++ b/vendor/github.com/rhobs/obs-mcp/pkg/toolset/toolset.go @@ -6,6 +6,7 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/api" "github.com/containers/kubernetes-mcp-server/pkg/toolsets" + "github.com/rhobs/obs-mcp/pkg/logs" "github.com/rhobs/obs-mcp/pkg/otelcol" "github.com/rhobs/obs-mcp/pkg/toolset/config" toolset_tools "github.com/rhobs/obs-mcp/pkg/toolset/tools" @@ -62,5 +63,6 @@ func (t *Toolset) GetResourceTemplates() []api.ServerResourceTemplate { func init() { toolsets.Register(&Toolset{}) toolsets.Register(&tempo.Toolset{NewTempoLoader: toolset_tools.NewTempoLoader}) + toolsets.Register(&logs.Toolset{NewLokiLoader: toolset_tools.NewLokiLoader}) toolsets.Register(&otelcol.Toolset{}) } diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/traces/common.go b/vendor/github.com/rhobs/obs-mcp/pkg/traces/common.go index ff0483fb0..2b1b6fea3 100644 --- a/vendor/github.com/rhobs/obs-mcp/pkg/traces/common.go +++ b/vendor/github.com/rhobs/obs-mcp/pkg/traces/common.go @@ -38,43 +38,59 @@ var ( } ) -// getTempoClient returns a Tempo client based on the tempoNamespace, tempoName and tenant parameters. +// getTempoClient returns a Tempo client based on the config and tempoNamespace, tempoName and tenant parameters. +// When a static TempoURL is configured, it is used directly without discovery. +// Otherwise, the Tempo instance is resolved via Kubernetes discovery using the provided parameters. func (t *Toolset) getTempoClient(params ToolParams) (tempoclient.Loader, error) { + url, err := resolveTempoURL(params) + if err != nil { + return nil, err + } + return params.newTempoLoader(url) +} + +func resolveTempoURL(params ToolParams) (string, error) { + if params.config != nil && params.config.TempoURL != "" { + return params.config.TempoURL, nil + } + args := params.arguments namespace := tools.GetString(args, "tempoNamespace", "") + name := tools.GetString(args, "tempoName", "") + + if namespace == "" && name == "" { + return "", fmt.Errorf("tempo URL not configured; set tempo_url/--traces.tempo-url/TEMPO_URL or provide tempoNamespace and tempoName") + } if namespace == "" { - return nil, errors.New("tempoNamespace parameter must not be empty") + return "", errors.New("tempoNamespace parameter must not be empty") } - - name := tools.GetString(args, "tempoName", "") if name == "" { - return nil, errors.New("tempoName parameter must not be empty") + return "", errors.New("tempoName parameter must not be empty") } instances, err := discovery.ListInstances(params.context, params.dynamicClient, params.config.UseRoute) if err != nil { - return nil, err + return "", err } // Make sure this Tempo instance exists in cluster. Otherwise, an attacker could potentially trick the MCP tool to connect to non-Tempo services. instance, err := findInstanceByName(instances, namespace, name) if err != nil { - return nil, err + return "", err } tenant := tools.GetString(args, "tenant", "") if instance.Multitenancy { if tenant == "" { - return nil, errors.New("tenant parameter must not be empty for multi-tenant instance") + return "", errors.New("tenant parameter must not be empty for multi-tenant instance") } if !slices.Contains(instance.Tenants, tenant) { - return nil, fmt.Errorf("tenant '%s' does not exist for instance '%s' in namespace '%s'", tenant, name, namespace) + return "", fmt.Errorf("tenant '%s' does not exist for instance '%s' in namespace '%s'", tenant, name, namespace) } } - url := instance.GetURL(tenant) - return params.newTempoLoader(url) + return instance.GetURL(tenant), nil } func findInstanceByName(instances []discovery.TempoInstance, namespace, name string) (discovery.TempoInstance, error) { diff --git a/vendor/github.com/rhobs/obs-mcp/pkg/traces/config.go b/vendor/github.com/rhobs/obs-mcp/pkg/traces/config.go index 13f36ff00..47fe08e6a 100644 --- a/vendor/github.com/rhobs/obs-mcp/pkg/traces/config.go +++ b/vendor/github.com/rhobs/obs-mcp/pkg/traces/config.go @@ -8,7 +8,7 @@ import ( "github.com/containers/kubernetes-mcp-server/pkg/api" serverconfig "github.com/containers/kubernetes-mcp-server/pkg/config" - toolsetconfig "github.com/rhobs/obs-mcp/pkg/toolset/config" + "github.com/rhobs/obs-mcp/pkg/auth" ) func init() { @@ -18,11 +18,15 @@ func init() { type Config struct { // AuthMode controls where the bearer token is obtained for authenticating against Tempo endpoints. // Valid values: "header" (default), "kubeconfig". - AuthMode toolsetconfig.AuthMode `toml:"auth_mode,omitempty"` + AuthMode auth.AuthMode `toml:"auth_mode,omitempty"` // Insecure controls whether to skip TLS certificate verification. Insecure bool `toml:"insecure,omitempty"` + // TempoURL is the URL of the Tempo API endpoint. + // When set, it is used directly instead of discovering Tempo instances via Kubernetes. + TempoURL string `toml:"tempo_url,omitempty"` + // UseRoute controls whether to use OpenShift Routes for discovering Tempo endpoints. UseRoute bool `toml:"useRoute,omitempty"` } @@ -34,15 +38,15 @@ var DefaultConfig = &Config{ } func (c *Config) Validate() error { - if c.AuthMode != "" && c.AuthMode != toolsetconfig.AuthModeHeader && c.AuthMode != toolsetconfig.AuthModeKubeConfig { - return fmt.Errorf("invalid auth_mode: %q (valid options: %q, %q)", c.AuthMode, toolsetconfig.AuthModeHeader, toolsetconfig.AuthModeKubeConfig) + if c.AuthMode != "" && c.AuthMode != auth.AuthModeHeader && c.AuthMode != auth.AuthModeKubeConfig { + return fmt.Errorf("invalid auth_mode: %q (valid options: %q, %q)", c.AuthMode, auth.AuthModeHeader, auth.AuthModeKubeConfig) } return nil } -func (c *Config) GetAuthMode() toolsetconfig.AuthMode { +func (c *Config) GetAuthMode() auth.AuthMode { if c.AuthMode == "" { - return toolsetconfig.AuthModeHeader + return auth.AuthModeHeader } return c.AuthMode } diff --git a/vendor/golang.org/x/net/http2/server_wrap.go b/vendor/golang.org/x/net/http2/server_wrap.go index a7a09551c..737f1f057 100644 --- a/vendor/golang.org/x/net/http2/server_wrap.go +++ b/vendor/golang.org/x/net/http2/server_wrap.go @@ -10,9 +10,11 @@ package http2 import ( "context" + "crypto/tls" "errors" "net" "net/http" + "slices" "sync" "time" ) @@ -44,6 +46,20 @@ func configureServer(s *http.Server, conf *Server) error { h2.IdleTimeout = h1.ReadTimeout } } + + // Register h2 and http/1.1 ALPN protocols on s.TLSConfig, matching + // the pre-wrapping implementation in server.go, so that TLS listeners + // built from s.TLSConfig still negotiate HTTP/2. + if s.TLSConfig == nil { + s.TLSConfig = new(tls.Config) + } + if !slices.Contains(s.TLSConfig.NextProtos, NextProtoTLS) { + s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, NextProtoTLS) + } + if !slices.Contains(s.TLSConfig.NextProtos, "http/1.1") { + s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, "http/1.1") + } + conf.state = &serverInternalState{ s1: s, } diff --git a/vendor/golang.org/x/net/http2/transport_wrap.go b/vendor/golang.org/x/net/http2/transport_wrap.go index d25d99bdb..eab2e6b07 100644 --- a/vendor/golang.org/x/net/http2/transport_wrap.go +++ b/vendor/golang.org/x/net/http2/transport_wrap.go @@ -22,8 +22,8 @@ import ( ) func configureTransport(t1 *http.Transport) error { - // ConfigureTransport is a no-op: The http.Transport already supports HTTP/2. - return nil + _, err := configureTransports(t1) + return err } func configureTransports(t1 *http.Transport) (*Transport, error) { @@ -31,6 +31,17 @@ func configureTransports(t1 *http.Transport) (*Transport, error) { // linked to the http.Transport's. tr2 := &Transport{} tr2.configure(t1) + // Enable HTTP/2 on the transport, as the pre-wrapping implementation did: + // net/http does not auto-enable it for a transport with a custom + // TLSClientConfig or dialer. + if t1.TLSClientConfig == nil { + t1.TLSClientConfig = &tls.Config{} + } + if t1.Protocols == nil { + t1.Protocols = new(http.Protocols) + t1.Protocols.SetHTTP1(true) + } + t1.Protocols.SetHTTP2(true) return tr2, nil } diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index d11d5b96a..526a0d5f4 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -6397,3 +6397,79 @@ const ( MPOL_PREFERRED_MANY = 0x5 MPOL_WEIGHTED_INTERLEAVE = 0x6 ) + +const ( + GPIO_V2_GET_LINEINFO_IOCTL = 0xc100b405 + GPIO_V2_GET_LINE_IOCTL = 0xc250b407 + GPIO_V2_LINE_GET_VALUES_IOCTL = 0xc010b40e + GPIO_V2_LINE_SET_VALUES_IOCTL = 0xc010b40f + GPIO_V2_GET_LINEINFO_WATCH_IOCTL = 0xc100b406 + GPIO_GET_LINEINFO_UNWATCH_IOCTL = 0xc004b40c +) +const ( + GPIO_V2_LINE_ATTR_ID_FLAGS = 0x1 + GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES = 0x2 + GPIO_V2_LINE_ATTR_ID_DEBOUNCE = 0x3 + GPIO_V2_LINE_CHANGED_REQUESTED = 0x1 + GPIO_V2_LINE_CHANGED_RELEASED = 0x2 + GPIO_V2_LINE_CHANGED_CONFIG = 0x3 + GPIO_V2_LINE_EVENT_RISING_EDGE = 0x1 + GPIO_V2_LINE_EVENT_FALLING_EDGE = 0x2 +) + +type GPIOChipInfo struct { + Name [32]byte + Label [32]byte + Lines uint32 +} +type GPIOV2LineValues struct { + Bits uint64 + Mask uint64 +} +type GPIOV2LineAttribute struct { + Id uint32 + _ uint32 + Flags uint64 +} +type GPIOV2LineConfigAttribute struct { + Attr GPIOV2LineAttribute + Mask uint64 +} +type GPIOV2LineConfig struct { + Flags uint64 + Num_attrs uint32 + _ [5]uint32 + Attrs [10]GPIOV2LineConfigAttribute +} +type GPIOV2LineRequest struct { + Offsets [64]uint32 + Consumer [32]byte + Config GPIOV2LineConfig + Num_lines uint32 + Event_buffer_size uint32 + _ [5]uint32 + Fd int32 +} +type GPIOV2LineInfo struct { + Name [32]byte + Consumer [32]byte + Offset uint32 + Num_attrs uint32 + Flags uint64 + Attrs [10]GPIOV2LineAttribute + _ [4]uint32 +} +type GPIOV2LineInfoChanged struct { + Info GPIOV2LineInfo + Timestamp_ns uint64 + Event_type uint32 + _ [5]uint32 +} +type GPIOV2LineEvent struct { + Timestamp_ns uint64 + Id uint32 + Offset uint32 + Seqno uint32 + Line_seqno uint32 + _ [6]uint32 +} diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_386.go b/vendor/golang.org/x/sys/unix/ztypes_linux_386.go index 97ef790de..aede1de7f 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_386.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_386.go @@ -711,3 +711,7 @@ type SysvShmDesc struct { _ uint32 _ uint32 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go index 90b50da68..bb3bc4dc2 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go @@ -725,3 +725,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go b/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go index acda13685..1fdf4c517 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_arm.go @@ -705,3 +705,7 @@ type SysvShmDesc struct { _ uint32 _ uint32 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go index ef7a99e1f..063e6f0b4 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go @@ -704,3 +704,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go index 966063dfc..9cf836c70 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go @@ -705,3 +705,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go index dc53b20b7..1d222fcb3 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mips.go @@ -710,3 +710,7 @@ type SysvShmDesc struct { Ctime_high uint16 _ uint16 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go index 9ad0aa8c3..912cc4ab6 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go @@ -707,3 +707,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go index 29d55493d..1e358ef34 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go @@ -707,3 +707,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go b/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go index a4d9e1584..df59f32f5 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go @@ -710,3 +710,7 @@ type SysvShmDesc struct { Ctime_high uint16 _ uint16 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go index f8a297771..29355aa0b 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go @@ -718,3 +718,7 @@ type SysvShmDesc struct { _ uint32 _ [4]byte } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go index 4158d6c4e..c6083a15d 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go @@ -713,3 +713,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go index 1035af49f..6321cc762 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go @@ -713,3 +713,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go index 2297125d3..b44f402fe 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go @@ -792,3 +792,7 @@ const ( RISCV_HWPROBE_KEY_ZICBOZ_BLOCK_SIZE = 0x6 RISCV_HWPROBE_WHICH_CPUS = 0x1 ) + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go b/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go index 8481e9bd9..b22c795a6 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go @@ -727,3 +727,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 +) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go b/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go index a6828a031..0b18075b5 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go @@ -708,3 +708,7 @@ type SysvShmDesc struct { _ uint64 _ uint64 } + +const ( + GPIO_GET_CHIPINFO_IOCTL = 0x4044b401 +) diff --git a/vendor/k8s.io/apimachinery/pkg/api/validation/objectmeta.go b/vendor/k8s.io/apimachinery/pkg/api/validation/objectmeta.go index 839fcbc2c..9a4f37848 100644 --- a/vendor/k8s.io/apimachinery/pkg/api/validation/objectmeta.go +++ b/vendor/k8s.io/apimachinery/pkg/api/validation/objectmeta.go @@ -46,7 +46,7 @@ func ValidateAnnotations(annotations map[string]string, fldPath *field.Path) fie for k := range annotations { // The rule is QualifiedName except that case doesn't matter, so convert to lowercase before checking. for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) { - allErrs = append(allErrs, field.Invalid(fldPath, k, msg)).WithOrigin("format=k8s-label-key") + allErrs = append(allErrs, field.Invalid(fldPath, k, msg).WithOrigin("format=k8s-label-key")) } } if err := ValidateAnnotationsSize(annotations); err != nil { diff --git a/vendor/modules.txt b/vendor/modules.txt index 0068ba939..e67ee471f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -146,7 +146,7 @@ github.com/go-openapi/jsonpointer ## explicit; go 1.25.0 github.com/go-openapi/jsonreference github.com/go-openapi/jsonreference/internal -# github.com/go-openapi/loads v0.23.4 +# github.com/go-openapi/loads v0.24.0 ## explicit; go 1.25.0 github.com/go-openapi/loads # github.com/go-openapi/runtime v0.32.3 @@ -166,53 +166,53 @@ github.com/go-openapi/runtime/server-middleware/docui github.com/go-openapi/runtime/server-middleware/mediatype github.com/go-openapi/runtime/server-middleware/negotiate github.com/go-openapi/runtime/server-middleware/negotiate/header -# github.com/go-openapi/spec v0.22.5 +# github.com/go-openapi/spec v0.22.6 ## explicit; go 1.25.0 github.com/go-openapi/spec # github.com/go-openapi/strfmt v0.26.3 ## explicit; go 1.25.0 github.com/go-openapi/strfmt github.com/go-openapi/strfmt/internal/bsonlite -# github.com/go-openapi/swag v0.26.0 +# github.com/go-openapi/swag v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag -# github.com/go-openapi/swag/cmdutils v0.26.0 +# github.com/go-openapi/swag/cmdutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/cmdutils -# github.com/go-openapi/swag/conv v0.26.0 +# github.com/go-openapi/swag/conv v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/conv -# github.com/go-openapi/swag/fileutils v0.26.0 +# github.com/go-openapi/swag/fileutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/fileutils -# github.com/go-openapi/swag/jsonname v0.26.0 +# github.com/go-openapi/swag/jsonname v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/jsonname -# github.com/go-openapi/swag/jsonutils v0.26.0 +# github.com/go-openapi/swag/jsonutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/jsonutils github.com/go-openapi/swag/jsonutils/adapters github.com/go-openapi/swag/jsonutils/adapters/ifaces github.com/go-openapi/swag/jsonutils/adapters/stdlib/json -# github.com/go-openapi/swag/loading v0.26.0 +# github.com/go-openapi/swag/loading v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/loading -# github.com/go-openapi/swag/mangling v0.26.0 +# github.com/go-openapi/swag/mangling v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/mangling -# github.com/go-openapi/swag/netutils v0.26.0 +# github.com/go-openapi/swag/netutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/netutils -# github.com/go-openapi/swag/stringutils v0.26.0 +# github.com/go-openapi/swag/stringutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/stringutils -# github.com/go-openapi/swag/typeutils v0.26.0 +# github.com/go-openapi/swag/typeutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/typeutils -# github.com/go-openapi/swag/yamlutils v0.26.0 +# github.com/go-openapi/swag/yamlutils v0.26.1 ## explicit; go 1.25.0 github.com/go-openapi/swag/yamlutils -# github.com/go-openapi/validate v0.25.3 +# github.com/go-openapi/validate v0.26.0 ## explicit; go 1.25.0 github.com/go-openapi/validate # github.com/go-viper/mapstructure/v2 v2.5.0 @@ -410,7 +410,7 @@ github.com/opencontainers/go-digest ## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 -# github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260603165435-b2d908d32435 +# github.com/os-observability/redhat-opentelemetry-collector/configschemas v0.0.0-20260611132535-04e24ebf54ab ## explicit; go 1.26.0 github.com/os-observability/redhat-opentelemetry-collector/configschemas # github.com/pavolloffay/opentelemetry-mcp-server/modules/collectorschema v0.0.0-20260520093054-4540dfe82192 @@ -428,7 +428,7 @@ github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 ## explicit github.com/pmezard/go-difflib/difflib -# github.com/prometheus/alertmanager v0.32.1 +# github.com/prometheus/alertmanager v0.33.0 ## explicit; go 1.25.0 github.com/prometheus/alertmanager/api/v2/client github.com/prometheus/alertmanager/api/v2/client/alert @@ -450,7 +450,7 @@ github.com/prometheus/client_golang/prometheus/promhttp/internal # github.com/prometheus/client_model v0.6.2 ## explicit; go 1.22.0 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.68.1 +# github.com/prometheus/common v0.69.0 ## explicit; go 1.25.0 github.com/prometheus/common/config github.com/prometheus/common/expfmt @@ -481,9 +481,13 @@ github.com/prometheus/prometheus/util/annotations github.com/prometheus/prometheus/util/features github.com/prometheus/prometheus/util/kahansum github.com/prometheus/prometheus/util/strutil -# github.com/rhobs/obs-mcp v0.3.0 -## explicit; go 1.26.0 +# github.com/rhobs/obs-mcp v0.4.0 +## explicit; go 1.26.3 github.com/rhobs/obs-mcp/pkg/alertmanager +github.com/rhobs/obs-mcp/pkg/auth +github.com/rhobs/obs-mcp/pkg/logs +github.com/rhobs/obs-mcp/pkg/logs/discovery +github.com/rhobs/obs-mcp/pkg/logs/loki github.com/rhobs/obs-mcp/pkg/otelcol github.com/rhobs/obs-mcp/pkg/prometheus github.com/rhobs/obs-mcp/pkg/resultutil @@ -715,7 +719,7 @@ go.yaml.in/yaml/v2 # go.yaml.in/yaml/v3 v3.0.4 ## explicit; go 1.16 go.yaml.in/yaml/v3 -# golang.org/x/crypto v0.52.0 +# golang.org/x/crypto v0.53.0 ## explicit; go 1.25.0 golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish @@ -735,7 +739,7 @@ golang.org/x/exp/slices # golang.org/x/mod v0.36.0 ## explicit; go 1.25.0 golang.org/x/mod/semver -# golang.org/x/net v0.55.0 +# golang.org/x/net v0.56.0 ## explicit; go 1.25.0 golang.org/x/net/bpf golang.org/x/net/http/httpguts @@ -767,17 +771,17 @@ golang.org/x/oauth2/internal golang.org/x/sync/errgroup golang.org/x/sync/semaphore golang.org/x/sync/singleflight -# golang.org/x/sys v0.45.0 +# golang.org/x/sys v0.46.0 ## explicit; go 1.25.0 golang.org/x/sys/cpu golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows golang.org/x/sys/windows/registry -# golang.org/x/term v0.43.0 +# golang.org/x/term v0.44.0 ## explicit; go 1.25.0 golang.org/x/term -# golang.org/x/text v0.37.0 +# golang.org/x/text v0.38.0 ## explicit; go 1.25.0 golang.org/x/text/encoding golang.org/x/text/encoding/internal @@ -996,7 +1000,7 @@ helm.sh/helm/v3/pkg/storage/driver helm.sh/helm/v3/pkg/time helm.sh/helm/v3/pkg/time/ctime helm.sh/helm/v3/pkg/uploader -# k8s.io/api v0.36.1 +# k8s.io/api v0.36.2 ## explicit; go 1.26.0 k8s.io/api/admission/v1 k8s.io/api/admission/v1beta1 @@ -1067,7 +1071,7 @@ k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1 k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1 -# k8s.io/apimachinery v0.36.1 +# k8s.io/apimachinery v0.36.2 ## explicit; go 1.26.0 k8s.io/apimachinery/pkg/api/equality k8s.io/apimachinery/pkg/api/errors @@ -1138,13 +1142,13 @@ k8s.io/apimachinery/third_party/forked/golang/reflect # k8s.io/apiserver v0.36.1 ## explicit; go 1.26.0 k8s.io/apiserver/pkg/endpoints/deprecation -# k8s.io/cli-runtime v0.36.1 +# k8s.io/cli-runtime v0.36.2 ## explicit; go 1.26.0 k8s.io/cli-runtime/pkg/genericclioptions k8s.io/cli-runtime/pkg/genericiooptions k8s.io/cli-runtime/pkg/printers k8s.io/cli-runtime/pkg/resource -# k8s.io/client-go v0.36.1 +# k8s.io/client-go v0.36.2 ## explicit; go 1.26.0 k8s.io/client-go/applyconfigurations/admissionregistration/v1 k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1 @@ -1431,7 +1435,7 @@ k8s.io/client-go/util/keyutil k8s.io/client-go/util/retry k8s.io/client-go/util/watchlist k8s.io/client-go/util/workqueue -# k8s.io/component-base v0.36.1 +# k8s.io/component-base v0.36.2 ## explicit; go 1.26.0 k8s.io/component-base/version # k8s.io/klog/v2 v2.140.0 @@ -1463,7 +1467,7 @@ k8s.io/kube-openapi/pkg/util k8s.io/kube-openapi/pkg/util/proto k8s.io/kube-openapi/pkg/util/proto/validation k8s.io/kube-openapi/pkg/validation/spec -# k8s.io/kubectl v0.36.1 +# k8s.io/kubectl v0.36.2 ## explicit; go 1.26.0 k8s.io/kubectl/pkg/cmd/util k8s.io/kubectl/pkg/metricsutil @@ -1474,14 +1478,14 @@ k8s.io/kubectl/pkg/util/openapi k8s.io/kubectl/pkg/util/templates k8s.io/kubectl/pkg/util/term k8s.io/kubectl/pkg/validation -# k8s.io/metrics v0.36.1 +# k8s.io/metrics v0.36.2 ## explicit; go 1.26.0 k8s.io/metrics/pkg/apis/metrics k8s.io/metrics/pkg/apis/metrics/v1alpha1 k8s.io/metrics/pkg/apis/metrics/v1beta1 k8s.io/metrics/pkg/client/clientset/versioned/scheme k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1 -# k8s.io/streaming v0.36.1 +# k8s.io/streaming v0.36.2 ## explicit; go 1.26.0 k8s.io/streaming/pkg/httpstream k8s.io/streaming/pkg/httpstream/spdy