diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index c56498dc877..b3b41347221 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -428,28 +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) - if err != nil { - logger.Log(logger.LevelError, nil, err, "Failed to get in-cluster context") - } - - 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 != "" { @@ -1094,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/cmd/headlamp_test.go b/backend/cmd/headlamp_test.go index 52b33841d1b..4b7362b5b9a 100644 --- a/backend/cmd/headlamp_test.go +++ b/backend/cmd/headlamp_test.go @@ -1731,3 +1731,44 @@ func TestHandleClusterServiceProxy(t *testing.T) { assert.Equal(t, "OK", rr.Body.String()) } } + +// TestCustomAPIServerEndpoint tests the custom API server endpoint configuration +// with validation logic, simulating kube-oidc-proxy scenarios. +func TestCustomAPIServerEndpoint(t *testing.T) { + // Test https validation directly - should reject http URLs + // Since validation now happens before InClusterConfig, this will always test validation + _, err := kubeconfig.GetInClusterContext( + "test-cluster", + "", "", "", "", + false, "", + "http://insecure-proxy.example.com:443", // http scheme should be rejected + ) + + // 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") + + // 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)") + } + + 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") +} 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..be45ddd6872 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,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; 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/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) { 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/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 780fd2728f3..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" @@ -1001,6 +1002,90 @@ 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 == "" || parsedURL.Hostname() == "" { + // Don't include the endpoint in error as it may contain sensitive data + return "", fmt.Errorf( + "invalid custom API server endpoint: must be an absolute URL with scheme and host", + ) + } + + if parsedURL.Scheme != "https" { + // Don't include scheme or host to avoid any information disclosure + return "", fmt.Errorf( + "invalid custom API server endpoint: must be a full https:// URL (non-https scheme detected)", + ) + } + + // 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: must not include user info (credentials)", + ) + } + + if parsedURL.RawQuery != "" { + return "", fmt.Errorf( + "invalid custom API server endpoint: must not include a query string", + ) + } + + if parsedURL.Fragment != "" { + return "", fmt.Errorf( + "invalid custom API server endpoint: must not include a fragment", + ) + } + + if parsedURL.Path != "" && parsedURL.Path != "/" { + // Don't include path to avoid potential sensitive information + return "", fmt.Errorf( + "invalid custom API server endpoint: path must be empty or '/' (scheme+host[:port] only)", + ) + } + + // 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 +} + +// 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, @@ -1009,14 +1094,28 @@ func GetInClusterContext( oidcScopes string, oidcSkipTLSVerify bool, oidcCACert string, + customAPIServerEndpoint string, ) (*Context, error) { + // Validate custom endpoint first, before attempting to load in-cluster config + customEndpoint, err := validateAPIServerEndpoint(customAPIServerEndpoint) + if err != nil { + return nil, err + } + 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 + } + cluster := &api.Cluster{ - Server: clusterConfig.Host, + Server: apiServerHost, CertificateAuthority: clusterConfig.CAFile, CertificateAuthorityData: clusterConfig.CAData, } @@ -1033,19 +1132,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, diff --git a/backend/pkg/kubeconfig/kubeconfig_test.go b/backend/pkg/kubeconfig/kubeconfig_test.go index e5efce60365..e7457d01d65 100644 --- a/backend/pkg/kubeconfig/kubeconfig_test.go +++ b/backend/pkg/kubeconfig/kubeconfig_test.go @@ -826,3 +826,144 @@ func TestHandleConfigLoadError(t *testing.T) { }) } } + +func TestValidateAPIServerEndpoint_ValidCases(t *testing.T) { + tests := []struct { + name string + endpoint string + wantResult string + }{ + { + name: "empty string returns empty", + endpoint: "", + wantResult: "", + }, + { + name: "whitespace only returns empty", + endpoint: " \t\n ", + wantResult: "", + }, + { + name: "valid https URL", + endpoint: "https://kube-oidc-proxy.example.com:443", + wantResult: "https://kube-oidc-proxy.example.com:443", + }, + { + name: "valid https URL with whitespace is trimmed", + endpoint: " https://kube-oidc-proxy.example.com:443 ", + wantResult: "https://kube-oidc-proxy.example.com:443", + }, + { + 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_Format(t *testing.T) { + tests := []struct { + name string + endpoint string + errContains string + }{ + { + name: "http URL is rejected", + endpoint: "http://insecure-proxy.example.com:443", + errContains: "must be a full https:// URL", + }, + { + name: "missing scheme is rejected", + endpoint: "kube-oidc-proxy.example.com:443", + errContains: "must be an absolute URL with scheme and host", + }, + { + name: "relative URL is rejected", + endpoint: "/path/to/proxy", + errContains: "must be an absolute URL with scheme and host", + }, + { + 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", + errContains: "must not include a query string", + }, + { + name: "URL with fragment is rejected", + endpoint: "https://proxy.example.com:443#section", + errContains: "must not include a fragment", + }, + { + name: "URL with path is rejected", + 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 { + t.Run(tt.name, func(t *testing.T) { + _, err := kubeconfig.ValidateAPIServerEndpoint(tt.endpoint) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) + }) + } +} diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md index ccff7e2c092..07cae96c1a9 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 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 ### 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 diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml index 9867a564b41..70f3dc44c7f 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 }} + - {{ printf "-api-server-endpoint=%s" .Values.config.apiServerEndpoint | quote }} + {{- end }} {{- end }} {{- with .Values.config.enableHelm }} - "-enable-helm" 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/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." } } }, 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: 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..5d7ea7736a9 --- /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/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. + +## 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/TremoloSecurity/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 2ded20f031f..46aaebb8794 100644 --- a/docs/installation/in-cluster/index.md +++ b/docs/installation/in-cluster/index.md @@ -47,6 +47,10 @@ 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 + +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 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 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",