diff --git a/cmd/vsphere-copy-offload-populator/README.md b/cmd/vsphere-copy-offload-populator/README.md index fbdf77533e..88531f576b 100644 --- a/cmd/vsphere-copy-offload-populator/README.md +++ b/cmd/vsphere-copy-offload-populator/README.md @@ -159,6 +159,7 @@ to have a secret with the following fields: | STORAGE_PASSWORD | string | y* | | | STORAGE_TOKEN | string | n** | | | STORAGE_SKIP_SSL_VERIFICATION | true/false | n | false | +| STORAGE_HTTP_TIMEOUT_SECONDS | integer | n | 30 | \* For most storage vendors, `STORAGE_USERNAME` and `STORAGE_PASSWORD` are required. Pure FlashArray is an exception - see below. diff --git a/cmd/vsphere-copy-offload-populator/internal/pure/flashArray.go b/cmd/vsphere-copy-offload-populator/internal/pure/flashArray.go index dcf2b8d2e3..13eff05dd7 100644 --- a/cmd/vsphere-copy-offload-populator/internal/pure/flashArray.go +++ b/cmd/vsphere-copy-offload-populator/internal/pure/flashArray.go @@ -37,13 +37,13 @@ printf "px_%s" $(oc get storagecluster -A -o=jsonpath='{.items[0].status.cluster // Authentication is mutually exclusive: // - If apiToken is provided (non-empty), it will be used for authentication (username/password ignored) // - If apiToken is empty, username and password will be used for authentication -func NewFlashArrayClonner(hostname, username, password, apiToken string, skipSSLVerification bool, clusterPrefix string) (FlashArrayClonner, error) { +func NewFlashArrayClonner(hostname, username, password, apiToken string, skipSSLVerification bool, clusterPrefix string, httpTimeoutSeconds int) (FlashArrayClonner, error) { if clusterPrefix == "" { return FlashArrayClonner{}, errors.New(helpMessage) } // Create the REST client for all operations - restClient, err := NewRestClient(hostname, username, password, apiToken, skipSSLVerification) + restClient, err := NewRestClient(hostname, username, password, apiToken, skipSSLVerification, httpTimeoutSeconds) if err != nil { return FlashArrayClonner{}, fmt.Errorf("failed to create REST client: %w", err) } diff --git a/cmd/vsphere-copy-offload-populator/internal/pure/flashArray_test.go b/cmd/vsphere-copy-offload-populator/internal/pure/flashArray_test.go index a59420724d..fbe94274c4 100644 --- a/cmd/vsphere-copy-offload-populator/internal/pure/flashArray_test.go +++ b/cmd/vsphere-copy-offload-populator/internal/pure/flashArray_test.go @@ -310,7 +310,7 @@ func TestAuthenticationMethods(t *testing.T) { hostname := strings.TrimPrefix(server.URL, "https://") // Create REST client with test parameters - client, err := NewRestClient(hostname, tc.username, tc.password, tc.token, true) + client, err := NewRestClient(hostname, tc.username, tc.password, tc.token, true, 30) if tc.expectError { if err == nil { diff --git a/cmd/vsphere-copy-offload-populator/internal/pure/rest_client.go b/cmd/vsphere-copy-offload-populator/internal/pure/rest_client.go index 84abaf8918..8cdf3f477d 100644 --- a/cmd/vsphere-copy-offload-populator/internal/pure/rest_client.go +++ b/cmd/vsphere-copy-offload-populator/internal/pure/rest_client.go @@ -119,7 +119,11 @@ type HostConnectionRequest struct { // NewRestClient creates a new REST client for Pure FlashArray // If apiToken is provided (non-empty), it will be used directly, skipping username/password authentication // If apiToken is empty, username and password will be used to obtain an API token -func NewRestClient(hostname, username, password, apiToken string, skipSSLVerify bool) (*RestClient, error) { +// httpTimeoutSeconds controls the HTTP client timeout; pass 0 to use the default of 30 seconds +func NewRestClient(hostname, username, password, apiToken string, skipSSLVerify bool, httpTimeoutSeconds int) (*RestClient, error) { + if httpTimeoutSeconds <= 0 { + httpTimeoutSeconds = 30 + } // Create base transport with TLS config baseTransport := &http.Transport{ TLSClientConfig: &tls.Config{ @@ -135,7 +139,7 @@ func NewRestClient(hostname, username, password, apiToken string, skipSSLVerify client := &RestClient{ hostname: hostname, httpClient: &http.Client{ - Timeout: 30 * time.Second, + Timeout: time.Duration(httpTimeoutSeconds) * time.Second, Transport: transport, }, } diff --git a/cmd/vsphere-copy-offload-populator/vsphere-copy-offload-populator.go b/cmd/vsphere-copy-offload-populator/vsphere-copy-offload-populator.go index 42eb769065..e9064e40e0 100644 --- a/cmd/vsphere-copy-offload-populator/vsphere-copy-offload-populator.go +++ b/cmd/vsphere-copy-offload-populator/vsphere-copy-offload-populator.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path" + "strconv" "strings" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -59,6 +60,7 @@ var ( vspherePassword string esxiCloneMethod string sshTimeoutSeconds int + storageAPITimeoutSeconds string // kube args httpEndpoint string @@ -104,8 +106,12 @@ func main() { } storageApi = &sm case forklift.StorageVendorProductPureFlashArray: + apiTimeout, err := strconv.Atoi(storageAPITimeoutSeconds) + if err != nil && storageAPITimeoutSeconds != "" { + klog.Warningf("invalid value %q for storage-http-timeout-seconds, using default (30s): %v", storageAPITimeoutSeconds, err) + } sm, err := pure.NewFlashArrayClonner( - storageHostname, storageUsername, storagePassword, storageToken, storageSkipSSLVerification == "true", os.Getenv(pure.ClusterPrefixEnv)) + storageHostname, storageUsername, storagePassword, storageToken, storageSkipSSLVerification == "true", os.Getenv(pure.ClusterPrefixEnv), apiTimeout) if err != nil { klog.Fatalf("failed to initialize Pure FlashArray clonner with %s", err) } @@ -306,6 +312,7 @@ func handleArgs() { flag.StringVar(&vspherePassword, "vsphere-password", os.Getenv("GOVMOMI_PASSWORD"), "vSphere's API password") flag.StringVar(&esxiCloneMethod, "esxi-clone-method", os.Getenv("ESXI_CLONE_METHOD"), "ESXi clone method: 'vib' (default) or 'ssh'") flag.IntVar(&sshTimeoutSeconds, "ssh-timeout-seconds", 30, "SSH timeout in seconds for ESXi operations (default: 30)") + flag.StringVar(&storageAPITimeoutSeconds, "storage-http-timeout-seconds", os.Getenv("STORAGE_HTTP_TIMEOUT_SECONDS"), "HTTP client timeout in seconds for storage API requests (default: 30)") flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") // Metrics args