From 8ef4bcea6964829a416dcc3fe32cc65a99c71b08 Mon Sep 17 00:00:00 2001 From: 3deep5me Date: Sun, 31 Aug 2025 14:24:29 +0200 Subject: [PATCH] feat: add config options token_id_file & token_secret_file Adds additional config options to read proxmox-cluster credentials from separate files. Signed-off-by: 3deep5me --- .../Chart.yaml | 2 +- .../README.md | 50 +++++++++++++++++++ .../README.md.gotmpl | 50 +++++++++++++++++++ docs/config.md | 3 ++ pkg/config/config.go | 18 ++++--- pkg/config/config_test.go | 29 +++++++++++ pkg/proxmoxpool/pool.go | 47 ++++++++++++++--- pkg/proxmoxpool/pool_test.go | 39 ++++++++++++++- 8 files changed, 222 insertions(+), 16 deletions(-) diff --git a/charts/proxmox-cloud-controller-manager/Chart.yaml b/charts/proxmox-cloud-controller-manager/Chart.yaml index d95240c..d790d41 100644 --- a/charts/proxmox-cloud-controller-manager/Chart.yaml +++ b/charts/proxmox-cloud-controller-manager/Chart.yaml @@ -16,7 +16,7 @@ maintainers: # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.15 +version: 0.2.16 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. diff --git a/charts/proxmox-cloud-controller-manager/README.md b/charts/proxmox-cloud-controller-manager/README.md index f0bb8f1..4f7e532 100644 --- a/charts/proxmox-cloud-controller-manager/README.md +++ b/charts/proxmox-cloud-controller-manager/README.md @@ -68,6 +68,56 @@ tolerations: effect: NoSchedule ``` +## Example for credentials from seperate Secrets +```yaml +# helm-values.yaml +config: + clusters: + - url: https://cluster-api-1.exmple.com:8006/api2/json + insecure: false + token_id_file: /run/secrets/cluster-1/token_id + token_secret_file: /run/secrets/cluster-1/token_secret + region: cluster-1 + - url: https://cluster-api-2.exmple.com:8006/api2/json + insecure: false + token_id_file: /run/secrets/cluster-2/token_id + token_secret_file: /run/secrets/cluster-2/token_secret + region: cluster-2 +extraVolumes: + - name: credentials-cluster-1 + secret: + secretName: proxmox-credentials-cluster-1 + - name: credentials-cluster-2 + secret: + secretName: proxmox-credentials-cluster-2 +extraVolumeMounts: + - name: credentials-cluster-1 + readOnly: true + mountPath: "/run/secrets/cluster-1" + - name: credentials-cluster-2 + readOnly: true + mountPath: "/run/secrets/cluster-2" + +``` +```yaml +# secrets-proxmox-clusters.yaml +apiVersion: v1 +kind: Secret +metadata: + name: proxmox-credentials-cluster-1 +stringData: + token_id: kubernetes@pve!csi + token_secret: key1 +--- +apiVersion: v1 +kind: Secret +metadata: + name: proxmox-credentials-cluster-2 +stringData: + token_id: kubernetes@pve!csi + token_secret: key2 +``` + Deploy chart: ```shell diff --git a/charts/proxmox-cloud-controller-manager/README.md.gotmpl b/charts/proxmox-cloud-controller-manager/README.md.gotmpl index 0971b9b..d07996e 100644 --- a/charts/proxmox-cloud-controller-manager/README.md.gotmpl +++ b/charts/proxmox-cloud-controller-manager/README.md.gotmpl @@ -66,6 +66,56 @@ tolerations: effect: NoSchedule ``` +## Example for credentials from separate Secrets +```yaml +# helm-values.yaml +config: + clusters: + - url: https://cluster-api-1.exmple.com:8006/api2/json + insecure: false + token_id_file: /run/secrets/cluster-1/token_id + token_secret_file: /run/secrets/cluster-1/token_secret + region: cluster-1 + - url: https://cluster-api-2.exmple.com:8006/api2/json + insecure: false + token_id_file: /run/secrets/cluster-2/token_id + token_secret_file: /run/secrets/cluster-2/token_secret + region: cluster-2 +extraVolumes: + - name: credentials-cluster-1 + secret: + secretName: proxmox-credentials-cluster-1 + - name: credentials-cluster-2 + secret: + secretName: proxmox-credentials-cluster-2 +extraVolumeMounts: + - name: credentials-cluster-1 + readOnly: true + mountPath: "/run/secrets/cluster-1" + - name: credentials-cluster-2 + readOnly: true + mountPath: "/run/secrets/cluster-2" + +``` +```yaml +# secrets-proxmox-clusters.yaml +apiVersion: v1 +kind: Secret +metadata: + name: proxmox-credentials-cluster-1 +stringData: + token_id: kubernetes@pve!csi + token_secret: key1 +--- +apiVersion: v1 +kind: Secret +metadata: + name: proxmox-credentials-cluster-2 +stringData: + token_id: kubernetes@pve!csi + token_secret: key2 +``` + Deploy chart: ```shell diff --git a/docs/config.md b/docs/config.md index 43dc82c..e145adf 100644 --- a/docs/config.md +++ b/docs/config.md @@ -26,6 +26,9 @@ clusters: # Proxmox api token token_id: "kubernetes-csi@pve!csi" token_secret: "secret" + # (optional) Proxmox api token from separate file (s. Helm README.md) + # token_id_file: /run/secrets/region-1/token_id + # token_secret_file: /run/secrets/region-1/token_secret # Region name, which is cluster name region: Region-1 diff --git a/pkg/config/config.go b/pkg/config/config.go index e9159e8..4198cc3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -76,8 +76,8 @@ type ClustersConfig struct { var ( ErrMissingPVERegion = errors.New("missing PVE region in cloud config") ErrMissingPVEAPIURL = errors.New("missing PVE API URL in cloud config") - ErrAuthCredentialsMissing = errors.New("user or token credentials are required") - ErrInvalidAuthCredentials = errors.New("must specify one of user or token credentials, not both") + ErrAuthCredentialsMissing = errors.New("user, token or file credentials are required") + ErrInvalidAuthCredentials = errors.New("must specify one of user, token or file credentials, not multiple") ErrInvalidCloudConfig = errors.New("invalid cloud config") ErrInvalidNetworkMode = fmt.Errorf("invalid network mode, valid modes are %v", ValidNetworkModes) ) @@ -93,11 +93,15 @@ func ReadCloudConfig(config io.Reader) (ClustersConfig, error) { } for idx, c := range cfg.Clusters { - if c.Username != "" && c.Password != "" { - if c.TokenID != "" || c.TokenSecret != "" { - return ClustersConfig{}, fmt.Errorf("cluster #%d: %w", idx+1, ErrInvalidAuthCredentials) - } - } else if c.TokenID == "" || c.TokenSecret == "" { + hasTokenAuth := c.TokenID != "" || c.TokenSecret != "" + hasTokenFileAuth := c.TokenIDFile != "" || c.TokenSecretFile != "" + + hasUserAuth := c.Username != "" && c.Password != "" + if (hasTokenAuth && hasUserAuth) || (hasTokenFileAuth && hasUserAuth) || (hasTokenAuth && hasTokenFileAuth) { + return ClustersConfig{}, fmt.Errorf("cluster #%d: %w", idx+1, ErrInvalidAuthCredentials) + } + + if !hasTokenAuth && !hasTokenFileAuth && !hasUserAuth { return ClustersConfig{}, fmt.Errorf("cluster #%d: %w", idx+1, ErrAuthCredentialsMissing) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ca6732d..844321a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -58,6 +58,20 @@ clusters: assert.ErrorIs(t, err, providerconfig.ErrAuthCredentialsMissing) assert.NotNil(t, cfg) + // Valid config with one cluster and secret_file + cfg, err = providerconfig.ReadCloudConfig(strings.NewReader(` +clusters: + - url: https://example.com + insecure: false + token_id_file: "/etc/proxmox-secrets/cluster1/token_id" + token_secret_file: "/etc/proxmox-secrets/cluster1/token_secret" + region: cluster-1 +`)) + assert.Nil(t, err) + assert.NotNil(t, cfg) + assert.Equal(t, 1, len(cfg.Clusters)) + assert.Equal(t, "/etc/proxmox-secrets/cluster1/token_id", cfg.Clusters[0].TokenIDFile) + // Valid config with one cluster cfg, err = providerconfig.ReadCloudConfig(strings.NewReader(` clusters: @@ -118,6 +132,21 @@ clusters: assert.Equal(t, 1, len(cfg.Clusters)) assert.Equal(t, providerconfig.ProviderCapmox, cfg.Features.Provider) + // Errors when token_id/token_secret are set with token_id_file/token_secret_file + _, err = providerconfig.ReadCloudConfig(strings.NewReader(` +features: + provider: 'capmox' +clusters: + - url: https://example.com + insecure: false + token_id_file: "/etc/proxmox-secrets/cluster1/token_id" + token_secret_file: "/etc/proxmox-secrets/cluster1/token_secret" + token_id: "ha" + token_secret: "secret" + region: cluster-1 +`)) + assert.NotNil(t, err) + // Errors when username/password are set with token_id/token_secret _, err = providerconfig.ReadCloudConfig(strings.NewReader(` features: diff --git a/pkg/proxmoxpool/pool.go b/pkg/proxmoxpool/pool.go index a3be999..a44d5b6 100644 --- a/pkg/proxmoxpool/pool.go +++ b/pkg/proxmoxpool/pool.go @@ -35,13 +35,15 @@ import ( // ProxmoxCluster defines a Proxmox cluster configuration. type ProxmoxCluster struct { - URL string `yaml:"url"` - Insecure bool `yaml:"insecure,omitempty"` - TokenID string `yaml:"token_id,omitempty"` - TokenSecret string `yaml:"token_secret,omitempty"` - Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` - Region string `yaml:"region,omitempty"` + URL string `yaml:"url"` + Insecure bool `yaml:"insecure,omitempty"` + TokenIDFile string `yaml:"token_id_file,omitempty"` + TokenSecretFile string `yaml:"token_secret_file,omitempty"` + TokenID string `yaml:"token_id,omitempty"` + TokenSecret string `yaml:"token_secret,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Region string `yaml:"region,omitempty"` } // ProxmoxPool is a Proxmox client. @@ -56,6 +58,24 @@ func NewProxmoxPool(config []*ProxmoxCluster, hClient *http.Client) (*ProxmoxPoo clients := make(map[string]*proxmox.Client, clusters) for _, cfg := range config { + if cfg.TokenID == "" { + var err error + + cfg.TokenID, err = readValueFromFile(cfg.TokenIDFile) + if err != nil { + return nil, err + } + } + + if cfg.TokenSecret == "" { + var err error + + cfg.TokenSecret, err = readValueFromFile(cfg.TokenSecretFile) + if err != nil { + return nil, err + } + } + tlsconf := &tls.Config{InsecureSkipVerify: true} if !cfg.Insecure { tlsconf = nil @@ -261,3 +281,16 @@ func (c *ProxmoxPool) getSMBSetting(vmInfo map[string]interface{}, name string) return "" } + +func readValueFromFile(path string) (string, error) { + if path == "" { + return "", fmt.Errorf("path cannot be empty") + } + + content, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("failed to read file '%s': %w", path, err) + } + + return strings.TrimSpace(string(content)), nil +} diff --git a/pkg/proxmoxpool/pool_test.go b/pkg/proxmoxpool/pool_test.go index 8abb9ad..2ba51fa 100644 --- a/pkg/proxmoxpool/pool_test.go +++ b/pkg/proxmoxpool/pool_test.go @@ -19,6 +19,7 @@ package proxmoxpool_test import ( "fmt" "net/http" + "os" "testing" "github.com/jarcoal/httpmock" @@ -28,7 +29,6 @@ import ( ) func newClusterEnv() []*pxpool.ProxmoxCluster { - // copilot convert the cfg call to an array of []*proxmox_pool.ProxmoxCluster: cfg := []*pxpool.ProxmoxCluster{ { URL: "https://127.0.0.1:8006/api2/json", @@ -49,6 +49,20 @@ func newClusterEnv() []*pxpool.ProxmoxCluster { return cfg } +func newClusterEnvWithFiles(tokenIDPath, tokenSecretPath string) []*pxpool.ProxmoxCluster { + cfg := []*pxpool.ProxmoxCluster{ + { + URL: "https://127.0.0.1:8006/api2/json", + Insecure: false, + TokenIDFile: tokenIDPath, + TokenSecretFile: tokenSecretPath, + Region: "cluster-1", + }, + } + + return cfg +} + func TestNewClient(t *testing.T) { cfg := newClusterEnv() assert.NotNil(t, cfg) @@ -62,6 +76,29 @@ func TestNewClient(t *testing.T) { assert.NotNil(t, pClient) } +func TestNewClientWithCredentialsFromFile(t *testing.T) { + tempDir := t.TempDir() + + tokenIDFile, err := os.CreateTemp(tempDir, "token_id") + assert.Nil(t, err) + + tokenSecretFile, err := os.CreateTemp(tempDir, "token_secret") + assert.Nil(t, err) + + _, err = tokenIDFile.WriteString("user!token-id") + assert.Nil(t, err) + _, err = tokenSecretFile.WriteString("secret") + assert.Nil(t, err) + + cfg := newClusterEnvWithFiles(tokenIDFile.Name(), tokenSecretFile.Name()) + + pClient, err := pxpool.NewProxmoxPool(cfg, nil) + assert.Nil(t, err) + assert.NotNil(t, pClient) + assert.Equal(t, "user!token-id", cfg[0].TokenID) + assert.Equal(t, "secret", cfg[0].TokenSecret) +} + func TestCheckClusters(t *testing.T) { cfg := newClusterEnv() assert.NotNil(t, cfg)