From 2a565a4c1f132cab4f86ce792c91d2458eed8034 Mon Sep 17 00:00:00 2001 From: CSM Bot <105446864+csmbot@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:20:35 -0400 Subject: [PATCH] Mirror internal repository with cleaned references --- README.md | 84 +++--- csireverseproxy/main.go | 94 ++++++- csireverseproxy/main_test.go | 327 ++++++++++++++++++++++++ csireverseproxy/pkg/common/constants.go | 6 + csireverseproxy/pkg/config/config.go | 4 +- csireverseproxy/pkg/proxy/proxy.go | 37 ++- csireverseproxy/pkg/proxy/proxy_test.go | 8 +- dell-csi-helm-installer/csi-install.sh | 18 +- service/envvars.go | 5 + service/service.go | 40 ++- 10 files changed, 561 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 3ba93c75..a18da50f 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,45 @@ -# CSI Driver for Dell PowerMax - -[![Go Report Card](https://goreportcard.com/badge/github.com/dell/csi-powermax?style=flat-square)](https://goreportcard.com/report/github.com/dell/csi-powermax) -[![License](https://img.shields.io/github/license/dell/csi-powermax?style=flat-square&color=blue&label=License)](https://github.com/dell/csi-powermax/blob/main/LICENSE) -[![Docker](https://img.shields.io/docker/pulls/dellemc/csi-powermax.svg?logo=docker&style=flat-square&label=Pulls)](https://hub.docker.com/r/dellemc/csi-powermax) -[![Last Release](https://img.shields.io/github/v/release/dell/csi-powermax?label=Latest&style=flat-square&logo=go)](https://github.com/dell/csi-powermax/releases) - -**Repository for CSI Driver for Dell PowerMax** - -## Description -CSI Driver for PowerMax is part of the [CSM (Container Storage Modules)](https://github.com/dell/csm) open-source suite of Kubernetes storage enablers for Dell products. The CSI Driver for PowerMax is a Container Storage Interface (CSI) driver that provides support for provisioning persistent storage using Dell PowerMax storage array. - -This project may be compiled as a stand-alone binary using Golang that, when run, provides a valid CSI endpoint. It also can be used as a precompiled container image. - -## Table of Contents - -* [Code of Conduct](https://github.com/dell/csm/blob/main/docs/CODE_OF_CONDUCT.md) -* [Maintainer Guide](https://github.com/dell/csm/blob/main/docs/MAINTAINER_GUIDE.md) -* [Committer Guide](https://github.com/dell/csm/blob/main/docs/COMMITTER_GUIDE.md) -* [Contributing Guide](https://github.com/dell/csm/blob/main/docs/CONTRIBUTING.md) -* [List of Adopters](https://github.com/dell/csm/blob/main/docs/ADOPTERS.md) -* [Support](#support) -* [Security](https://github.com/dell/csm/blob/main/docs/SECURITY.md) -* [Building](#building) -* [Runtime Dependecies](#runtime-dependencies) -* [Documentation](#documentation) - -## Support -For any issues, questions or feedback, please contact [Dell support](https://www.dell.com/support/incidents-online/en-us/contactus/product/container-storage-modules). - -## Building - -To build the source, execute `make build`. - -To run unit tests, execute `make unit-test`. - -To build an image, execute `make docker`. - -You can run an integration test on a Linux system by populating the file `env.sh` with values for your Dell PowerMax systems and then run "`make integration-test`". +# CSI Driver for Dell PowerMax + +[![Go Report Card](https://goreportcard.com/badge/github.com/dell/csi-powermax?style=flat-square)](https://goreportcard.com/report/github.com/dell/csi-powermax) +[![License](https://img.shields.io/github/license/dell/csi-powermax?style=flat-square&color=blue&label=License)](https://github.com/dell/csi-powermax/blob/main/LICENSE) +[![Docker](https://img.shields.io/docker/pulls/dellemc/csi-powermax.svg?logo=docker&style=flat-square&label=Pulls)](https://hub.docker.com/r/dellemc/csi-powermax) +[![Last Release](https://img.shields.io/github/v/release/dell/csi-powermax?label=Latest&style=flat-square&logo=go)](https://github.com/dell/csi-powermax/releases) + +**Repository for CSI Driver for Dell PowerMax** + +## Description +CSI Driver for PowerMax is part of the [CSM (Container Storage Modules)](https://github.com/dell/csm) open-source suite of Kubernetes storage enablers for Dell products. The CSI Driver for PowerMax is a Container Storage Interface (CSI) driver that provides support for provisioning persistent storage using Dell PowerMax storage array. + +This project may be compiled as a stand-alone binary using Golang that, when run, provides a valid CSI endpoint. It also can be used as a precompiled container image. + +## Table of Contents + +* [Code of Conduct](https://github.com/dell/csm/blob/main/docs/CODE_OF_CONDUCT.md) +* [Maintainer Guide](https://github.com/dell/csm/blob/main/docs/MAINTAINER_GUIDE.md) +* [Committer Guide](https://github.com/dell/csm/blob/main/docs/COMMITTER_GUIDE.md) +* [Contributing Guide](https://github.com/dell/csm/blob/main/docs/CONTRIBUTING.md) +* [List of Adopters](https://github.com/dell/csm/blob/main/docs/ADOPTERS.md) +* [Support](#support) +* [Security](https://github.com/dell/csm/blob/main/docs/SECURITY.md) +* [Building](#building) +* [Runtime Dependecies](#runtime-dependencies) +* [Documentation](#documentation) + +## Support +For any issues, questions or feedback, please contact [Dell support](https://www.dell.com/support/incidents-online/en-us/contactus/product/container-storage-modules). + +## Building + +To build the source, execute `make build`. + +To run unit tests, execute `make unit-test`. + +To build an image, execute `make docker`. + +You can run an integration test on a Linux system by populating the file `env.sh` with values for your Dell PowerMax systems and then run "`make integration-test`". + +## Runtime Dependencies +For a complete list of dependencies, please see the project documentation. + +## Documentation +For more detailed information on the driver, please refer to [Container Storage Modules documentation](https://www.dell.com/support/product-details/en-us/product/container-storage-modules/resources/manuals). diff --git a/csireverseproxy/main.go b/csireverseproxy/main.go index 53e3e0ad..a975eaed 100644 --- a/csireverseproxy/main.go +++ b/csireverseproxy/main.go @@ -49,6 +49,7 @@ type RevProxy interface { ServeReverseProxy(res http.ResponseWriter, req *http.Request) UpdateConfig(proxyConfig config.ProxyConfig) error GetRouter() http.Handler + SetAuthToken(token string) } // ServerOpts - Proxy server configuration @@ -127,10 +128,69 @@ func (s *Server) Config() *config.ProxyConfig { return s.config } +// isSidecarMode detects if running as sidecar by checking multiple indicators +func isSidecarMode() bool { + // Method 1: Check the DeployAsSidecar environment variable set by the operator + // In sidecar mode, the operator sets DeployAsSidecar=true + deployAsSidecar := getEnv("DeployAsSidecar", "") // No default for more accurate detection + + // Method 2: Check if we're running in the same pod as CSI driver containers + // by checking for CSI driver environment variables or container presence + + // Check for CSI driver specific environment variables that indicate sidecar mode + csiDriverEnvVars := []string{ + "X_CSI_POWERMAX_ENDPOINT", + "X_CSI_POWERMAX_USER", + "X_CSI_POWERMAX_PASSWORD", + "X_CSI_POWERMAX_NODENAME", + "X_CSI_POWERMAX_PORTGROUPS", + } + + hasCSIEnvVars := false + for _, envVar := range csiDriverEnvVars { + if getEnv(envVar, "") != "" { + hasCSIEnvVars = true + break + } + } + + // Method 3: Check pod name and namespace patterns + podName := getEnv("POD_NAME", "") + + // CSI driver pods typically have specific naming patterns + isCSIDriverPod := strings.Contains(podName, "controller") || + strings.Contains(podName, "node") || + strings.Contains(podName, "csi-") + + // Debug logging to understand the mode detection + log.Infof("DeployAsSidecar environment variable: '%s'", deployAsSidecar) + log.Infof("Has CSI driver environment variables: %t", hasCSIEnvVars) + log.Infof("Pod name: '%s', Is CSI driver pod: %t", podName, isCSIDriverPod) + + // Determine sidecar mode based on multiple indicators + isSidecar := false + + // Primary check: DeployAsSidecar environment variable explicitly set + if deployAsSidecar != "" { + lowerValue := strings.ToLower(strings.TrimSpace(deployAsSidecar)) + isSidecar = lowerValue == "true" || lowerValue == "1" || lowerValue == "yes" + log.Infof("DeployAsSidecar explicitly set, sidecar mode: %t", isSidecar) + } else { + // Fallback check: If we have CSI driver env vars and are in a CSI driver pod + isSidecar = hasCSIEnvVars && isCSIDriverPod + log.Infof("DeployAsSidecar not set, using fallback detection, sidecar mode: %t", isSidecar) + } + + log.Infof("Final sidecar mode determination: %t", isSidecar) + return isSidecar +} + // Setup sets up the server and the proxy configuration // this includes - reading the secret or config map, creating appropriate proxy instance // and setting up the signal handler channel func (s *Server) Setup(k8sUtils k8sutils.UtilsInterface) error { + // Auth token validation is handled during loading in the proxy configuration section below + // Read the config from secret if secret provided if getEnv(common.EnvReverseProxyUseSecret, "false") == "true" { log.Info("Reading config using secret") @@ -212,6 +272,32 @@ func (s *Server) Setup(k8sUtils k8sutils.UtilsInterface) error { s.SigChan = make(chan os.Signal, 1) } + // Load shared auth token if configured + // Note: Auth token is only used in standalone mode, not in sidecar mode + isSidecar := isSidecarMode() + if tokenFile := getEnv(common.EnvProxyAuthTokenFile, ""); tokenFile != "" { + if isSidecar { + log.Info("Sidecar deployment detected - auth token is not used in sidecar mode, skipping") + // Skip auth token loading entirely in sidecar mode + } else { + log.Info("Standalone deployment detected - loading auth token") + tokenBytes, err := os.ReadFile(filepath.Clean(tokenFile)) + if err != nil { + log.Warnf("Proxy auth token file %s not found or not readable in standalone mode, continuing without auth token: %v", tokenFile, err) + // Continue deployment without auth token - don't fail + } else { + token := strings.TrimSpace(string(tokenBytes)) + if token == "" { + log.Warnf("Proxy auth token file %s is empty in standalone mode, continuing without auth token", tokenFile) + // Continue deployment without auth token - don't fail + } else { + s.Proxy.SetAuthToken(token) + log.Info("Proxy auth token loaded successfully for standalone deployment") + } + } + } + } + return nil } @@ -314,7 +400,7 @@ func (s *Server) SetupConfigWatcher(k8sUtils k8sutils.UtilsInterface, v *viper.V } func (s *Server) configChangeConfigMap(k8sUtils k8sutils.UtilsInterface, vcm *viper.Viper) { - log.Infof("Received a config change event for configmap - all settings %v", vcm.AllSettings()) + log.Infof("Received a config change event for configmap - all settings") var proxyConfigMap config.ProxyConfigMap err := vcm.Unmarshal(&proxyConfigMap) if err != nil { @@ -336,12 +422,12 @@ func (s *Server) configChangeConfigMap(k8sUtils k8sutils.UtilsInterface, vcm *vi if err != nil { log.Errorf("Error in updating the config: %s", err.Error()) } - log.Infof("Updated proxy config: %+v", proxyConfig) + log.Infof("Updated proxy config") } } func (s *Server) configChangeSecret(k8sUtils k8sutils.UtilsInterface, vs *viper.Viper) { - log.Infof("Received a config change event for secret - all settings %v", vs.AllSettings()) + log.Info("Received a config change event for secret") var proxySecret config.ProxySecret err := vs.Unmarshal(&proxySecret) if err != nil { @@ -361,7 +447,7 @@ func (s *Server) configChangeSecret(k8sUtils k8sutils.UtilsInterface, vs *viper. } func (s *Server) configChangeParamsConfigMap(k8sUtils k8sutils.UtilsInterface, vcmp *viper.Viper) { - log.Infof("Received a config change event for params configmap - all settings %v", vcmp.AllSettings()) + log.Infof("Received a config change event for params configmap") var ParamsConfigMap config.ParamsConfigMap err := vcmp.Unmarshal(&ParamsConfigMap) if err != nil { diff --git a/csireverseproxy/main_test.go b/csireverseproxy/main_test.go index 9f5d8ee7..7bd4c970 100644 --- a/csireverseproxy/main_test.go +++ b/csireverseproxy/main_test.go @@ -39,6 +39,7 @@ import ( "github.com/dell/csi-powermax/csireverseproxy/v2/pkg/config" "github.com/dell/csi-powermax/csireverseproxy/v2/pkg/k8smock" "github.com/dell/csi-powermax/csireverseproxy/v2/pkg/k8sutils" + "github.com/dell/csi-powermax/csireverseproxy/v2/pkg/proxy" "github.com/dell/csi-powermax/csireverseproxy/v2/pkg/servermock" "github.com/dell/csi-powermax/csireverseproxy/v2/pkg/utils" "github.com/dell/csmlog" @@ -1357,6 +1358,332 @@ func TestK8sInitFunc(t *testing.T) { } } +func TestSetup(t *testing.T) { + origUseSecret := os.Getenv(common.EnvReverseProxyUseSecret) + origTokenFile := os.Getenv(common.EnvProxyAuthTokenFile) + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + defer func() { + os.Setenv(common.EnvReverseProxyUseSecret, origUseSecret) + os.Setenv(common.EnvProxyAuthTokenFile, origTokenFile) + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + }() + + tests := []struct { + name string + setupFunc func() func() + expectError bool + }{ + { + name: "Setup with invalid config directory", + setupFunc: func() func() { + origUseSecret := os.Getenv(common.EnvReverseProxyUseSecret) + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + os.Setenv(common.EnvReverseProxyUseSecret, "false") + os.Setenv("DeployAsSidecar", "false") + opts := ServerOpts{ + ConfigDir: "/nonexistent-dir", + ConfigFileName: "config.yaml", + } + server := &Server{Opts: opts} + err := server.Setup(k8smock.Init()) + if err == nil { + t.Errorf("Expected error for invalid config directory") + } + return func() { + os.Setenv(common.EnvReverseProxyUseSecret, origUseSecret) + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + } + }, + expectError: false, + }, + { + name: "Setup with secret mode but invalid secret path", + setupFunc: func() func() { + origUseSecret := os.Getenv(common.EnvReverseProxyUseSecret) + origSecretPath := os.Getenv(common.EnvSecretFilePath) + origParamsPath := os.Getenv(common.EnvPowermaxConfigPath) + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + os.Setenv(common.EnvReverseProxyUseSecret, "true") + os.Setenv(common.EnvSecretFilePath, "/nonexistent/secret.yaml") + os.Setenv(common.EnvPowermaxConfigPath, "/nonexistent/params.yaml") + os.Setenv("DeployAsSidecar", "false") + opts := ServerOpts{ + ConfigDir: common.TempConfigDir, + ConfigFileName: tmpSAConfigFile, + } + server := &Server{Opts: opts} + err := server.Setup(k8smock.Init()) + if err == nil { + t.Errorf("Expected error for invalid secret path") + } + return func() { + os.Setenv(common.EnvReverseProxyUseSecret, origUseSecret) + os.Setenv(common.EnvSecretFilePath, origSecretPath) + os.Setenv(common.EnvPowermaxConfigPath, origParamsPath) + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + } + }, + expectError: false, + }, + { + name: "Setup with invalid params config path in secret mode", + setupFunc: func() func() { + origUseSecret := os.Getenv(common.EnvReverseProxyUseSecret) + origParamsPath := os.Getenv(common.EnvPowermaxConfigPath) + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + os.Setenv(common.EnvReverseProxyUseSecret, "true") + os.Setenv(common.EnvPowermaxConfigPath, "/nonexistent/params.yaml") + os.Setenv("DeployAsSidecar", "false") + opts := ServerOpts{ + ConfigDir: common.TempConfigDir, + ConfigFileName: tmpSAConfigFile, + } + server := &Server{Opts: opts} + err := server.Setup(k8smock.Init()) + // This should fail because params config path is invalid + if err == nil { + t.Errorf("Expected error for invalid params config path") + } + return func() { + os.Setenv(common.EnvReverseProxyUseSecret, origUseSecret) + os.Setenv(common.EnvPowermaxConfigPath, origParamsPath) + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + } + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cleanup := tt.setupFunc() + defer cleanup() + }) + } +} + +func TestIsSidecarMode(t *testing.T) { + tests := []struct { + name string + setupFunc func() func() + expectSidecar bool + }{ + { + name: "DeployAsSidecar explicitly set to true", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Setenv("DeployAsSidecar", "true") + os.Unsetenv("POD_NAME") + os.Unsetenv("X_CSI_POWERMAX_ENDPOINT") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: true, + }, + { + name: "DeployAsSidecar explicitly set to false", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Setenv("DeployAsSidecar", "false") + os.Unsetenv("POD_NAME") + os.Unsetenv("X_CSI_POWERMAX_ENDPOINT") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: false, + }, + { + name: "DeployAsSidecar set to 1", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Setenv("DeployAsSidecar", "1") + os.Unsetenv("POD_NAME") + os.Unsetenv("X_CSI_POWERMAX_ENDPOINT") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: true, + }, + { + name: "DeployAsSidecar set to yes", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Setenv("DeployAsSidecar", "yes") + os.Unsetenv("POD_NAME") + os.Unsetenv("X_CSI_POWERMAX_ENDPOINT") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: true, + }, + { + name: "DeployAsSidecar not set, but CSI env vars present and pod name indicates CSI driver", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Unsetenv("DeployAsSidecar") + os.Setenv("POD_NAME", "csi-powermax-controller-abc123") + os.Setenv("X_CSI_POWERMAX_ENDPOINT", "https://unisphere.example.com") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: true, + }, + { + name: "DeployAsSidecar not set, CSI env vars present but pod name does not indicate CSI driver", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Unsetenv("DeployAsSidecar") + os.Setenv("POD_NAME", "my-app-xyz") + os.Setenv("X_CSI_POWERMAX_ENDPOINT", "https://unisphere.example.com") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: false, + }, + { + name: "DeployAsSidecar not set, pod name indicates CSI driver but no CSI env vars", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Unsetenv("DeployAsSidecar") + os.Setenv("POD_NAME", "csi-powermax-node-abc123") + os.Unsetenv("X_CSI_POWERMAX_ENDPOINT") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: false, + }, + { + name: "DeployAsSidecar not set, no CSI env vars or pod name", + setupFunc: func() func() { + origDeployAsSidecar := os.Getenv("DeployAsSidecar") + origPodName := os.Getenv("POD_NAME") + origEndpoint := os.Getenv("X_CSI_POWERMAX_ENDPOINT") + os.Unsetenv("DeployAsSidecar") + os.Unsetenv("POD_NAME") + os.Unsetenv("X_CSI_POWERMAX_ENDPOINT") + return func() { + os.Setenv("DeployAsSidecar", origDeployAsSidecar) + os.Setenv("POD_NAME", origPodName) + os.Setenv("X_CSI_POWERMAX_ENDPOINT", origEndpoint) + } + }, + expectSidecar: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cleanup := tt.setupFunc() + defer cleanup() + result := isSidecarMode() + if result != tt.expectSidecar { + t.Errorf("isSidecarMode() = %v, want %v", result, tt.expectSidecar) + } + }) + } +} + +func TestEventHandler(t *testing.T) { + tests := []struct { + name string + setupFunc func() func() + expectError bool + }{ + { + name: "EventHandler with mounted secret enabled", + setupFunc: func() func() { + origUseSecret := os.Getenv(common.EnvReverseProxyUseSecret) + os.Setenv(common.EnvReverseProxyUseSecret, "true") + server := &Server{} + server.SetConfig(&config.ProxyConfig{}) + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: common.DefaultNameSpace, + }, + } + server.EventHandler(k8smock.Init(), secret) + return func() { + os.Setenv(common.EnvReverseProxyUseSecret, origUseSecret) + } + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cleanup := tt.setupFunc() + defer cleanup() + }) + } +} + +func TestSignalHandler(t *testing.T) { + server := &Server{ + HTTPServer: &http.Server{}, + SigChan: make(chan os.Signal, 1), + } + server.SignalHandler(k8smock.Init()) + // Send a signal to trigger the handler + time.Sleep(50 * time.Millisecond) + server.SigChan <- syscall.SIGHUP + time.Sleep(100 * time.Millisecond) +} + +func TestStart(t *testing.T) { + proxy, err := proxy.NewProxy(config.ProxyConfig{ + Port: "8080", + }) + if err != nil { + t.Fatalf("Failed to create proxy: %v", err) + } + server := &Server{ + Port: "8080", + Proxy: proxy, + HTTPServer: &http.Server{}, + } + // Start the server in a goroutine + go server.Start() + // Wait a bit for the server to start + time.Sleep(100 * time.Millisecond) +} + func TestStartServerFuncFailure(t *testing.T) { stopServers() diff --git a/csireverseproxy/pkg/common/constants.go b/csireverseproxy/pkg/common/constants.go index 393ac7bb..3ffb43d8 100644 --- a/csireverseproxy/pkg/common/constants.go +++ b/csireverseproxy/pkg/common/constants.go @@ -46,4 +46,10 @@ const ( // EnvSidecarProxyPort is the port on which the reverse proxy // server run, if run as a sidecar container EnvSidecarProxyPort = "X_CSI_POWERMAX_SIDECAR_PROXY_PORT" + // EnvProxyAuthTokenFile is the path to a file containing a shared auth token + // used for authenticating requests between the CSI driver and the reverse proxy. + // Both driver and proxy must mount the same K8s Secret. + EnvProxyAuthTokenFile = "X_CSI_REVPROXY_AUTH_TOKEN_FILE" // #nosec G101 + // ProxyAuthTokenHeader is the HTTP header name used to carry the shared auth token + ProxyAuthTokenHeader = "X-Proxy-Auth-Token" // #nosec G101 ) diff --git a/csireverseproxy/pkg/config/config.go b/csireverseproxy/pkg/config/config.go index f5347641..ff434195 100644 --- a/csireverseproxy/pkg/config/config.go +++ b/csireverseproxy/pkg/config/config.go @@ -819,13 +819,11 @@ func NewProxyConfig(configMap *ProxyConfigMap, k8sUtils k8sutils.UtilsInterface) */ func (c *ProxyConfigMap) CustomUnmarshal(vcm *viper.Viper) error { settings := vcm.AllSettings() - - log.Infof("viper all settings: %+v\n", vcm.AllSettings()) // Retrieve all settings as a map // Custom handling for URL fields before unmarshaling for i, managementServer := range vcm.Get("config.managementservers").([]interface{}) { serverMap := managementServer.(map[string]interface{}) - log.Infof("serverMap: %+v", serverMap) + log.Infof("serverMap:") // Check if the "url" field exists and is a string if urlStr, ok := serverMap["url"].(string); ok { parsedURL, err := url.Parse(urlStr) diff --git a/csireverseproxy/pkg/proxy/proxy.go b/csireverseproxy/pkg/proxy/proxy.go index a0132c64..f8e14991 100644 --- a/csireverseproxy/pkg/proxy/proxy.go +++ b/csireverseproxy/pkg/proxy/proxy.go @@ -51,6 +51,7 @@ type Proxy struct { envoyMap map[string]common.Envoy iteratorCache cache.Cache mutex sync.Mutex + authToken string } // NewProxy - Given a proxy config, returns a proxy @@ -258,6 +259,34 @@ func (revProxy *Proxy) setPortGroups(resp *http.Response, URL url.URL, symID str return portGroups, nil } +// SetAuthToken sets the shared authentication token used to validate inbound requests. +// When set, every request must include the matching token in the X-Proxy-Auth-Token header. +func (revProxy *Proxy) SetAuthToken(token string) { + revProxy.mutex.Lock() + defer revProxy.mutex.Unlock() + revProxy.authToken = token +} + +func (revProxy *Proxy) tokenAuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + revProxy.mutex.Lock() + token := revProxy.authToken + revProxy.mutex.Unlock() + if token != "" { + provided := r.Header.Get(common.ProxyAuthTokenHeader) + if provided == "" { + utils.WriteHTTPError(w, "missing proxy auth token", utils.StatusUnAuthorized) + return + } + if provided != token { + utils.WriteHTTPError(w, "invalid proxy auth token", utils.StatusUnAuthorized) + return + } + } + next.ServeHTTP(w, r) + }) +} + func (revProxy *Proxy) loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reqID := revProxy.getRequestID() @@ -492,7 +521,7 @@ func (revProxy *Proxy) GetRouter() http.Handler { log.Errorf("Error walking routes: %v", err) } - return revProxy.loggingMiddleware(router) + return revProxy.tokenAuthMiddleware(revProxy.loggingMiddleware(router)) } func (revProxy *Proxy) ifNoSymIDInvoke(customHandler http.HandlerFunc) http.HandlerFunc { @@ -651,6 +680,9 @@ func (revProxy *Proxy) ServeVolumePerformance(res http.ResponseWriter, req *http utils.WriteHTTPError(res, "failed to decode request", http.StatusInternalServerError) return } + if err := revProxy.isAuthorized(res, req, reqParam.SystemID); err != nil { + return + } resp, err := revProxy.getResponseIfAuthorised(res, req, reqParam.SystemID) if err != nil { @@ -683,6 +715,9 @@ func (revProxy *Proxy) ServeFSPerformance(res http.ResponseWriter, req *http.Req utils.WriteHTTPError(res, "failed to decode request", http.StatusInternalServerError) return } + if err := revProxy.isAuthorized(res, req, reqParam.SystemID); err != nil { + return + } resp, err := revProxy.getResponseIfAuthorised(res, req, reqParam.SystemID) if err != nil { diff --git a/csireverseproxy/pkg/proxy/proxy_test.go b/csireverseproxy/pkg/proxy/proxy_test.go index 827ef5d7..dd5308a2 100644 --- a/csireverseproxy/pkg/proxy/proxy_test.go +++ b/csireverseproxy/pkg/proxy/proxy_test.go @@ -1173,8 +1173,8 @@ func TestGetRouter_ServeVolumePerformance(t *testing.T) { } })), expect: &httptest.ResponseRecorder{ - Code: http.StatusInternalServerError, - Body: bytes.NewBuffer([]byte(`failed to find array id in the configuration`)), + Code: http.StatusUnauthorized, + Body: bytes.NewBuffer([]byte(`unauthorized for this array`)), }, }, { @@ -1395,8 +1395,8 @@ func TestGetRouter_ServeFSPerformance(t *testing.T) { }, server: fakeServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})), expect: &httptest.ResponseRecorder{ - Code: http.StatusInternalServerError, - Body: bytes.NewBuffer([]byte(`failed to find array id in the configuration`)), + Code: http.StatusUnauthorized, + Body: bytes.NewBuffer([]byte(`unauthorized for this array`)), }, }, { diff --git a/dell-csi-helm-installer/csi-install.sh b/dell-csi-helm-installer/csi-install.sh index b31dc624..cc041323 100755 --- a/dell-csi-helm-installer/csi-install.sh +++ b/dell-csi-helm-installer/csi-install.sh @@ -22,6 +22,7 @@ VERIFY=1 MODE="install" DEFAULT_VERSION="v2.17.0" WATCHLIST="" +DRIVER="csi-powermax" # # usage will print command execution help and then exit @@ -128,6 +129,14 @@ while getopts ":h-:" optchar; do esac done +# Derive helm chart version from DEFAULT_VERSION (single source of truth) +DRIVERVERSION="${DRIVER}-${DEFAULT_VERSION#v}" + +# Allow override via --helm-charts-version +if [ -n "$HELMCHARTVERSION" ]; then + DRIVERVERSION=$HELMCHARTVERSION +fi + if [ ! -d "$DRIVERDIR/helm-charts" ]; then if [ ! -d "$SCRIPTDIR/helm-charts" ]; then @@ -140,17 +149,8 @@ else fi fi DRIVERDIR="${SCRIPTDIR}/../helm-charts/charts" -DRIVER="csi-powermax" VERIFYSCRIPT="${SCRIPTDIR}/verify.sh" -# Derive helm chart version from DEFAULT_VERSION (single source of truth) -DRIVERVERSION="${DRIVER}-${DEFAULT_VERSION#v}" - -# Allow override via --helm-charts-version -if [ -n "$HELMCHARTVERSION" ]; then - DRIVERVERSION=$HELMCHARTVERSION -fi - # export the name of the debug log, so child processes will see it export DEBUGLOG="${SCRIPTDIR}/install-debug.log" declare -a VALIDDRIVERS diff --git a/service/envvars.go b/service/envvars.go index 81e0a322..126339f3 100644 --- a/service/envvars.go +++ b/service/envvars.go @@ -197,4 +197,9 @@ const ( // EnvSpaceReclamationTimeout is the timeout for each reclamation operation EnvSpaceReclamationTimeout = "X_CSI_SPACE_RECLAMATION_TIMEOUT" + + // EnvProxyAuthTokenFile is the path to a file containing a shared auth token + // used for authenticating requests between the CSI driver and the reverse proxy. + // Both driver and proxy must mount the same K8s Secret. + EnvProxyAuthTokenFile = "X_CSI_REVPROXY_AUTH_TOKEN_FILE" // #nosec G101 ) diff --git a/service/service.go b/service/service.go index 746a9154..b53f6bbb 100644 --- a/service/service.go +++ b/service/service.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "net" + "net/http" "os" "path/filepath" "strconv" @@ -56,8 +57,10 @@ import ( // Constants for the service const ( - Name = "csi-powermax.dellemc.com" // Name is the name of the CSI plug-in. - ApplicationName = "CSI Driver for Dell EMC PowerMax" // ApplicationName is the name used to register with Powermax REST APIs + Name = "csi-powermax.dellemc.com" // Name is the name of the CSI plug-in. + ApplicationName = "CSI Driver for Dell EMC PowerMax" // ApplicationName is the name used to register with Powermax REST APIs + // ProxyAuthTokenHeader is the HTTP header for shared auth token + ProxyAuthTokenHeader = "X-Proxy-Auth-Token" // #nosec G101 -- header name, not a credential defaultPrivDir = "/dev/disk/csi-powermax" defaultLockCleanupDuration = 4 csiPrefix = "csi-" @@ -215,6 +218,7 @@ type service struct { useNVMeTCP bool iscsiTargets map[string][]string nvmeTargets *sync.Map + authToken string // shared auth token for reverse proxy authentication // Timeout for storage pool cache storagePoolCacheDuration time.Duration @@ -570,6 +574,24 @@ func (s *service) BeforeServe( opts.TLSCertDir = tlsCertDir } + // Load shared auth token if configured + if tokenFile, ok := csictx.LookupEnv(ctx, EnvProxyAuthTokenFile); ok { + tokenBytes, err := os.ReadFile(filepath.Clean(tokenFile)) + if err != nil { + log.Warnf("Proxy auth token file %s not found or not readable, continuing without auth token: %v", tokenFile, err) + // Continue deployment without auth token - don't fail + } else { + token := strings.TrimSpace(string(tokenBytes)) + if token == "" { + log.Warnf("Proxy auth token file %s is empty, continuing without auth token", tokenFile) + // Continue deployment without auth token - don't fail + } else { + s.authToken = token + log.Info("Proxy auth token loaded successfully") + } + } + } + opts.TransportProtocol = s.getTransportProtocolFromEnv() opts.ProxyServiceHost, opts.ProxyServicePort, opts.UseProxy = s.getProxySettingsFromEnv() if !opts.UseProxy && !inducedMockReverseProxy { @@ -929,6 +951,14 @@ func (s *service) createPowerMaxClients(ctx context.Context) error { return status.Errorf(codes.FailedPrecondition, "unable to create PowerMax client: %s", err.Error()) } + // Set the proxy auth token header if configured, so all requests to the + // reverse proxy include the shared auth token for authentication + if s.authToken != "" { + headers := make(http.Header) + headers.Set(ProxyAuthTokenHeader, s.authToken) + c.SetCustomHTTPHeaders(headers) + log.Info("Proxy auth token header set on PowerMax client") + } s.adminClient = c for i := 0; i < maxAuthenticateRetryCount; i++ { @@ -956,6 +986,12 @@ func (s *service) createPowerMaxClients(ctx context.Context) error { log.Warnf("unable to create 10.4 PowerMax client: %s, will use main client", err.Error()) s.adminClient104 = s.adminClient } else { + // Set the proxy auth token header on the 10.4 client as well + if s.authToken != "" { + headers := make(http.Header) + headers.Set(ProxyAuthTokenHeader, s.authToken) + c104.SetCustomHTTPHeaders(headers) + } for i := 0; i < maxAuthenticateRetryCount; i++ { err = c104.Authenticate(ctx, &pmax.ConfigConnect{ Endpoint: endPoint,