From a805367a350d8b62ef54f97c620b92e8268da3c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:33:14 +0000 Subject: [PATCH 01/22] Initial plan From 01db26c6a3d11e70da22a9b91dbb2b0cefc19700 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:41:55 +0000 Subject: [PATCH 02/22] Add custom API server endpoint configuration - Added APIServerEndpoint field to backend config - Added --api-server-endpoint CLI flag and environment variable support - Updated GetInClusterContext to accept and use custom endpoint - Added Helm chart value config.apiServerEndpoint - Updated deployment template to pass value as argument Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp.go | 3 ++- backend/cmd/server.go | 1 + backend/pkg/config/config.go | 2 ++ backend/pkg/headlampconfig/headlampConfig.go | 1 + backend/pkg/kubeconfig/kubeconfig.go | 9 ++++++++- charts/headlamp/templates/deployment.yaml | 3 +++ charts/headlamp/values.yaml | 4 ++++ 7 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index c56498dc877..05b8b55f891 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -434,7 +434,8 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { config.OidcClientID, config.OidcClientSecret, strings.Join(config.OidcScopes, ","), config.OidcSkipTLSVerify, - config.OidcCACert) + config.OidcCACert, + config.APIServerEndpoint) if err != nil { logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") } diff --git a/backend/cmd/server.go b/backend/cmd/server.go index b7f4cc2fca1..42cda50af99 100644 --- a/backend/cmd/server.go +++ b/backend/cmd/server.go @@ -82,6 +82,7 @@ func buildHeadlampCFG(conf *config.Config, kubeConfigStore kubeconfig.ContextSto return &headlampconfig.HeadlampCFG{ UseInCluster: conf.InCluster, InClusterContextName: conf.InClusterContextName, + APIServerEndpoint: conf.APIServerEndpoint, KubeConfigPath: conf.KubeConfigPath, SkippedKubeContexts: conf.SkippedKubeContexts, ListenAddr: conf.ListenAddr, diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index 06f4a4d1eb6..e21b2cbcb28 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -35,6 +35,7 @@ type Config struct { Version bool `koanf:"version"` InCluster bool `koanf:"in-cluster"` InClusterContextName string `koanf:"in-cluster-context-name"` + APIServerEndpoint string `koanf:"api-server-endpoint"` DevMode bool `koanf:"dev"` InsecureSsl bool `koanf:"insecure-ssl"` LogLevel string `koanf:"log-level"` @@ -415,6 +416,7 @@ func addGeneralFlags(f *flag.FlagSet) { f.Bool("version", false, "Print version information and exit") f.Bool("in-cluster", false, "Set when running from a k8s cluster") f.String("in-cluster-context-name", "main", "Name to use for the in-cluster Kubernetes context") + f.String("api-server-endpoint", "", "Custom Kubernetes API server endpoint (overrides default in-cluster endpoint)") f.Bool("dev", false, "Allow connections from other origins") f.Bool("cache-enabled", false, "K8s cache in backend") f.Bool("no-browser", false, "Disable automatically opening the browser when using embedded frontend") diff --git a/backend/pkg/headlampconfig/headlampConfig.go b/backend/pkg/headlampconfig/headlampConfig.go index 05b51f9bb1b..290f36b2dd5 100644 --- a/backend/pkg/headlampconfig/headlampConfig.go +++ b/backend/pkg/headlampconfig/headlampConfig.go @@ -41,6 +41,7 @@ type HeadlampConfig struct { type HeadlampCFG struct { UseInCluster bool InClusterContextName string + APIServerEndpoint string ListenAddr string CacheEnabled bool DevMode bool diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 780fd2728f3..4e48fc346b1 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1009,14 +1009,21 @@ func GetInClusterContext( oidcScopes string, oidcSkipTLSVerify bool, oidcCACert string, + customAPIServerEndpoint string, ) (*Context, error) { clusterConfig, err := rest.InClusterConfig() if err != nil { return nil, err } + // Use custom API server endpoint if provided, otherwise use default from in-cluster config + apiServerHost := clusterConfig.Host + if customAPIServerEndpoint != "" { + apiServerHost = customAPIServerEndpoint + } + cluster := &api.Cluster{ - Server: clusterConfig.Host, + Server: apiServerHost, CertificateAuthority: clusterConfig.CAFile, CertificateAuthorityData: clusterConfig.CAData, } diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index 9867a564b41..a0f9db39a80 100644 --- a/charts/headlamp/templates/deployment.yaml +++ b/charts/headlamp/templates/deployment.yaml @@ -235,6 +235,9 @@ spec: {{- if .Values.config.inClusterContextName }} - "-in-cluster-context-name={{ .Values.config.inClusterContextName }}" {{- end }} + {{- if .Values.config.apiServerEndpoint }} + - "-api-server-endpoint={{ .Values.config.apiServerEndpoint }}" + {{- end }} {{- end }} {{- with .Values.config.enableHelm }} - "-enable-helm" diff --git a/charts/headlamp/values.yaml b/charts/headlamp/values.yaml index c314fd5141c..ff0f3e8f0bf 100644 --- a/charts/headlamp/values.yaml +++ b/charts/headlamp/values.yaml @@ -34,6 +34,10 @@ extraContainers: [] config: inCluster: true inClusterContextName: "main" + # -- Custom Kubernetes API server endpoint (overrides the default in-cluster endpoint). + # Useful when requests need to pass through a proxy (e.g., kube-oidc-proxy). + # Example: "https://kube-oidc-proxy.example.com:443" + apiServerEndpoint: "" # -- base url path at which headlamp should run baseURL: "" oidc: From 7051d8fe7c830657862b2d36c0dedeb4edee8b72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:42:55 +0000 Subject: [PATCH 03/22] Add tests for API server endpoint configuration - Added test for --api-server-endpoint flag - Added test for HEADLAMP_CONFIG_API_SERVER_ENDPOINT env var - Verified Helm template renders correctly with apiServerEndpoint value Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/pkg/config/config_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/pkg/config/config_test.go b/backend/pkg/config/config_test.go index 46ae2fed0ee..a84ad331fe8 100644 --- a/backend/pkg/config/config_test.go +++ b/backend/pkg/config/config_test.go @@ -55,6 +55,13 @@ func TestParseBasic(t *testing.T) { assert.Equal(t, config.DefaultMeUsernamePath, conf.MeUsernamePath) }, }, + { + name: "api_server_endpoint_flag", + args: []string{"go run ./cmd", "--api-server-endpoint=https://kube-proxy.example.com"}, + verify: func(t *testing.T, conf *config.Config) { + assert.Equal(t, "https://kube-proxy.example.com", conf.APIServerEndpoint) + }, + }, } for _, tt := range tests { @@ -141,6 +148,16 @@ var ParseWithEnvTests = []struct { assert.Equal(t, "warn", conf.LogLevel) }, }, + { + name: "api_server_endpoint_from_env", + args: []string{"go run ./cmd"}, + env: map[string]string{ + "HEADLAMP_CONFIG_API_SERVER_ENDPOINT": "https://kube-oidc-proxy.example.com:443", + }, + verify: func(t *testing.T, conf *config.Config) { + assert.Equal(t, "https://kube-oidc-proxy.example.com:443", conf.APIServerEndpoint) + }, + }, } func TestParseWithEnv(t *testing.T) { From ef33855d0fb6559a2eae1917c2e76e867389f45d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:44:05 +0000 Subject: [PATCH 04/22] Add documentation for custom API server endpoint - Updated Helm chart README with apiServerEndpoint configuration - Added installation example with custom API endpoint - Documented use case with kube-oidc-proxy Co-authored-by: illume <9541+illume@users.noreply.github.com> --- charts/headlamp/README.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index ccff7e2c092..dc45512cd3e 100644 --- a/charts/headlamp/README.md +++ b/charts/headlamp/README.md @@ -53,6 +53,18 @@ $ helm install my-headlamp headlamp/headlamp \ --set ingress.hosts[0].paths[0].path=/ ``` +### Installation with Custom API Server Endpoint + +For use cases where API requests need to pass through a proxy (e.g., kube-oidc-proxy for OIDC authentication from private endpoints): + +```console +$ helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.example.com:443 +``` + +This is particularly useful with managed Kubernetes clusters (like EKS) when using [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle OIDC authentication. + ## Configuration ### Core Parameters @@ -67,15 +79,16 @@ $ helm install my-headlamp headlamp/headlamp \ ### Application Configuration -| Key | Type | Default | Description | -|--------------------|--------|-----------------------|---------------------------------------------------------------------------| -| config.inCluster | bool | `true` | Run Headlamp in-cluster | -| config.baseURL | string | `""` | Base URL path for Headlamp UI | -| config.pluginsDir | string | `"/headlamp/plugins"` | Directory to load Headlamp plugins from | -| config.enableHelm | bool | `false` | Enable Helm operations like install, upgrade and uninstall of Helm charts | -| config.extraArgs | array | `[]` | Additional arguments for Headlamp server | -| config.tlsCertPath | string | `""` | Certificate for serving TLS | -| config.tlsKeyPath | string | `""` | Key for serving TLS | +| Key | Type | Default | Description | +|-------------------------|--------|-----------------------|---------------------------------------------------------------------------| +| config.inCluster | bool | `true` | Run Headlamp in-cluster | +| config.apiServerEndpoint| string | `""` | Custom Kubernetes API server endpoint (overrides default in-cluster endpoint). Useful when requests need to pass through a proxy like kube-oidc-proxy. | +| config.baseURL | string | `""` | Base URL path for Headlamp UI | +| config.pluginsDir | string | `"/headlamp/plugins"` | Directory to load Headlamp plugins from | +| config.enableHelm | bool | `false` | Enable Helm operations like install, upgrade and uninstall of Helm charts | +| config.extraArgs | array | `[]` | Additional arguments for Headlamp server | +| config.tlsCertPath | string | `""` | Certificate for serving TLS | +| config.tlsKeyPath | string | `""` | Key for serving TLS | ### OIDC Configuration From 55cc354e5dd49b4597cb7144274670af83b16a3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:47:55 +0000 Subject: [PATCH 05/22] Feature complete: Allow custom API server endpoint Co-authored-by: illume <9541+illume@users.noreply.github.com> --- frontend/package-lock.json | 61 ++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9cc86d2eae7..eda1d4e7037 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -216,6 +216,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -608,6 +609,7 @@ "version": "11.13.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -648,6 +650,7 @@ "version": "11.13.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -1761,6 +1764,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1826,6 +1830,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.7", @@ -1927,6 +1932,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.6", @@ -2009,6 +2015,7 @@ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.15.0.tgz", "integrity": "sha512-YQEQICNxUEFYp/I/yP58cqihA8yhXaXSNZ1/N0JANu2IlCwoJ4Jzi+S0s4RN7RghpiDyoSMFijROBC5HfpTjiw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.4", "@mui/utils": "^5.16.6", @@ -2536,6 +2543,7 @@ "integrity": "sha512-KoSTtKjzQUQwamcbeCp63Ne9kL7io1WI4+skTJe2chfLz6wsp/Gfg8aKkfs1DuyG1p+zxFDcYpwTWMsNtxqqiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.4.11", "@rspack/lite-tapable": "~1.0.1", @@ -3346,6 +3354,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3489,6 +3498,7 @@ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -3533,6 +3543,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.3.tgz", "integrity": "sha512-1K7l2hkqlWuh5SdaTYPSwMmHJF5dDk5INK+EtiEwUZW4+usWTXZx7QeHuk078oSzTzaVkEFyT3VquK7F0hYkUw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.52.3" }, @@ -3621,6 +3632,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4099,6 +4111,7 @@ "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4108,6 +4121,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "peer": true, "dependencies": { "@types/react": "*" } @@ -4216,6 +4230,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -4248,6 +4263,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.3.0", "@typescript-eslint/types": "8.3.0", @@ -4695,7 +4711,8 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.3.1", @@ -4745,6 +4762,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4789,6 +4807,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5427,6 +5446,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -6256,7 +6276,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -6354,6 +6375,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -6884,6 +6906,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7114,6 +7137,7 @@ "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7185,6 +7209,7 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7239,6 +7264,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7292,6 +7318,7 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -7391,6 +7418,7 @@ "version": "6.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", + "peer": true, "dependencies": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -7458,6 +7486,7 @@ "version": "7.35.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7489,6 +7518,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "peer": true, "engines": { "node": ">=10" }, @@ -7555,6 +7585,7 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -7563,6 +7594,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", + "peer": true, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" @@ -8934,6 +8966,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -9874,6 +9907,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -11398,7 +11432,8 @@ "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "peer": true }, "node_modules/moo-color": { "version": "1.0.3", @@ -11420,6 +11455,7 @@ "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", @@ -11935,7 +11971,8 @@ "node_modules/openapi-types": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", - "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" + "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==", + "peer": true }, "node_modules/opencollective-postinstall": { "version": "2.0.3", @@ -12328,6 +12365,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12630,6 +12668,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12673,6 +12712,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12805,6 +12845,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -12827,6 +12868,7 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13067,7 +13109,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", @@ -13393,6 +13436,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -14067,6 +14111,7 @@ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14717,6 +14762,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14960,6 +15006,7 @@ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", "dev": true, + "peer": true, "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", @@ -15005,6 +15052,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15472,6 +15520,7 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15644,6 +15693,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15657,6 +15707,7 @@ "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "3.0.6", "@vitest/mocker": "3.0.6", From bd2ff57b7e9fb841a5be9589e907629bf7a1c5b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 19:54:09 +0000 Subject: [PATCH 06/22] Address PR feedback: Add Helm tests and in-cluster docs - Added Helm chart test case for apiServerEndpoint configuration - Generated expected template for the test case - Added comprehensive documentation in docs/installation/in-cluster/index.md - Reverted unneeded frontend/package-lock.json changes - All Helm template tests pass Co-authored-by: illume <9541+illume@users.noreply.github.com> --- .../api-server-endpoint.yaml | 130 ++++++++++++++++++ .../tests/test_cases/api-server-endpoint.yaml | 4 + docs/installation/in-cluster/index.md | 53 +++++++ frontend/package-lock.json | 61 +------- 4 files changed, 192 insertions(+), 56 deletions(-) create mode 100644 charts/headlamp/tests/expected_templates/api-server-endpoint.yaml create mode 100644 charts/headlamp/tests/test_cases/api-server-endpoint.yaml diff --git a/charts/headlamp/tests/expected_templates/api-server-endpoint.yaml b/charts/headlamp/tests/expected_templates/api-server-endpoint.yaml new file mode 100644 index 00000000000..bb1b1fac02f --- /dev/null +++ b/charts/headlamp/tests/expected_templates/api-server-endpoint.yaml @@ -0,0 +1,130 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.40.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.40.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.40.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.40.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.40.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.40.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.40.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.40.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.40.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-api-server-endpoint=https://kube-oidc-proxy.example.com:443" + - "-plugins-dir=/headlamp/plugins" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + readinessProbe: + httpGet: + path: "/" + port: http + resources: + {} diff --git a/charts/headlamp/tests/test_cases/api-server-endpoint.yaml b/charts/headlamp/tests/test_cases/api-server-endpoint.yaml new file mode 100644 index 00000000000..6351888c3f8 --- /dev/null +++ b/charts/headlamp/tests/test_cases/api-server-endpoint.yaml @@ -0,0 +1,4 @@ +# This is a test case for apiServerEndpoint in the Headlamp deployment. +# Tests the behavior when a custom API server endpoint is configured. +config: + apiServerEndpoint: "https://kube-oidc-proxy.example.com:443" diff --git a/docs/installation/in-cluster/index.md b/docs/installation/in-cluster/index.md index 2ded20f031f..1a9e84efc24 100644 --- a/docs/installation/in-cluster/index.md +++ b/docs/installation/in-cluster/index.md @@ -47,6 +47,59 @@ kubectl apply -f https://raw.githubusercontent.com/kubernetes-sigs/headlamp/main Headlamp supports optional TLS termination at the backend server. The default is to terminate at the ingress (default) or optionally directly at the Headlamp container. This enables use cases such as NGINX TLS passthrough and transport server. See [tls](./tls.md) for details and usage. +## Custom API Server Endpoint + +By default, when running in-cluster, Headlamp automatically detects and connects to the Kubernetes API server using the in-cluster configuration. However, in some scenarios, you may need to route API requests through a proxy server instead of connecting directly to the API server. + +### Use Cases + +This feature is particularly useful for: + +- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle authentication requests. +- **API Gateway or Proxy Requirements**: When your cluster requires all API traffic to go through a specific gateway or proxy for security, logging, or compliance reasons. +- **Multi-cluster Authentication**: When using a centralized authentication proxy across multiple clusters. + +### Configuration + +You can configure a custom API server endpoint using Helm values: + +```bash +helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.example.com:443 +``` + +Or in your Helm values file: + +```yaml +config: + apiServerEndpoint: "https://kube-oidc-proxy.example.com:443" +``` + +You can also configure it using environment variables or command-line flags: + +- **Environment variable**: `HEADLAMP_CONFIG_API_SERVER_ENDPOINT=https://kube-oidc-proxy.example.com:443` +- **Command-line flag**: `--api-server-endpoint=https://kube-oidc-proxy.example.com:443` + +### Example: Using with kube-oidc-proxy on EKS + +When using Amazon EKS with a private OIDC issuer, you can deploy kube-oidc-proxy to handle authentication and configure Headlamp to route requests through it: + +```bash +# Install kube-oidc-proxy (example) +kubectl apply -f kube-oidc-proxy-deployment.yaml + +# Install Headlamp with custom API endpoint +helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.kube-system.svc.cluster.local:443 \ + --set config.oidc.clientID=your-client-id \ + --set config.oidc.clientSecret=your-client-secret \ + --set config.oidc.issuerURL=https://your-private-issuer.example.com +``` + +For more information about using kube-oidc-proxy with EKS, see the [AWS blog post on consistent OIDC authentication](https://aws.amazon.com/blogs/opensource/consistent-oidc-authentication-across-multiple-eks-clusters-using-kube-oidc-proxy/). + ## Use a non-default kube config file By default, Headlamp uses the default service account from the namespace it is deployed to, and generates a kubeconfig from it named `main`. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index eda1d4e7037..9cc86d2eae7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -216,7 +216,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -609,7 +608,6 @@ "version": "11.13.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -650,7 +648,6 @@ "version": "11.13.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -1764,7 +1761,6 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1830,7 +1826,6 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.7", @@ -1932,7 +1927,6 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.6", @@ -2015,7 +2009,6 @@ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.15.0.tgz", "integrity": "sha512-YQEQICNxUEFYp/I/yP58cqihA8yhXaXSNZ1/N0JANu2IlCwoJ4Jzi+S0s4RN7RghpiDyoSMFijROBC5HfpTjiw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.4", "@mui/utils": "^5.16.6", @@ -2543,7 +2536,6 @@ "integrity": "sha512-KoSTtKjzQUQwamcbeCp63Ne9kL7io1WI4+skTJe2chfLz6wsp/Gfg8aKkfs1DuyG1p+zxFDcYpwTWMsNtxqqiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rspack/core": "1.4.11", "@rspack/lite-tapable": "~1.0.1", @@ -3354,7 +3346,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3498,7 +3489,6 @@ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -3543,7 +3533,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.3.tgz", "integrity": "sha512-1K7l2hkqlWuh5SdaTYPSwMmHJF5dDk5INK+EtiEwUZW4+usWTXZx7QeHuk078oSzTzaVkEFyT3VquK7F0hYkUw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.52.3" }, @@ -3632,7 +3621,6 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4111,7 +4099,6 @@ "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4121,7 +4108,6 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "peer": true, "dependencies": { "@types/react": "*" } @@ -4230,7 +4216,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -4263,7 +4248,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.3.0", "@typescript-eslint/types": "8.3.0", @@ -4711,8 +4695,7 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "peer": true + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, "node_modules/@xyflow/react": { "version": "12.3.1", @@ -4762,7 +4745,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4807,7 +4789,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5446,7 +5427,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -6276,8 +6256,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "peer": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-array": { "version": "3.2.4", @@ -6375,7 +6354,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6906,7 +6884,6 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7137,7 +7114,6 @@ "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7209,7 +7185,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7264,7 +7239,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7318,7 +7292,6 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -7418,7 +7391,6 @@ "version": "6.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", - "peer": true, "dependencies": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -7486,7 +7458,6 @@ "version": "7.35.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7518,7 +7489,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "peer": true, "engines": { "node": ">=10" }, @@ -7585,7 +7555,6 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", - "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -7594,7 +7563,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", - "peer": true, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" @@ -8966,7 +8934,6 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], - "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -9907,7 +9874,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -11432,8 +11398,7 @@ "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", - "peer": true + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" }, "node_modules/moo-color": { "version": "1.0.3", @@ -11455,7 +11420,6 @@ "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", @@ -11971,8 +11935,7 @@ "node_modules/openapi-types": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", - "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==", - "peer": true + "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" }, "node_modules/opencollective-postinstall": { "version": "2.0.3", @@ -12365,7 +12328,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12668,7 +12630,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12712,7 +12673,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12845,7 +12805,6 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -12868,7 +12827,6 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13109,8 +13067,7 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "peer": true + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", @@ -13436,7 +13393,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -14111,7 +14067,6 @@ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14762,7 +14717,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15006,7 +14960,6 @@ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", "dev": true, - "peer": true, "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", @@ -15052,7 +15005,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15520,7 +15472,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15693,7 +15644,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15707,7 +15657,6 @@ "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "3.0.6", "@vitest/mocker": "3.0.6", From 3ca5e5b65c8402e3aaf095cba304e8752430083c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:01:48 +0000 Subject: [PATCH 07/22] Add URL validation for custom API server endpoint - Validate endpoint is absolute URL with scheme and host - Trim whitespace from endpoint before validation - Return clear error message for invalid endpoints - Update help text to clarify flag requires https:// URL and is only used with --in-cluster - Fix linter issues (line length, whitespace) Addresses review comments from copilot-pull-request-reviewer[bot] Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/pkg/config/config.go | 5 ++++- backend/pkg/kubeconfig/kubeconfig.go | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index e21b2cbcb28..be45ddd6872 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -416,7 +416,10 @@ func addGeneralFlags(f *flag.FlagSet) { f.Bool("version", false, "Print version information and exit") f.Bool("in-cluster", false, "Set when running from a k8s cluster") f.String("in-cluster-context-name", "main", "Name to use for the in-cluster Kubernetes context") - f.String("api-server-endpoint", "", "Custom Kubernetes API server endpoint (overrides default in-cluster endpoint)") + f.String( + "api-server-endpoint", "", + "Custom Kubernetes API server endpoint; only used with --in-cluster and must be a full https:// URL", + ) f.Bool("dev", false, "Allow connections from other origins") f.Bool("cache-enabled", false, "K8s cache in backend") f.Bool("no-browser", false, "Disable automatically opening the browser when using embedded frontend") diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 4e48fc346b1..793979413c3 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1018,8 +1018,19 @@ func GetInClusterContext( // Use custom API server endpoint if provided, otherwise use default from in-cluster config apiServerHost := clusterConfig.Host - if customAPIServerEndpoint != "" { - apiServerHost = customAPIServerEndpoint + + if strings.TrimSpace(customAPIServerEndpoint) != "" { + endpoint := strings.TrimSpace(customAPIServerEndpoint) + parsedURL, err := url.Parse(endpoint) + + if err != nil || !parsedURL.IsAbs() || parsedURL.Host == "" { + return nil, fmt.Errorf( + "invalid custom API server endpoint %q: must be an absolute URL with scheme and host", + endpoint, + ) + } + + apiServerHost = endpoint } cluster := &api.Cluster{ From af843d9b6121a4c55f03ccc1e5ac436f9b5c8c4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:03:14 +0000 Subject: [PATCH 08/22] Fix lint issues: Extract helper functions to reduce function length - Extracted validateAPIServerEndpoint helper function for URL validation - Extracted buildOIDCConfig helper function for OIDC config creation - Reduced GetInClusterContext from 67 lines to 59 lines (under 60 line limit) - Fixed line length issues by wrapping long lines - Fixed whitespace linting issues Linter now passes with no errors. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/pkg/kubeconfig/kubeconfig.go | 78 +++++++++++++++++++--------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 793979413c3..5515d544ec2 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1001,6 +1001,46 @@ func splitKubeConfigPath(path string) []string { return strings.Split(path, delimiter) } +// validateAPIServerEndpoint validates and returns a trimmed API server endpoint. +// Returns empty string if endpoint is empty, or an error if endpoint is invalid. +func validateAPIServerEndpoint(endpoint string) (string, error) { + trimmed := strings.TrimSpace(endpoint) + if trimmed == "" { + return "", nil + } + + parsedURL, err := url.Parse(trimmed) + if err != nil || !parsedURL.IsAbs() || parsedURL.Host == "" { + return "", fmt.Errorf( + "invalid custom API server endpoint %q: must be an absolute URL with scheme and host", + trimmed, + ) + } + + return trimmed, nil +} + +// buildOIDCConfig creates an OIDC configuration if the required parameters are provided. +func buildOIDCConfig( + clientID, issuerURL, scopes string, + clientSecret string, + skipTLSVerify bool, + caCert string, +) *OidcConfig { + if clientID == "" || issuerURL == "" || scopes == "" { + return nil + } + + return &OidcConfig{ + ClientID: clientID, + ClientSecret: clientSecret, + IdpIssuerURL: issuerURL, + Scopes: strings.Split(scopes, ","), + SkipTLSVerify: &skipTLSVerify, + CACert: &caCert, + } +} + // GetInClusterContext returns the in-cluster context. func GetInClusterContext( contextName string, @@ -1019,18 +1059,13 @@ func GetInClusterContext( // Use custom API server endpoint if provided, otherwise use default from in-cluster config apiServerHost := clusterConfig.Host - if strings.TrimSpace(customAPIServerEndpoint) != "" { - endpoint := strings.TrimSpace(customAPIServerEndpoint) - parsedURL, err := url.Parse(endpoint) - - if err != nil || !parsedURL.IsAbs() || parsedURL.Host == "" { - return nil, fmt.Errorf( - "invalid custom API server endpoint %q: must be an absolute URL with scheme and host", - endpoint, - ) - } + customEndpoint, err := validateAPIServerEndpoint(customAPIServerEndpoint) + if err != nil { + return nil, err + } - apiServerHost = endpoint + if customEndpoint != "" { + apiServerHost = customEndpoint } cluster := &api.Cluster{ @@ -1051,19 +1086,14 @@ func GetInClusterContext( inClusterAuthInfo := &api.AuthInfo{} - var oidcConf *OidcConfig - - if oidcClientID != "" && oidcIssuerURL != "" && oidcScopes != "" { - // client secret is optional for in-cluster OIDC configuration - oidcConf = &OidcConfig{ - ClientID: oidcClientID, - ClientSecret: oidcClientSecret, - IdpIssuerURL: oidcIssuerURL, - Scopes: strings.Split(oidcScopes, ","), - SkipTLSVerify: &oidcSkipTLSVerify, - CACert: &oidcCACert, - } - } + oidcConf := buildOIDCConfig( + oidcClientID, + oidcIssuerURL, + oidcScopes, + oidcClientSecret, + oidcSkipTLSVerify, + oidcCACert, + ) return &Context{ Name: contextName, From b01a0eaca3f495ba5d6adbbf4cbefd2e2bb0ae8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:18:30 +0000 Subject: [PATCH 09/22] Address review comments: Add https validation, quote Helm values, improve docs - Add https scheme validation (enforce https:// URLs only) - Quote apiServerEndpoint in Helm template to handle special characters - Fix markdown table formatting spacing - Add clarification that endpoint is only for in-cluster mode - Add comprehensive manual testing documentation with kube-oidc-proxy - Add mock integration test simulating proxy endpoint - Regenerate Helm expected template with quoted value Addresses review comments from copilot-pull-request-reviewer[bot] and @illume Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp_test.go | 47 +++++++++++++++++++++++ backend/pkg/kubeconfig/kubeconfig.go | 7 ++++ charts/headlamp/README.md | 10 ++--- charts/headlamp/templates/deployment.yaml | 2 +- docs/installation/in-cluster/index.md | 34 ++++++++++++++++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 52b33841d1b..e3ec50b6861 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1731,3 +1731,50 @@ func TestHandleClusterServiceProxy(t *testing.T) { assert.Equal(t, "OK", rr.Body.String()) } } + +// TestCustomAPIServerEndpoint tests the custom API server endpoint configuration +// with a mock proxy server, simulating kube-oidc-proxy behavior. +func TestCustomAPIServerEndpoint(t *testing.T) { +istrue := true +if os.Getenv("HEADLAMP_RUN_INTEGRATION_TESTS") != strconv.FormatBool(istrue) { +t.Skip("skipping integration test") +} + +// Create a mock "proxy" server that forwards to the actual K8s API +mockK8sAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +w.WriteHeader(http.StatusOK) +w.Write([]byte(`{"kind":"APIVersions","versions":["v1"]}`)) +})) +defer mockK8sAPI.Close() + +// Create a mock proxy server (simulates kube-oidc-proxy) +proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// Simulate proxy forwarding to the actual K8s API +w.WriteHeader(http.StatusOK) +w.Write([]byte(`{"kind":"APIVersions","versions":["v1"]}`)) +})) +defer proxyServer.Close() + +// Test that custom endpoint is used when provided +ctx, err := kubeconfig.GetInClusterContext( +"test-cluster", +"", "", "", "", +false, "", +proxyServer.URL, // Use proxy URL as custom endpoint +) + +// Since we're not actually in a cluster, this will fail to get in-cluster config +// but we're testing the parameter passing works correctly +if err != nil && !strings.Contains(err.Error(), "unable to load in-cluster configuration") { +// If error is not the expected "not in cluster" error, verify it's the validation working +if strings.Contains(err.Error(), "invalid custom API server endpoint") { +// This is expected if URL validation fails +t.Logf("URL validation working: %v", err) +} else { +t.Fatalf("unexpected error: %v", err) +} +} else if err == nil { +// If no error (running in actual cluster), verify custom endpoint was set +assert.Equal(t, proxyServer.URL, ctx.Cluster.Server, "Custom endpoint should be used") +} +} diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 5515d544ec2..2021b96e35d 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1017,6 +1017,13 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { ) } + if parsedURL.Scheme != "https" { + return "", fmt.Errorf( + "invalid custom API server endpoint %q: must be a full https:// URL", + trimmed, + ) + } + return trimmed, nil } diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index dc45512cd3e..e928b9169a6 100644 --- a/charts/headlamp/README.md +++ b/charts/headlamp/README.md @@ -79,11 +79,11 @@ This is particularly useful with managed Kubernetes clusters (like EKS) when usi ### Application Configuration -| Key | Type | Default | Description | -|-------------------------|--------|-----------------------|---------------------------------------------------------------------------| -| config.inCluster | bool | `true` | Run Headlamp in-cluster | -| config.apiServerEndpoint| string | `""` | Custom Kubernetes API server endpoint (overrides default in-cluster endpoint). Useful when requests need to pass through a proxy like kube-oidc-proxy. | -| config.baseURL | string | `""` | Base URL path for Headlamp UI | +| Key | Type | Default | Description | +|--------------------------|--------|-----------------------|---------------------------------------------------------------------------| +| config.inCluster | bool | `true` | Run Headlamp in-cluster | +| config.apiServerEndpoint | string | `""` | Custom Kubernetes API server endpoint (overrides default in-cluster endpoint). Useful when requests need to pass through a proxy like kube-oidc-proxy. | +| config.baseURL | string | `""` | Base URL path for Headlamp UI | | config.pluginsDir | string | `"/headlamp/plugins"` | Directory to load Headlamp plugins from | | config.enableHelm | bool | `false` | Enable Helm operations like install, upgrade and uninstall of Helm charts | | config.extraArgs | array | `[]` | Additional arguments for Headlamp server | diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index a0f9db39a80..70f3dc44c7f 100644 --- a/charts/headlamp/templates/deployment.yaml +++ b/charts/headlamp/templates/deployment.yaml @@ -236,7 +236,7 @@ spec: - "-in-cluster-context-name={{ .Values.config.inClusterContextName }}" {{- end }} {{- if .Values.config.apiServerEndpoint }} - - "-api-server-endpoint={{ .Values.config.apiServerEndpoint }}" + - {{ printf "-api-server-endpoint=%s" .Values.config.apiServerEndpoint | quote }} {{- end }} {{- end }} {{- with .Values.config.enableHelm }} diff --git a/docs/installation/in-cluster/index.md b/docs/installation/in-cluster/index.md index 1a9e84efc24..39d50527484 100644 --- a/docs/installation/in-cluster/index.md +++ b/docs/installation/in-cluster/index.md @@ -81,6 +81,8 @@ You can also configure it using environment variables or command-line flags: - **Environment variable**: `HEADLAMP_CONFIG_API_SERVER_ENDPOINT=https://kube-oidc-proxy.example.com:443` - **Command-line flag**: `--api-server-endpoint=https://kube-oidc-proxy.example.com:443` +**Note**: These options are only used when running Headlamp with in-cluster mode enabled (`--in-cluster` flag or `config.inCluster: true` in Helm values). + ### Example: Using with kube-oidc-proxy on EKS When using Amazon EKS with a private OIDC issuer, you can deploy kube-oidc-proxy to handle authentication and configure Headlamp to route requests through it: @@ -100,6 +102,38 @@ helm install my-headlamp headlamp/headlamp \ For more information about using kube-oidc-proxy with EKS, see the [AWS blog post on consistent OIDC authentication](https://aws.amazon.com/blogs/opensource/consistent-oidc-authentication-across-multiple-eks-clusters-using-kube-oidc-proxy/). +### Testing the Custom API Server Endpoint + +To manually test the custom API server endpoint configuration with kube-oidc-proxy: + +1. **Deploy kube-oidc-proxy** in your cluster following the [kube-oidc-proxy documentation](https://github.com/jetstack/kube-oidc-proxy). + +2. **Install Headlamp** with the custom endpoint pointing to kube-oidc-proxy: + ```bash + helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.kube-system.svc.cluster.local:443 + ``` + +3. **Verify the configuration**: + ```bash + # Check pod arguments include the custom endpoint + kubectl get pod -n kube-system -l app.kubernetes.io/name=headlamp -o jsonpath='{.items[0].spec.containers[0].args}' | grep api-server-endpoint + ``` + +4. **Check Headlamp logs** to confirm it's connecting through the custom endpoint: + ```bash + kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp | grep -i "api server\|endpoint\|proxy" + ``` + +5. **Test API connectivity** by accessing Headlamp and verifying you can list resources from the cluster. + +6. **Test backward compatibility** by installing without the custom endpoint: + ```bash + helm install headlamp-default headlamp/headlamp --namespace kube-system + # Verify it connects to the default in-cluster API server + ``` + ## Use a non-default kube config file By default, Headlamp uses the default service account from the namespace it is deployed to, and generates a kubeconfig from it named `main`. From 0e82afc65cb4945e0ad38e1b02b6547cd6250ebe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:22:34 +0000 Subject: [PATCH 10/22] Fix integration test formatting and use existing istrue constant - Remove duplicate istrue variable definition (use existing const) - Fix test function indentation - Test validates https scheme enforcement - Test skips when HEADLAMP_RUN_INTEGRATION_TESTS not set Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp_test.go | 82 ++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index e3ec50b6861..945eb95ce9f 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1733,48 +1733,48 @@ func TestHandleClusterServiceProxy(t *testing.T) { } // TestCustomAPIServerEndpoint tests the custom API server endpoint configuration -// with a mock proxy server, simulating kube-oidc-proxy behavior. +// with validation logic, simulating kube-oidc-proxy scenarios. func TestCustomAPIServerEndpoint(t *testing.T) { -istrue := true -if os.Getenv("HEADLAMP_RUN_INTEGRATION_TESTS") != strconv.FormatBool(istrue) { -t.Skip("skipping integration test") -} + if os.Getenv("HEADLAMP_RUN_INTEGRATION_TESTS") != strconv.FormatBool(istrue) { + t.Skip("skipping integration test") + } -// Create a mock "proxy" server that forwards to the actual K8s API -mockK8sAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -w.WriteHeader(http.StatusOK) -w.Write([]byte(`{"kind":"APIVersions","versions":["v1"]}`)) -})) -defer mockK8sAPI.Close() - -// Create a mock proxy server (simulates kube-oidc-proxy) -proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// Simulate proxy forwarding to the actual K8s API -w.WriteHeader(http.StatusOK) -w.Write([]byte(`{"kind":"APIVersions","versions":["v1"]}`)) -})) -defer proxyServer.Close() - -// Test that custom endpoint is used when provided -ctx, err := kubeconfig.GetInClusterContext( -"test-cluster", -"", "", "", "", -false, "", -proxyServer.URL, // Use proxy URL as custom endpoint -) + // Test https validation - should reject http URLs + _, err := kubeconfig.GetInClusterContext( + "test-cluster", + "", "", "", "", + false, "", + "http://insecure-proxy.example.com:443", // http scheme should be rejected + ) -// Since we're not actually in a cluster, this will fail to get in-cluster config -// but we're testing the parameter passing works correctly -if err != nil && !strings.Contains(err.Error(), "unable to load in-cluster configuration") { -// If error is not the expected "not in cluster" error, verify it's the validation working -if strings.Contains(err.Error(), "invalid custom API server endpoint") { -// This is expected if URL validation fails -t.Logf("URL validation working: %v", err) -} else { -t.Fatalf("unexpected error: %v", err) -} -} else if err == nil { -// If no error (running in actual cluster), verify custom endpoint was set -assert.Equal(t, proxyServer.URL, ctx.Cluster.Server, "Custom endpoint should be used") -} + // Should fail with validation error (or in-cluster config error if not in cluster) + if err != nil { + if strings.Contains(err.Error(), "must be a full https:// URL") { + t.Logf("https validation working correctly: %v", err) + } else if strings.Contains(err.Error(), "unable to load in-cluster configuration") { + t.Skip("not running in cluster environment, cannot test full functionality") + } else { + t.Fatalf("unexpected error: %v", err) + } + } + + // Test with valid https URL (will fail with in-cluster error if not in cluster) + ctx, err := kubeconfig.GetInClusterContext( + "test-cluster", + "", "", "", "", + false, "", + "https://kube-oidc-proxy.example.com:443", // Valid https endpoint + ) + + if err != nil { + if strings.Contains(err.Error(), "unable to load in-cluster configuration") { + t.Skip("not running in cluster environment, cannot test full functionality") + } + t.Fatalf("unexpected error with valid https endpoint: %v", err) + } + + // If we get here, we're in a cluster and endpoint was accepted + assert.Equal(t, "https://kube-oidc-proxy.example.com:443", ctx.Cluster.Server, + "Custom endpoint should be used") } + From e38dcfb2a9b7f9afa07fcb896d09032e50402a01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:24:07 +0000 Subject: [PATCH 11/22] Fix test logic: Fail if http URL doesn't return error - Add explicit check that http:// URLs must return an error - Improve test logic to properly validate https enforcement - Test now correctly fails if validation is bypassed Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp_test.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 945eb95ce9f..5dd9f795fc4 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1747,15 +1747,17 @@ func TestCustomAPIServerEndpoint(t *testing.T) { "http://insecure-proxy.example.com:443", // http scheme should be rejected ) - // Should fail with validation error (or in-cluster config error if not in cluster) - if err != nil { - if strings.Contains(err.Error(), "must be a full https:// URL") { - t.Logf("https validation working correctly: %v", err) - } else if strings.Contains(err.Error(), "unable to load in-cluster configuration") { - t.Skip("not running in cluster environment, cannot test full functionality") - } else { - t.Fatalf("unexpected error: %v", err) - } + // Should always fail - either with validation error or in-cluster config error + if err == nil { + t.Fatal("Expected error with http:// URL, but got nil") + } + + if strings.Contains(err.Error(), "must be a full https:// URL") { + t.Logf("https validation working correctly: %v", err) + } else if strings.Contains(err.Error(), "unable to load in-cluster configuration") { + t.Skip("not running in cluster environment, cannot test full functionality") + } else { + t.Fatalf("unexpected error: %v", err) } // Test with valid https URL (will fail with in-cluster error if not in cluster) From 84a51f7cdadc974720320c22060a1d74b5e4c734 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:45:43 +0000 Subject: [PATCH 12/22] Add comprehensive URL validation and unit tests - Add validation to reject URLs with embedded credentials (prevents secret leakage in logs) - Add validation to reject URLs with query strings (prevents accidental token exposure) - Add validation to reject URLs with fragments - Add validation to reject URLs with paths (except root /) - Export validateAPIServerEndpoint for testing via export_test.go - Add 13 unit tests covering all validation cases - Add detailed architecture explanation in documentation - Fix markdown table formatting All tests pass. Addresses security concerns from review comments. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp_test.go | 3 +- backend/pkg/kubeconfig/export_test.go | 3 + backend/pkg/kubeconfig/kubeconfig.go | 29 ++++++ backend/pkg/kubeconfig/kubeconfig_test.go | 106 ++++++++++++++++++++++ docs/installation/in-cluster/index.md | 13 +++ 5 files changed, 152 insertions(+), 2 deletions(-) diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 5dd9f795fc4..14f27a4820a 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1767,11 +1767,11 @@ func TestCustomAPIServerEndpoint(t *testing.T) { false, "", "https://kube-oidc-proxy.example.com:443", // Valid https endpoint ) - if err != nil { if strings.Contains(err.Error(), "unable to load in-cluster configuration") { t.Skip("not running in cluster environment, cannot test full functionality") } + t.Fatalf("unexpected error with valid https endpoint: %v", err) } @@ -1779,4 +1779,3 @@ func TestCustomAPIServerEndpoint(t *testing.T) { assert.Equal(t, "https://kube-oidc-proxy.example.com:443", ctx.Cluster.Server, "Custom endpoint should be used") } - diff --git a/backend/pkg/kubeconfig/export_test.go b/backend/pkg/kubeconfig/export_test.go index feae17da909..a864234a1ec 100644 --- a/backend/pkg/kubeconfig/export_test.go +++ b/backend/pkg/kubeconfig/export_test.go @@ -8,6 +8,9 @@ import "net/http" // BuildUserAgent is exported for testing. var BuildUserAgent = buildUserAgent +// ValidateAPIServerEndpoint is exported for testing. +var ValidateAPIServerEndpoint = validateAPIServerEndpoint + // UserAgentRoundTripper is exported for testing. type UserAgentRoundTripper struct { Base roundTripperInterface diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 2021b96e35d..2c2c8b95724 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1024,6 +1024,35 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { ) } + // Disallow embedded credentials, query strings, fragments, and non-root paths + if parsedURL.User != nil { + return "", fmt.Errorf( + "invalid custom API server endpoint %q: must not include user info (credentials)", + trimmed, + ) + } + + if parsedURL.RawQuery != "" { + return "", fmt.Errorf( + "invalid custom API server endpoint %q: must not include a query string", + trimmed, + ) + } + + if parsedURL.Fragment != "" { + return "", fmt.Errorf( + "invalid custom API server endpoint %q: must not include a fragment", + trimmed, + ) + } + + if parsedURL.Path != "" && parsedURL.Path != "/" { + return "", fmt.Errorf( + "invalid custom API server endpoint %q: path must be empty or '/' (scheme+host[:port] only)", + trimmed, + ) + } + return trimmed, nil } diff --git a/backend/pkg/kubeconfig/kubeconfig_test.go b/backend/pkg/kubeconfig/kubeconfig_test.go index e5efce60365..aad377fd1ea 100644 --- a/backend/pkg/kubeconfig/kubeconfig_test.go +++ b/backend/pkg/kubeconfig/kubeconfig_test.go @@ -826,3 +826,109 @@ func TestHandleConfigLoadError(t *testing.T) { }) } } + +func TestValidateAPIServerEndpoint(t *testing.T) { + tests := []struct { + name string + endpoint string + wantResult string + wantErr bool + errContains string + }{ + { + name: "empty string returns empty", + endpoint: "", + wantResult: "", + wantErr: false, + }, + { + name: "whitespace only returns empty", + endpoint: " \t\n ", + wantResult: "", + wantErr: false, + }, + { + name: "valid https URL", + endpoint: "https://kube-oidc-proxy.example.com:443", + wantResult: "https://kube-oidc-proxy.example.com:443", + wantErr: false, + }, + { + name: "valid https URL with whitespace is trimmed", + endpoint: " https://kube-oidc-proxy.example.com:443 ", + wantResult: "https://kube-oidc-proxy.example.com:443", + wantErr: false, + }, + { + name: "http URL is rejected", + endpoint: "http://insecure-proxy.example.com:443", + wantErr: true, + errContains: "must be a full https:// URL", + }, + { + name: "missing scheme is rejected", + endpoint: "kube-oidc-proxy.example.com:443", + wantErr: true, + errContains: "must be an absolute URL with scheme and host", + }, + { + name: "relative URL is rejected", + endpoint: "/path/to/proxy", + wantErr: true, + errContains: "must be an absolute URL with scheme and host", + }, + { + name: "URL with embedded credentials is rejected", + endpoint: "https://user:password@proxy.example.com:443", + wantErr: true, + errContains: "must not include user info (credentials)", + }, + { + name: "URL with query string is rejected", + endpoint: "https://proxy.example.com:443?token=secret", + wantErr: true, + errContains: "must not include a query string", + }, + { + name: "URL with fragment is rejected", + endpoint: "https://proxy.example.com:443#section", + wantErr: true, + errContains: "must not include a fragment", + }, + { + name: "URL with path is rejected", + endpoint: "https://proxy.example.com:443/api/v1", + wantErr: true, + errContains: "path must be empty or '/'", + }, + { + name: "URL with root path is allowed", + endpoint: "https://proxy.example.com:443/", + wantResult: "https://proxy.example.com:443/", + wantErr: false, + }, + { + name: "URL without port", + endpoint: "https://proxy.example.com", + wantResult: "https://proxy.example.com", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := kubeconfig.ValidateAPIServerEndpoint(tt.endpoint) + + if tt.wantErr { + require.Error(t, err) + + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantResult, result) + } + }) + } +} diff --git a/docs/installation/in-cluster/index.md b/docs/installation/in-cluster/index.md index 39d50527484..10d073820ee 100644 --- a/docs/installation/in-cluster/index.md +++ b/docs/installation/in-cluster/index.md @@ -104,6 +104,19 @@ For more information about using kube-oidc-proxy with EKS, see the [AWS blog pos ### Testing the Custom API Server Endpoint +The custom API server endpoint feature allows you to configure Headlamp to route all Kubernetes API requests through an intermediate proxy server instead of connecting directly to the cluster's API server. This is useful in architectures where: + +- **Private OIDC providers**: Your OIDC identity provider is on a private network and needs a proxy like kube-oidc-proxy to handle authentication requests before they reach the API server. +- **Centralized authentication**: You have multiple clusters and want to use a single authentication proxy to provide consistent OIDC authentication across all of them. +- **Security policies**: Your organization requires all API traffic to go through a specific gateway for auditing, logging, or compliance reasons. +- **Network segmentation**: The cluster API server is not directly accessible from where Headlamp is deployed and must go through a proxy. + +**Benefits:** +- Enables using OIDC authentication with private identity providers in managed Kubernetes services (like AWS EKS) +- Centralizes authentication logic across multiple clusters +- Provides a single point for API traffic monitoring and control +- Maintains security by enforcing https:// connections and validating endpoint URLs + To manually test the custom API server endpoint configuration with kube-oidc-proxy: 1. **Deploy kube-oidc-proxy** in your cluster following the [kube-oidc-proxy documentation](https://github.com/jetstack/kube-oidc-proxy). From 940030ae9731b7734f94ea468b0db598927eec9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:01:20 +0000 Subject: [PATCH 13/22] Mention multiple cloud vendors and fix if-else chain - Update README and docs to mention AWS EKS, Azure AKS, and Google GKE - Change example section title to "Managed Kubernetes" instead of just "EKS" - Add note that configuration works with AKS and GKE - Fix ifElseChain lint suggestion by using switch statement - All linter warnings addressed Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp_test.go | 7 ++++--- charts/headlamp/README.md | 2 +- docs/installation/in-cluster/index.md | 12 ++++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 14f27a4820a..4af9f4d254d 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1752,11 +1752,12 @@ func TestCustomAPIServerEndpoint(t *testing.T) { t.Fatal("Expected error with http:// URL, but got nil") } - if strings.Contains(err.Error(), "must be a full https:// URL") { + switch { + case strings.Contains(err.Error(), "must be a full https:// URL"): t.Logf("https validation working correctly: %v", err) - } else if strings.Contains(err.Error(), "unable to load in-cluster configuration") { + case strings.Contains(err.Error(), "unable to load in-cluster configuration"): t.Skip("not running in cluster environment, cannot test full functionality") - } else { + default: t.Fatalf("unexpected error: %v", err) } diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index e928b9169a6..59922321a14 100644 --- a/charts/headlamp/README.md +++ b/charts/headlamp/README.md @@ -63,7 +63,7 @@ $ helm install my-headlamp headlamp/headlamp \ --set config.apiServerEndpoint=https://kube-oidc-proxy.example.com:443 ``` -This is particularly useful with managed Kubernetes clusters (like EKS) when using [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle OIDC authentication. +This is particularly useful with managed Kubernetes clusters (like AWS EKS, Azure AKS, or Google GKE) when using [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle OIDC authentication with private identity providers. ## Configuration diff --git a/docs/installation/in-cluster/index.md b/docs/installation/in-cluster/index.md index 10d073820ee..dfdb814d043 100644 --- a/docs/installation/in-cluster/index.md +++ b/docs/installation/in-cluster/index.md @@ -55,7 +55,7 @@ By default, when running in-cluster, Headlamp automatically detects and connects This feature is particularly useful for: -- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle authentication requests. +- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS, Azure AKS, or Google GKE) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle authentication requests. - **API Gateway or Proxy Requirements**: When your cluster requires all API traffic to go through a specific gateway or proxy for security, logging, or compliance reasons. - **Multi-cluster Authentication**: When using a centralized authentication proxy across multiple clusters. @@ -83,9 +83,11 @@ You can also configure it using environment variables or command-line flags: **Note**: These options are only used when running Headlamp with in-cluster mode enabled (`--in-cluster` flag or `config.inCluster: true` in Helm values). -### Example: Using with kube-oidc-proxy on EKS +### Example: Using with kube-oidc-proxy on Managed Kubernetes -When using Amazon EKS with a private OIDC issuer, you can deploy kube-oidc-proxy to handle authentication and configure Headlamp to route requests through it: +When using managed Kubernetes services (AWS EKS, Azure AKS, Google GKE) with a private OIDC issuer, you can deploy kube-oidc-proxy to handle authentication and configure Headlamp to route requests through it. + +**Example with AWS EKS:** ```bash # Install kube-oidc-proxy (example) @@ -100,7 +102,9 @@ helm install my-headlamp headlamp/headlamp \ --set config.oidc.issuerURL=https://your-private-issuer.example.com ``` -For more information about using kube-oidc-proxy with EKS, see the [AWS blog post on consistent OIDC authentication](https://aws.amazon.com/blogs/opensource/consistent-oidc-authentication-across-multiple-eks-clusters-using-kube-oidc-proxy/). +For more information about using kube-oidc-proxy with managed Kubernetes clusters, see: +- AWS EKS: [Consistent OIDC authentication across multiple EKS clusters](https://aws.amazon.com/blogs/opensource/consistent-oidc-authentication-across-multiple-eks-clusters-using-kube-oidc-proxy/) +- The configuration works similarly with Azure AKS and Google GKE when using private OIDC providers ### Testing the Custom API Server Endpoint From 50424595f60622d02e21eec8b2dd4a2d1f311991 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:06:07 +0000 Subject: [PATCH 14/22] Co-authored-by: illume <9541+illume@users.noreply.github.com> --- frontend/package-lock.json | 61 ++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9cc86d2eae7..eda1d4e7037 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -216,6 +216,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -608,6 +609,7 @@ "version": "11.13.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -648,6 +650,7 @@ "version": "11.13.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -1761,6 +1764,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1826,6 +1830,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.7", @@ -1927,6 +1932,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.6", @@ -2009,6 +2015,7 @@ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.15.0.tgz", "integrity": "sha512-YQEQICNxUEFYp/I/yP58cqihA8yhXaXSNZ1/N0JANu2IlCwoJ4Jzi+S0s4RN7RghpiDyoSMFijROBC5HfpTjiw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.4", "@mui/utils": "^5.16.6", @@ -2536,6 +2543,7 @@ "integrity": "sha512-KoSTtKjzQUQwamcbeCp63Ne9kL7io1WI4+skTJe2chfLz6wsp/Gfg8aKkfs1DuyG1p+zxFDcYpwTWMsNtxqqiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.4.11", "@rspack/lite-tapable": "~1.0.1", @@ -3346,6 +3354,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3489,6 +3498,7 @@ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -3533,6 +3543,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.3.tgz", "integrity": "sha512-1K7l2hkqlWuh5SdaTYPSwMmHJF5dDk5INK+EtiEwUZW4+usWTXZx7QeHuk078oSzTzaVkEFyT3VquK7F0hYkUw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.52.3" }, @@ -3621,6 +3632,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4099,6 +4111,7 @@ "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4108,6 +4121,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "peer": true, "dependencies": { "@types/react": "*" } @@ -4216,6 +4230,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -4248,6 +4263,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.3.0", "@typescript-eslint/types": "8.3.0", @@ -4695,7 +4711,8 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.3.1", @@ -4745,6 +4762,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4789,6 +4807,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5427,6 +5446,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -6256,7 +6276,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -6354,6 +6375,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -6884,6 +6906,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7114,6 +7137,7 @@ "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7185,6 +7209,7 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7239,6 +7264,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7292,6 +7318,7 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -7391,6 +7418,7 @@ "version": "6.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", + "peer": true, "dependencies": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -7458,6 +7486,7 @@ "version": "7.35.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7489,6 +7518,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "peer": true, "engines": { "node": ">=10" }, @@ -7555,6 +7585,7 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -7563,6 +7594,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", + "peer": true, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" @@ -8934,6 +8966,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -9874,6 +9907,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -11398,7 +11432,8 @@ "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "peer": true }, "node_modules/moo-color": { "version": "1.0.3", @@ -11420,6 +11455,7 @@ "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", @@ -11935,7 +11971,8 @@ "node_modules/openapi-types": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", - "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" + "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==", + "peer": true }, "node_modules/opencollective-postinstall": { "version": "2.0.3", @@ -12328,6 +12365,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12630,6 +12668,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12673,6 +12712,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12805,6 +12845,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -12827,6 +12868,7 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13067,7 +13109,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", @@ -13393,6 +13436,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -14067,6 +14111,7 @@ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14717,6 +14762,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14960,6 +15006,7 @@ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", "dev": true, + "peer": true, "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", @@ -15005,6 +15052,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15472,6 +15520,7 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15644,6 +15693,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15657,6 +15707,7 @@ "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "3.0.6", "@vitest/mocker": "3.0.6", From caad2ff10bac5470190aca56d5c75336769b1cd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:36:35 +0000 Subject: [PATCH 15/22] Move custom API endpoint docs to separate file - Create docs/installation/in-cluster/custom-api-endpoint.md - Extract 104 lines of custom API endpoint documentation - Replace index.md section with brief intro and link - Promote headings to appropriate levels for standalone doc - Follows pattern of tls.md and other separate doc files Addresses review comment to separate this content. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- .../in-cluster/custom-api-endpoint.md | 104 ++++++++++++++++++ docs/installation/in-cluster/index.md | 102 +---------------- 2 files changed, 105 insertions(+), 101 deletions(-) create mode 100644 docs/installation/in-cluster/custom-api-endpoint.md diff --git a/docs/installation/in-cluster/custom-api-endpoint.md b/docs/installation/in-cluster/custom-api-endpoint.md new file mode 100644 index 00000000000..811ad80a261 --- /dev/null +++ b/docs/installation/in-cluster/custom-api-endpoint.md @@ -0,0 +1,104 @@ +# Custom API Server Endpoint + +By default, when running in-cluster, Headlamp automatically detects and connects to the Kubernetes API server using the in-cluster configuration. However, in some scenarios, you may need to route API requests through a proxy server instead of connecting directly to the API server. + +## Use Cases + +This feature is particularly useful for: + +- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS, Azure AKS, or Google GKE) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle authentication requests. +- **API Gateway or Proxy Requirements**: When your cluster requires all API traffic to go through a specific gateway or proxy for security, logging, or compliance reasons. +- **Multi-cluster Authentication**: When using a centralized authentication proxy across multiple clusters. + +## Configuration + +You can configure a custom API server endpoint using Helm values: + +```bash +helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.example.com:443 +``` + +Or in your Helm values file: + +```yaml +config: + apiServerEndpoint: "https://kube-oidc-proxy.example.com:443" +``` + +You can also configure it using environment variables or command-line flags: + +- **Environment variable**: `HEADLAMP_CONFIG_API_SERVER_ENDPOINT=https://kube-oidc-proxy.example.com:443` +- **Command-line flag**: `--api-server-endpoint=https://kube-oidc-proxy.example.com:443` + +**Note**: These options are only used when running Headlamp with in-cluster mode enabled (`--in-cluster` flag or `config.inCluster: true` in Helm values). + +## Example: Using with kube-oidc-proxy on Managed Kubernetes + +When using managed Kubernetes services (AWS EKS, Azure AKS, Google GKE) with a private OIDC issuer, you can deploy kube-oidc-proxy to handle authentication and configure Headlamp to route requests through it. + +**Example with AWS EKS:** + +```bash +# Install kube-oidc-proxy (example) +kubectl apply -f kube-oidc-proxy-deployment.yaml + +# Install Headlamp with custom API endpoint +helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.kube-system.svc.cluster.local:443 \ + --set config.oidc.clientID=your-client-id \ + --set config.oidc.clientSecret=your-client-secret \ + --set config.oidc.issuerURL=https://your-private-issuer.example.com +``` + +For more information about using kube-oidc-proxy with managed Kubernetes clusters, see: +- AWS EKS: [Consistent OIDC authentication across multiple EKS clusters](https://aws.amazon.com/blogs/opensource/consistent-oidc-authentication-across-multiple-eks-clusters-using-kube-oidc-proxy/) +- The configuration works similarly with Azure AKS and Google GKE when using private OIDC providers + +## Testing the Custom API Server Endpoint + +The custom API server endpoint feature allows you to configure Headlamp to route all Kubernetes API requests through an intermediate proxy server instead of connecting directly to the cluster's API server. This is useful in architectures where: + +- **Private OIDC providers**: Your OIDC identity provider is on a private network and needs a proxy like kube-oidc-proxy to handle authentication requests before they reach the API server. +- **Centralized authentication**: You have multiple clusters and want to use a single authentication proxy to provide consistent OIDC authentication across all of them. +- **Security policies**: Your organization requires all API traffic to go through a specific gateway for auditing, logging, or compliance reasons. +- **Network segmentation**: The cluster API server is not directly accessible from where Headlamp is deployed and must go through a proxy. + +**Benefits:** +- Enables using OIDC authentication with private identity providers in managed Kubernetes services (like AWS EKS) +- Centralizes authentication logic across multiple clusters +- Provides a single point for API traffic monitoring and control +- Maintains security by enforcing https:// connections and validating endpoint URLs + +To manually test the custom API server endpoint configuration with kube-oidc-proxy: + +1. **Deploy kube-oidc-proxy** in your cluster following the [kube-oidc-proxy documentation](https://github.com/jetstack/kube-oidc-proxy). + +2. **Install Headlamp** with the custom endpoint pointing to kube-oidc-proxy: + ```bash + helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.apiServerEndpoint=https://kube-oidc-proxy.kube-system.svc.cluster.local:443 + ``` + +3. **Verify the configuration**: + ```bash + # Check pod arguments include the custom endpoint + kubectl get pod -n kube-system -l app.kubernetes.io/name=headlamp -o jsonpath='{.items[0].spec.containers[0].args}' | grep api-server-endpoint + ``` + +4. **Check Headlamp logs** to confirm it's connecting through the custom endpoint: + ```bash + kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp | grep -i "api server\|endpoint\|proxy" + ``` + +5. **Test API connectivity** by accessing Headlamp and verifying you can list resources from the cluster. + +6. **Test backward compatibility** by installing without the custom endpoint: + ```bash + helm install headlamp-default headlamp/headlamp --namespace kube-system + # Verify it connects to the default in-cluster API server + ``` + diff --git a/docs/installation/in-cluster/index.md b/docs/installation/in-cluster/index.md index dfdb814d043..46aaebb8794 100644 --- a/docs/installation/in-cluster/index.md +++ b/docs/installation/in-cluster/index.md @@ -49,107 +49,7 @@ Headlamp supports optional TLS termination at the backend server. The default is ## Custom API Server Endpoint -By default, when running in-cluster, Headlamp automatically detects and connects to the Kubernetes API server using the in-cluster configuration. However, in some scenarios, you may need to route API requests through a proxy server instead of connecting directly to the API server. - -### Use Cases - -This feature is particularly useful for: - -- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS, Azure AKS, or Google GKE) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle authentication requests. -- **API Gateway or Proxy Requirements**: When your cluster requires all API traffic to go through a specific gateway or proxy for security, logging, or compliance reasons. -- **Multi-cluster Authentication**: When using a centralized authentication proxy across multiple clusters. - -### Configuration - -You can configure a custom API server endpoint using Helm values: - -```bash -helm install my-headlamp headlamp/headlamp \ - --namespace kube-system \ - --set config.apiServerEndpoint=https://kube-oidc-proxy.example.com:443 -``` - -Or in your Helm values file: - -```yaml -config: - apiServerEndpoint: "https://kube-oidc-proxy.example.com:443" -``` - -You can also configure it using environment variables or command-line flags: - -- **Environment variable**: `HEADLAMP_CONFIG_API_SERVER_ENDPOINT=https://kube-oidc-proxy.example.com:443` -- **Command-line flag**: `--api-server-endpoint=https://kube-oidc-proxy.example.com:443` - -**Note**: These options are only used when running Headlamp with in-cluster mode enabled (`--in-cluster` flag or `config.inCluster: true` in Helm values). - -### Example: Using with kube-oidc-proxy on Managed Kubernetes - -When using managed Kubernetes services (AWS EKS, Azure AKS, Google GKE) with a private OIDC issuer, you can deploy kube-oidc-proxy to handle authentication and configure Headlamp to route requests through it. - -**Example with AWS EKS:** - -```bash -# Install kube-oidc-proxy (example) -kubectl apply -f kube-oidc-proxy-deployment.yaml - -# Install Headlamp with custom API endpoint -helm install my-headlamp headlamp/headlamp \ - --namespace kube-system \ - --set config.apiServerEndpoint=https://kube-oidc-proxy.kube-system.svc.cluster.local:443 \ - --set config.oidc.clientID=your-client-id \ - --set config.oidc.clientSecret=your-client-secret \ - --set config.oidc.issuerURL=https://your-private-issuer.example.com -``` - -For more information about using kube-oidc-proxy with managed Kubernetes clusters, see: -- AWS EKS: [Consistent OIDC authentication across multiple EKS clusters](https://aws.amazon.com/blogs/opensource/consistent-oidc-authentication-across-multiple-eks-clusters-using-kube-oidc-proxy/) -- The configuration works similarly with Azure AKS and Google GKE when using private OIDC providers - -### Testing the Custom API Server Endpoint - -The custom API server endpoint feature allows you to configure Headlamp to route all Kubernetes API requests through an intermediate proxy server instead of connecting directly to the cluster's API server. This is useful in architectures where: - -- **Private OIDC providers**: Your OIDC identity provider is on a private network and needs a proxy like kube-oidc-proxy to handle authentication requests before they reach the API server. -- **Centralized authentication**: You have multiple clusters and want to use a single authentication proxy to provide consistent OIDC authentication across all of them. -- **Security policies**: Your organization requires all API traffic to go through a specific gateway for auditing, logging, or compliance reasons. -- **Network segmentation**: The cluster API server is not directly accessible from where Headlamp is deployed and must go through a proxy. - -**Benefits:** -- Enables using OIDC authentication with private identity providers in managed Kubernetes services (like AWS EKS) -- Centralizes authentication logic across multiple clusters -- Provides a single point for API traffic monitoring and control -- Maintains security by enforcing https:// connections and validating endpoint URLs - -To manually test the custom API server endpoint configuration with kube-oidc-proxy: - -1. **Deploy kube-oidc-proxy** in your cluster following the [kube-oidc-proxy documentation](https://github.com/jetstack/kube-oidc-proxy). - -2. **Install Headlamp** with the custom endpoint pointing to kube-oidc-proxy: - ```bash - helm install my-headlamp headlamp/headlamp \ - --namespace kube-system \ - --set config.apiServerEndpoint=https://kube-oidc-proxy.kube-system.svc.cluster.local:443 - ``` - -3. **Verify the configuration**: - ```bash - # Check pod arguments include the custom endpoint - kubectl get pod -n kube-system -l app.kubernetes.io/name=headlamp -o jsonpath='{.items[0].spec.containers[0].args}' | grep api-server-endpoint - ``` - -4. **Check Headlamp logs** to confirm it's connecting through the custom endpoint: - ```bash - kubectl logs -n kube-system -l app.kubernetes.io/name=headlamp | grep -i "api server\|endpoint\|proxy" - ``` - -5. **Test API connectivity** by accessing Headlamp and verifying you can list resources from the cluster. - -6. **Test backward compatibility** by installing without the custom endpoint: - ```bash - helm install headlamp-default headlamp/headlamp --namespace kube-system - # Verify it connects to the default in-cluster API server - ``` +For scenarios where API requests need to be routed through a proxy server (such as when using kube-oidc-proxy with private OIDC providers on managed Kubernetes services like AWS EKS, Azure AKS, or Google GKE), Headlamp supports configuring a custom API server endpoint. See [custom-api-endpoint](./custom-api-endpoint.md) for details and usage. ## Use a non-default kube config file From 2007e3d0db11dac8a07aa7127d46cd9318d6e586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:40:47 +0000 Subject: [PATCH 16/22] Fix critical nil pointer bug and improve validation - Fix nil pointer dereference when GetInClusterContext returns error - Add else block to only use context when err == nil - Move validation before InClusterConfig for consistent error messages - Add Hostname() check to reject URLs like https://:443 - Add test case for empty hostname validation - Improve integration test to always test validation - Revert unintended frontend/package-lock.json changes Addresses critical bug and review comments. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp.go | 20 ++++---- backend/cmd/headlamp_test.go | 26 ++++------ backend/pkg/kubeconfig/kubeconfig.go | 13 ++--- backend/pkg/kubeconfig/kubeconfig_test.go | 6 +++ frontend/package-lock.json | 61 ++--------------------- 5 files changed, 37 insertions(+), 89 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 05b8b55f891..cdc965f659a 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -438,18 +438,18 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { config.APIServerEndpoint) if err != nil { logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") - } - - context.Source = kubeconfig.InCluster + } else { + context.Source = kubeconfig.InCluster - err = context.SetupProxy() - if err != nil { - logger.Log(logger.LevelError, nil, err, "Failed to setup proxy for in-cluster context") - } + err = context.SetupProxy() + if err != nil { + logger.Log(logger.LevelError, nil, err, "Failed to setup proxy for in-cluster context") + } - err = config.KubeConfigStore.AddContext(context) - if err != nil { - logger.Log(logger.LevelError, nil, err, "Failed to add in-cluster context") + err = config.KubeConfigStore.AddContext(context) + if err != nil { + logger.Log(logger.LevelError, nil, err, "Failed to add in-cluster context") + } } } diff --git a/backend/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 4af9f4d254d..4b7362b5b9a 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1735,11 +1735,8 @@ func TestHandleClusterServiceProxy(t *testing.T) { // TestCustomAPIServerEndpoint tests the custom API server endpoint configuration // with validation logic, simulating kube-oidc-proxy scenarios. func TestCustomAPIServerEndpoint(t *testing.T) { - if os.Getenv("HEADLAMP_RUN_INTEGRATION_TESTS") != strconv.FormatBool(istrue) { - t.Skip("skipping integration test") - } - - // Test https validation - should reject http URLs + // Test https validation directly - should reject http URLs + // Since validation now happens before InClusterConfig, this will always test validation _, err := kubeconfig.GetInClusterContext( "test-cluster", "", "", "", "", @@ -1747,21 +1744,16 @@ func TestCustomAPIServerEndpoint(t *testing.T) { "http://insecure-proxy.example.com:443", // http scheme should be rejected ) - // Should always fail - either with validation error or in-cluster config error - if err == nil { - t.Fatal("Expected error with http:// URL, but got nil") - } + // Should always fail with validation error (validation happens before in-cluster check) + require.Error(t, err, "Expected error with http:// URL") + assert.Contains(t, err.Error(), "must be a full https:// URL", + "Error should be about https requirement") - switch { - case strings.Contains(err.Error(), "must be a full https:// URL"): - t.Logf("https validation working correctly: %v", err) - case strings.Contains(err.Error(), "unable to load in-cluster configuration"): - t.Skip("not running in cluster environment, cannot test full functionality") - default: - t.Fatalf("unexpected error: %v", err) + // Test with valid https URL - will fail with in-cluster error if not actually in cluster + if os.Getenv("HEADLAMP_RUN_INTEGRATION_TESTS") != strconv.FormatBool(istrue) { + t.Skip("skipping full integration test (validation already tested above)") } - // Test with valid https URL (will fail with in-cluster error if not in cluster) ctx, err := kubeconfig.GetInClusterContext( "test-cluster", "", "", "", "", diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 2c2c8b95724..aead86f516e 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1010,7 +1010,7 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { } parsedURL, err := url.Parse(trimmed) - if err != nil || !parsedURL.IsAbs() || parsedURL.Host == "" { + if err != nil || !parsedURL.IsAbs() || parsedURL.Host == "" || parsedURL.Hostname() == "" { return "", fmt.Errorf( "invalid custom API server endpoint %q: must be an absolute URL with scheme and host", trimmed, @@ -1087,19 +1087,20 @@ func GetInClusterContext( oidcCACert string, customAPIServerEndpoint string, ) (*Context, error) { - clusterConfig, err := rest.InClusterConfig() + // Validate custom endpoint first, before attempting to load in-cluster config + customEndpoint, err := validateAPIServerEndpoint(customAPIServerEndpoint) if err != nil { return nil, err } - // Use custom API server endpoint if provided, otherwise use default from in-cluster config - apiServerHost := clusterConfig.Host - - customEndpoint, err := validateAPIServerEndpoint(customAPIServerEndpoint) + clusterConfig, err := rest.InClusterConfig() if err != nil { return nil, err } + // Use custom API server endpoint if provided, otherwise use default from in-cluster config + apiServerHost := clusterConfig.Host + if customEndpoint != "" { apiServerHost = customEndpoint } diff --git a/backend/pkg/kubeconfig/kubeconfig_test.go b/backend/pkg/kubeconfig/kubeconfig_test.go index aad377fd1ea..41d8fef103e 100644 --- a/backend/pkg/kubeconfig/kubeconfig_test.go +++ b/backend/pkg/kubeconfig/kubeconfig_test.go @@ -883,6 +883,12 @@ func TestValidateAPIServerEndpoint(t *testing.T) { wantErr: true, errContains: "must not include user info (credentials)", }, + { + name: "URL with empty hostname is rejected", + endpoint: "https://:443", + wantErr: true, + errContains: "must be an absolute URL with scheme and host", + }, { name: "URL with query string is rejected", endpoint: "https://proxy.example.com:443?token=secret", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index eda1d4e7037..9cc86d2eae7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -216,7 +216,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -609,7 +608,6 @@ "version": "11.13.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -650,7 +648,6 @@ "version": "11.13.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -1764,7 +1761,6 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1830,7 +1826,6 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.7", @@ -1932,7 +1927,6 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.6", @@ -2015,7 +2009,6 @@ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.15.0.tgz", "integrity": "sha512-YQEQICNxUEFYp/I/yP58cqihA8yhXaXSNZ1/N0JANu2IlCwoJ4Jzi+S0s4RN7RghpiDyoSMFijROBC5HfpTjiw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.4", "@mui/utils": "^5.16.6", @@ -2543,7 +2536,6 @@ "integrity": "sha512-KoSTtKjzQUQwamcbeCp63Ne9kL7io1WI4+skTJe2chfLz6wsp/Gfg8aKkfs1DuyG1p+zxFDcYpwTWMsNtxqqiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rspack/core": "1.4.11", "@rspack/lite-tapable": "~1.0.1", @@ -3354,7 +3346,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3498,7 +3489,6 @@ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -3543,7 +3533,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.3.tgz", "integrity": "sha512-1K7l2hkqlWuh5SdaTYPSwMmHJF5dDk5INK+EtiEwUZW4+usWTXZx7QeHuk078oSzTzaVkEFyT3VquK7F0hYkUw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.52.3" }, @@ -3632,7 +3621,6 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4111,7 +4099,6 @@ "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4121,7 +4108,6 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "peer": true, "dependencies": { "@types/react": "*" } @@ -4230,7 +4216,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -4263,7 +4248,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.3.0", "@typescript-eslint/types": "8.3.0", @@ -4711,8 +4695,7 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "peer": true + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" }, "node_modules/@xyflow/react": { "version": "12.3.1", @@ -4762,7 +4745,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4807,7 +4789,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5446,7 +5427,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -6276,8 +6256,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "peer": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-array": { "version": "3.2.4", @@ -6375,7 +6354,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -6906,7 +6884,6 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, - "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7137,7 +7114,6 @@ "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7209,7 +7185,6 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7264,7 +7239,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7318,7 +7292,6 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -7418,7 +7391,6 @@ "version": "6.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", - "peer": true, "dependencies": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -7486,7 +7458,6 @@ "version": "7.35.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7518,7 +7489,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "peer": true, "engines": { "node": ">=10" }, @@ -7585,7 +7555,6 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", - "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -7594,7 +7563,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", - "peer": true, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" @@ -8966,7 +8934,6 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], - "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -9907,7 +9874,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -11432,8 +11398,7 @@ "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", - "peer": true + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" }, "node_modules/moo-color": { "version": "1.0.3", @@ -11455,7 +11420,6 @@ "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", @@ -11971,8 +11935,7 @@ "node_modules/openapi-types": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", - "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==", - "peer": true + "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" }, "node_modules/opencollective-postinstall": { "version": "2.0.3", @@ -12365,7 +12328,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12668,7 +12630,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12712,7 +12673,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12845,7 +12805,6 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -12868,7 +12827,6 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13109,8 +13067,7 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "peer": true + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", @@ -13436,7 +13393,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -14111,7 +14067,6 @@ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14762,7 +14717,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15006,7 +14960,6 @@ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", "dev": true, - "peer": true, "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", @@ -15052,7 +15005,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15520,7 +15472,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15693,7 +15644,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15707,7 +15657,6 @@ "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "3.0.6", "@vitest/mocker": "3.0.6", From 738c6e0f94e3508298a34e355b92cc663b0fdffd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:05:35 +0000 Subject: [PATCH 17/22] Fix lint errors: Split test and extract helper function - Split TestValidateAPIServerEndpoint into two functions: - TestValidateAPIServerEndpoint_ValidCases (37 lines) - TestValidateAPIServerEndpoint_InvalidCases (40 lines) - Extract setupInClusterContext helper to reduce nesting complexity - Reduces if config.UseInCluster block from complexity 6 to 1 - All tests still pass (14 validation tests + integration test) - Backend linter passes with no errors Addresses lint errors: funlen and nestif. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/cmd/headlamp.go | 52 +++++++++------- backend/pkg/kubeconfig/kubeconfig_test.go | 76 ++++++++++------------- 2 files changed, 63 insertions(+), 65 deletions(-) diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index cdc965f659a..b3b41347221 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -428,29 +428,7 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler { // In-cluster if config.UseInCluster { - context, err := kubeconfig.GetInClusterContext( - config.InClusterContextName, - config.OidcIdpIssuerURL, - config.OidcClientID, config.OidcClientSecret, - strings.Join(config.OidcScopes, ","), - config.OidcSkipTLSVerify, - config.OidcCACert, - config.APIServerEndpoint) - if err != nil { - logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") - } else { - context.Source = kubeconfig.InCluster - - err = context.SetupProxy() - if err != nil { - logger.Log(logger.LevelError, nil, err, "Failed to setup proxy for in-cluster context") - } - - err = config.KubeConfigStore.AddContext(context) - if err != nil { - logger.Log(logger.LevelError, nil, err, "Failed to add in-cluster context") - } - } + setupInClusterContext(config) } if config.StaticDir != "" { @@ -1095,6 +1073,34 @@ func (c *HeadlampConfig) OIDCTokenRefreshMiddleware(next http.Handler) http.Hand }) } +// setupInClusterContext sets up the in-cluster Kubernetes context. +func setupInClusterContext(config *HeadlampConfig) { + context, err := kubeconfig.GetInClusterContext( + config.InClusterContextName, + config.OidcIdpIssuerURL, + config.OidcClientID, config.OidcClientSecret, + strings.Join(config.OidcScopes, ","), + config.OidcSkipTLSVerify, + config.OidcCACert, + config.APIServerEndpoint) + if err != nil { + logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") + return + } + + context.Source = kubeconfig.InCluster + + err = context.SetupProxy() + if err != nil { + logger.Log(logger.LevelError, nil, err, "Failed to setup proxy for in-cluster context") + } + + err = config.KubeConfigStore.AddContext(context) + if err != nil { + logger.Log(logger.LevelError, nil, err, "Failed to add in-cluster context") + } +} + func StartHeadlampServer(config *HeadlampConfig) { tel, err := telemetry.NewTelemetry(config.TelemetryConfig) if err != nil { diff --git a/backend/pkg/kubeconfig/kubeconfig_test.go b/backend/pkg/kubeconfig/kubeconfig_test.go index 41d8fef103e..ae972a691d6 100644 --- a/backend/pkg/kubeconfig/kubeconfig_test.go +++ b/backend/pkg/kubeconfig/kubeconfig_test.go @@ -827,114 +827,106 @@ func TestHandleConfigLoadError(t *testing.T) { } } -func TestValidateAPIServerEndpoint(t *testing.T) { +func TestValidateAPIServerEndpoint_ValidCases(t *testing.T) { tests := []struct { - name string - endpoint string - wantResult string - wantErr bool - errContains string + name string + endpoint string + wantResult string }{ { name: "empty string returns empty", endpoint: "", wantResult: "", - wantErr: false, }, { name: "whitespace only returns empty", endpoint: " \t\n ", wantResult: "", - wantErr: false, }, { name: "valid https URL", endpoint: "https://kube-oidc-proxy.example.com:443", wantResult: "https://kube-oidc-proxy.example.com:443", - wantErr: false, }, { name: "valid https URL with whitespace is trimmed", endpoint: " https://kube-oidc-proxy.example.com:443 ", wantResult: "https://kube-oidc-proxy.example.com:443", - wantErr: false, }, + { + name: "URL with root path is allowed", + endpoint: "https://proxy.example.com:443/", + wantResult: "https://proxy.example.com:443/", + }, + { + name: "URL without port", + endpoint: "https://proxy.example.com", + wantResult: "https://proxy.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := kubeconfig.ValidateAPIServerEndpoint(tt.endpoint) + require.NoError(t, err) + assert.Equal(t, tt.wantResult, result) + }) + } +} + +func TestValidateAPIServerEndpoint_InvalidCases(t *testing.T) { + tests := []struct { + name string + endpoint string + errContains string + }{ { name: "http URL is rejected", endpoint: "http://insecure-proxy.example.com:443", - wantErr: true, errContains: "must be a full https:// URL", }, { name: "missing scheme is rejected", endpoint: "kube-oidc-proxy.example.com:443", - wantErr: true, errContains: "must be an absolute URL with scheme and host", }, { name: "relative URL is rejected", endpoint: "/path/to/proxy", - wantErr: true, errContains: "must be an absolute URL with scheme and host", }, { name: "URL with embedded credentials is rejected", endpoint: "https://user:password@proxy.example.com:443", - wantErr: true, errContains: "must not include user info (credentials)", }, { name: "URL with empty hostname is rejected", endpoint: "https://:443", - wantErr: true, errContains: "must be an absolute URL with scheme and host", }, { name: "URL with query string is rejected", endpoint: "https://proxy.example.com:443?token=secret", - wantErr: true, errContains: "must not include a query string", }, { name: "URL with fragment is rejected", endpoint: "https://proxy.example.com:443#section", - wantErr: true, errContains: "must not include a fragment", }, { name: "URL with path is rejected", endpoint: "https://proxy.example.com:443/api/v1", - wantErr: true, errContains: "path must be empty or '/'", }, - { - name: "URL with root path is allowed", - endpoint: "https://proxy.example.com:443/", - wantResult: "https://proxy.example.com:443/", - wantErr: false, - }, - { - name: "URL without port", - endpoint: "https://proxy.example.com", - wantResult: "https://proxy.example.com", - wantErr: false, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := kubeconfig.ValidateAPIServerEndpoint(tt.endpoint) - - if tt.wantErr { - require.Error(t, err) - - if tt.errContains != "" { - assert.Contains(t, err.Error(), tt.errContains) - } - } else { - require.NoError(t, err) - assert.Equal(t, tt.wantResult, result) - } + _, err := kubeconfig.ValidateAPIServerEndpoint(tt.endpoint) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) }) } } From 8d7a767be5a6acd54a71cc8f401abb8b8fd809e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:48:33 +0000 Subject: [PATCH 18/22] Add values.schema.json and redact secrets from error messages - Add config.apiServerEndpoint to charts/headlamp/values.schema.json - Redact user-supplied endpoints from error messages to prevent secret logging - For credentials/query/fragment errors: omit endpoint completely - For scheme errors: include only scheme+host (safe parts) - For path errors: include scheme+host+path (safe parts) - All 14 validation tests still pass - JSON schema valid Addresses security concern about logging secrets in error messages. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/pkg/kubeconfig/kubeconfig.go | 24 ++++++++++++------------ charts/headlamp/values.schema.json | 4 ++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index aead86f516e..119008d2096 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1011,45 +1011,45 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { parsedURL, err := url.Parse(trimmed) if err != nil || !parsedURL.IsAbs() || parsedURL.Host == "" || parsedURL.Hostname() == "" { + // Don't include the endpoint in error as it may contain sensitive data return "", fmt.Errorf( - "invalid custom API server endpoint %q: must be an absolute URL with scheme and host", - trimmed, + "invalid custom API server endpoint: must be an absolute URL with scheme and host", ) } if parsedURL.Scheme != "https" { + // Safe to include scheme+host as it doesn't contain secrets return "", fmt.Errorf( - "invalid custom API server endpoint %q: must be a full https:// URL", - trimmed, + "invalid custom API server endpoint %s://%s: must be a full https:// URL", + parsedURL.Scheme, parsedURL.Host, ) } // Disallow embedded credentials, query strings, fragments, and non-root paths + // Don't include the full URL in these errors to avoid logging secrets if parsedURL.User != nil { return "", fmt.Errorf( - "invalid custom API server endpoint %q: must not include user info (credentials)", - trimmed, + "invalid custom API server endpoint: must not include user info (credentials)", ) } if parsedURL.RawQuery != "" { return "", fmt.Errorf( - "invalid custom API server endpoint %q: must not include a query string", - trimmed, + "invalid custom API server endpoint: must not include a query string", ) } if parsedURL.Fragment != "" { return "", fmt.Errorf( - "invalid custom API server endpoint %q: must not include a fragment", - trimmed, + "invalid custom API server endpoint: must not include a fragment", ) } if parsedURL.Path != "" && parsedURL.Path != "/" { + // Safe to include scheme+host+path as path shouldn't contain secrets return "", fmt.Errorf( - "invalid custom API server endpoint %q: path must be empty or '/' (scheme+host[:port] only)", - trimmed, + "invalid custom API server endpoint https://%s%s: path must be empty or '/' (scheme+host[:port] only)", + parsedURL.Host, parsedURL.Path, ) } diff --git a/charts/headlamp/values.schema.json b/charts/headlamp/values.schema.json index 82010c8cf08..7eb17b53992 100644 --- a/charts/headlamp/values.schema.json +++ b/charts/headlamp/values.schema.json @@ -245,6 +245,10 @@ "items": { "type": "string" } + }, + "apiServerEndpoint": { + "type": "string", + "description": "Custom Kubernetes API server endpoint (overrides default in-cluster endpoint). Must be a full https:// URL. Useful when requests need to pass through a proxy like kube-oidc-proxy." } } }, From 28eef5fc05f2ac94cb4d672ff56c1a04e898c172 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:49:53 +0000 Subject: [PATCH 19/22] Further redact error messages for maximum security - Remove scheme+host from non-https error message - Remove path from path validation error message - All error messages now completely redact user input - Prevents any potential information disclosure - All 14 validation tests still pass Final security hardening per code review feedback. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/pkg/kubeconfig/kubeconfig.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 119008d2096..027522cd2a0 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -1018,10 +1018,9 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { } if parsedURL.Scheme != "https" { - // Safe to include scheme+host as it doesn't contain secrets + // Don't include scheme or host to avoid any information disclosure return "", fmt.Errorf( - "invalid custom API server endpoint %s://%s: must be a full https:// URL", - parsedURL.Scheme, parsedURL.Host, + "invalid custom API server endpoint: must be a full https:// URL (non-https scheme detected)", ) } @@ -1046,10 +1045,9 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { } if parsedURL.Path != "" && parsedURL.Path != "/" { - // Safe to include scheme+host+path as path shouldn't contain secrets + // Don't include path to avoid potential sensitive information return "", fmt.Errorf( - "invalid custom API server endpoint https://%s%s: path must be empty or '/' (scheme+host[:port] only)", - parsedURL.Host, parsedURL.Path, + "invalid custom API server endpoint: path must be empty or '/' (scheme+host[:port] only)", ) } From 8573a160870aaa4666e08179a549cce884b9b878 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:53:39 +0000 Subject: [PATCH 20/22] Update kube-oidc-proxy repository URL - Update all references from jetstack/kube-oidc-proxy to TremoloSecurity/kube-oidc-proxy - Repository has moved to new maintainer - Updated in custom-api-endpoint.md (2 occurrences) - Updated in charts/headlamp/README.md (1 occurrence) Addresses comment 2779229494. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- charts/headlamp/README.md | 2 +- docs/installation/in-cluster/custom-api-endpoint.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index 59922321a14..07cae96c1a9 100644 --- a/charts/headlamp/README.md +++ b/charts/headlamp/README.md @@ -63,7 +63,7 @@ $ helm install my-headlamp headlamp/headlamp \ --set config.apiServerEndpoint=https://kube-oidc-proxy.example.com:443 ``` -This is particularly useful with managed Kubernetes clusters (like AWS EKS, Azure AKS, or Google GKE) when using [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle OIDC authentication with private identity providers. +This is particularly useful with managed Kubernetes clusters (like AWS EKS, Azure AKS, or Google GKE) when using [kube-oidc-proxy](https://github.com/TremoloSecurity/kube-oidc-proxy) to handle OIDC authentication with private identity providers. ## Configuration diff --git a/docs/installation/in-cluster/custom-api-endpoint.md b/docs/installation/in-cluster/custom-api-endpoint.md index 811ad80a261..5d7ea7736a9 100644 --- a/docs/installation/in-cluster/custom-api-endpoint.md +++ b/docs/installation/in-cluster/custom-api-endpoint.md @@ -6,7 +6,7 @@ By default, when running in-cluster, Headlamp automatically detects and connects This feature is particularly useful for: -- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS, Azure AKS, or Google GKE) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/jetstack/kube-oidc-proxy) to handle authentication requests. +- **OIDC Authentication with Private Endpoints**: When using managed Kubernetes services (like AWS EKS, Azure AKS, or Google GKE) with OIDC authentication where the identity provider is on a private endpoint, you can use [kube-oidc-proxy](https://github.com/TremoloSecurity/kube-oidc-proxy) to handle authentication requests. - **API Gateway or Proxy Requirements**: When your cluster requires all API traffic to go through a specific gateway or proxy for security, logging, or compliance reasons. - **Multi-cluster Authentication**: When using a centralized authentication proxy across multiple clusters. @@ -74,7 +74,7 @@ The custom API server endpoint feature allows you to configure Headlamp to route To manually test the custom API server endpoint configuration with kube-oidc-proxy: -1. **Deploy kube-oidc-proxy** in your cluster following the [kube-oidc-proxy documentation](https://github.com/jetstack/kube-oidc-proxy). +1. **Deploy kube-oidc-proxy** in your cluster following the [kube-oidc-proxy documentation](https://github.com/TremoloSecurity/kube-oidc-proxy). 2. **Install Headlamp** with the custom endpoint pointing to kube-oidc-proxy: ```bash From 2b0d62758bbc964bfbc206bd0aca84c97a3bf3be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:18:57 +0000 Subject: [PATCH 21/22] Add port validation to prevent runtime errors - Validate port is numeric and in range 1-65535 - Add strconv import for port parsing - Add 4 test cases for port validation: - Non-numeric port (abc) - caught by url.Parse - Port too high (99999) - caught by port validation - Port zero (0) - caught by port validation - Negative port (-1) - caught by url.Parse - Split InvalidCases tests into Format and Security functions (both < 60 lines) - All 18 validation tests pass - Prevents hard-to-diagnose runtime errors when dialing invalid ports Addresses review comment 2779302441. Co-authored-by: illume <9541+illume@users.noreply.github.com> --- backend/pkg/kubeconfig/kubeconfig.go | 11 +++++ backend/pkg/kubeconfig/kubeconfig_test.go | 49 ++++++++++++++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/backend/pkg/kubeconfig/kubeconfig.go b/backend/pkg/kubeconfig/kubeconfig.go index 027522cd2a0..ba71ac151aa 100644 --- a/backend/pkg/kubeconfig/kubeconfig.go +++ b/backend/pkg/kubeconfig/kubeconfig.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "runtime" + "strconv" "strings" "gopkg.in/yaml.v2" @@ -1051,6 +1052,16 @@ func validateAPIServerEndpoint(endpoint string) (string, error) { ) } + // Validate port if present + if portStr := parsedURL.Port(); portStr != "" { + port, err := strconv.Atoi(portStr) + if err != nil || port < 1 || port > 65535 { + return "", fmt.Errorf( + "invalid custom API server endpoint: port must be a valid number between 1 and 65535", + ) + } + } + return trimmed, nil } diff --git a/backend/pkg/kubeconfig/kubeconfig_test.go b/backend/pkg/kubeconfig/kubeconfig_test.go index ae972a691d6..e7457d01d65 100644 --- a/backend/pkg/kubeconfig/kubeconfig_test.go +++ b/backend/pkg/kubeconfig/kubeconfig_test.go @@ -874,7 +874,7 @@ func TestValidateAPIServerEndpoint_ValidCases(t *testing.T) { } } -func TestValidateAPIServerEndpoint_InvalidCases(t *testing.T) { +func TestValidateAPIServerEndpoint_InvalidCases_Format(t *testing.T) { tests := []struct { name string endpoint string @@ -895,16 +895,43 @@ func TestValidateAPIServerEndpoint_InvalidCases(t *testing.T) { endpoint: "/path/to/proxy", errContains: "must be an absolute URL with scheme and host", }, - { - name: "URL with embedded credentials is rejected", - endpoint: "https://user:password@proxy.example.com:443", - errContains: "must not include user info (credentials)", - }, { name: "URL with empty hostname is rejected", endpoint: "https://:443", errContains: "must be an absolute URL with scheme and host", }, + { + name: "URL with invalid port (non-numeric) is rejected", + endpoint: "https://proxy.example.com:abc", + errContains: "must be an absolute URL with scheme and host", + }, + { + name: "URL with port out of range (negative via parsing) is rejected", + endpoint: "https://proxy.example.com:-1", + errContains: "must be an absolute URL with scheme and host", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := kubeconfig.ValidateAPIServerEndpoint(tt.endpoint) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) + }) + } +} + +func TestValidateAPIServerEndpoint_InvalidCases_Security(t *testing.T) { + tests := []struct { + name string + endpoint string + errContains string + }{ + { + name: "URL with embedded credentials is rejected", + endpoint: "https://user:password@proxy.example.com:443", + errContains: "must not include user info (credentials)", + }, { name: "URL with query string is rejected", endpoint: "https://proxy.example.com:443?token=secret", @@ -920,6 +947,16 @@ func TestValidateAPIServerEndpoint_InvalidCases(t *testing.T) { endpoint: "https://proxy.example.com:443/api/v1", errContains: "path must be empty or '/'", }, + { + name: "URL with port out of range (too high) is rejected", + endpoint: "https://proxy.example.com:99999", + errContains: "port must be a valid number between 1 and 65535", + }, + { + name: "URL with port out of range (zero) is rejected", + endpoint: "https://proxy.example.com:0", + errContains: "port must be a valid number between 1 and 65535", + }, } for _, tt := range tests { From 99de9bc23f867f3e0e7d6c294cf00521c3a18711 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:23:51 +0000 Subject: [PATCH 22/22] Co-authored-by: illume <9541+illume@users.noreply.github.com> --- frontend/package-lock.json | 61 ++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9cc86d2eae7..eda1d4e7037 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -216,6 +216,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -608,6 +609,7 @@ "version": "11.13.3", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -648,6 +650,7 @@ "version": "11.13.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.12.0", @@ -1761,6 +1764,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9" }, @@ -1826,6 +1830,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.16.7", @@ -1927,6 +1932,7 @@ "version": "5.16.7", "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/private-theming": "^5.16.6", @@ -2009,6 +2015,7 @@ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.15.0.tgz", "integrity": "sha512-YQEQICNxUEFYp/I/yP58cqihA8yhXaXSNZ1/N0JANu2IlCwoJ4Jzi+S0s4RN7RghpiDyoSMFijROBC5HfpTjiw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.4", "@mui/utils": "^5.16.6", @@ -2536,6 +2543,7 @@ "integrity": "sha512-KoSTtKjzQUQwamcbeCp63Ne9kL7io1WI4+skTJe2chfLz6wsp/Gfg8aKkfs1DuyG1p+zxFDcYpwTWMsNtxqqiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.4.11", "@rspack/lite-tapable": "~1.0.1", @@ -3346,6 +3354,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -3489,6 +3498,7 @@ "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -3533,6 +3543,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.52.3.tgz", "integrity": "sha512-1K7l2hkqlWuh5SdaTYPSwMmHJF5dDk5INK+EtiEwUZW4+usWTXZx7QeHuk078oSzTzaVkEFyT3VquK7F0hYkUw==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.52.3" }, @@ -3621,6 +3632,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4099,6 +4111,7 @@ "version": "18.3.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4108,6 +4121,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "peer": true, "dependencies": { "@types/react": "*" } @@ -4216,6 +4230,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.3.0", @@ -4248,6 +4263,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.3.0", "@typescript-eslint/types": "8.3.0", @@ -4695,7 +4711,8 @@ "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==" + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "peer": true }, "node_modules/@xyflow/react": { "version": "12.3.1", @@ -4745,6 +4762,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4789,6 +4807,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5427,6 +5446,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -6256,7 +6276,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/d3-array": { "version": "3.2.4", @@ -6354,6 +6375,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -6884,6 +6906,7 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "peer": true, "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -7114,6 +7137,7 @@ "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -7185,6 +7209,7 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7239,6 +7264,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7292,6 +7318,7 @@ "version": "2.29.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -7391,6 +7418,7 @@ "version": "6.9.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", + "peer": true, "dependencies": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -7458,6 +7486,7 @@ "version": "7.35.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -7489,6 +7518,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "peer": true, "engines": { "node": ">=10" }, @@ -7555,6 +7585,7 @@ "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "peer": true, "peerDependencies": { "eslint": ">=5.0.0" } @@ -7563,6 +7594,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.3.tgz", "integrity": "sha512-lqrNZIZjFMUr7P06eoKtQLwyVRibvG7N+LtfKtObYGizAAGrcqLkc3tDx+iAik2z7q0j/XI3ihjupIqxhFabFA==", + "peer": true, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" @@ -8934,6 +8966,7 @@ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" } ], + "peer": true, "dependencies": { "@babel/runtime": "^7.23.2" } @@ -9874,6 +9907,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -11398,7 +11432,8 @@ "node_modules/monaco-editor": { "version": "0.52.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", - "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==" + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "peer": true }, "node_modules/moo-color": { "version": "1.0.3", @@ -11420,6 +11455,7 @@ "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", "dev": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", @@ -11935,7 +11971,8 @@ "node_modules/openapi-types": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", - "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==" + "integrity": "sha512-/Yvsd2D7miYB4HLJ3hOOS0+vnowQpaT75FsHzr/y5M9P4q9bwa7RcbW2YdH6KZBn8ceLbKGnHxMZ1CHliGHUFw==", + "peer": true }, "node_modules/opencollective-postinstall": { "version": "2.0.3", @@ -12328,6 +12365,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12630,6 +12668,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12673,6 +12712,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12805,6 +12845,7 @@ "version": "9.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.3", "use-sync-external-store": "^1.0.0" @@ -12827,6 +12868,7 @@ "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13067,7 +13109,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", @@ -13393,6 +13436,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -14067,6 +14111,7 @@ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14717,6 +14762,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14960,6 +15006,7 @@ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.6.tgz", "integrity": "sha512-SfEU3SH3wHNaxhFPjaZE2kNl/NFtLNW5c1oHsg7mti7GjmUj1Roq6osBQeMd+F4kL0BoRBBr8gQAuqBlfFu8LA==", "dev": true, + "peer": true, "dependencies": { "lunr": "^2.3.9", "markdown-it": "^14.1.0", @@ -15005,6 +15052,7 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15472,6 +15520,7 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15644,6 +15693,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15657,6 +15707,7 @@ "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "3.0.6", "@vitest/mocker": "3.0.6",