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 <manuel.karim5@gmail.com>
This commit is contained in:
3deep5me
2025-08-31 14:24:29 +02:00
committed by Serge
parent 144b1c74e6
commit 8ef4bcea69
8 changed files with 222 additions and 16 deletions

View File

@@ -16,7 +16,7 @@ maintainers:
# This is the chart version. This version number should be incremented each time you make changes # 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. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # 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 # 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 # 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. # follow Semantic Versioning. They should reflect the version the application is using.

View File

@@ -68,6 +68,56 @@ tolerations:
effect: NoSchedule 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: Deploy chart:
```shell ```shell

View File

@@ -66,6 +66,56 @@ tolerations:
effect: NoSchedule 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: Deploy chart:
```shell ```shell

View File

@@ -26,6 +26,9 @@ clusters:
# Proxmox api token # Proxmox api token
token_id: "kubernetes-csi@pve!csi" token_id: "kubernetes-csi@pve!csi"
token_secret: "secret" 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 name, which is cluster name
region: Region-1 region: Region-1

View File

@@ -76,8 +76,8 @@ type ClustersConfig struct {
var ( var (
ErrMissingPVERegion = errors.New("missing PVE region in cloud config") ErrMissingPVERegion = errors.New("missing PVE region in cloud config")
ErrMissingPVEAPIURL = errors.New("missing PVE API URL in cloud config") ErrMissingPVEAPIURL = errors.New("missing PVE API URL in cloud config")
ErrAuthCredentialsMissing = errors.New("user or token credentials are required") ErrAuthCredentialsMissing = errors.New("user, token or file credentials are required")
ErrInvalidAuthCredentials = errors.New("must specify one of user or token credentials, not both") ErrInvalidAuthCredentials = errors.New("must specify one of user, token or file credentials, not multiple")
ErrInvalidCloudConfig = errors.New("invalid cloud config") ErrInvalidCloudConfig = errors.New("invalid cloud config")
ErrInvalidNetworkMode = fmt.Errorf("invalid network mode, valid modes are %v", ValidNetworkModes) 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 { for idx, c := range cfg.Clusters {
if c.Username != "" && c.Password != "" { hasTokenAuth := c.TokenID != "" || c.TokenSecret != ""
if c.TokenID != "" || c.TokenSecret != "" { hasTokenFileAuth := c.TokenIDFile != "" || c.TokenSecretFile != ""
return ClustersConfig{}, fmt.Errorf("cluster #%d: %w", idx+1, ErrInvalidAuthCredentials)
} hasUserAuth := c.Username != "" && c.Password != ""
} else if c.TokenID == "" || c.TokenSecret == "" { 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) return ClustersConfig{}, fmt.Errorf("cluster #%d: %w", idx+1, ErrAuthCredentialsMissing)
} }

View File

@@ -58,6 +58,20 @@ clusters:
assert.ErrorIs(t, err, providerconfig.ErrAuthCredentialsMissing) assert.ErrorIs(t, err, providerconfig.ErrAuthCredentialsMissing)
assert.NotNil(t, cfg) 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 // Valid config with one cluster
cfg, err = providerconfig.ReadCloudConfig(strings.NewReader(` cfg, err = providerconfig.ReadCloudConfig(strings.NewReader(`
clusters: clusters:
@@ -118,6 +132,21 @@ clusters:
assert.Equal(t, 1, len(cfg.Clusters)) assert.Equal(t, 1, len(cfg.Clusters))
assert.Equal(t, providerconfig.ProviderCapmox, cfg.Features.Provider) 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 // Errors when username/password are set with token_id/token_secret
_, err = providerconfig.ReadCloudConfig(strings.NewReader(` _, err = providerconfig.ReadCloudConfig(strings.NewReader(`
features: features:

View File

@@ -35,13 +35,15 @@ import (
// ProxmoxCluster defines a Proxmox cluster configuration. // ProxmoxCluster defines a Proxmox cluster configuration.
type ProxmoxCluster struct { type ProxmoxCluster struct {
URL string `yaml:"url"` URL string `yaml:"url"`
Insecure bool `yaml:"insecure,omitempty"` Insecure bool `yaml:"insecure,omitempty"`
TokenID string `yaml:"token_id,omitempty"` TokenIDFile string `yaml:"token_id_file,omitempty"`
TokenSecret string `yaml:"token_secret,omitempty"` TokenSecretFile string `yaml:"token_secret_file,omitempty"`
Username string `yaml:"username,omitempty"` TokenID string `yaml:"token_id,omitempty"`
Password string `yaml:"password,omitempty"` TokenSecret string `yaml:"token_secret,omitempty"`
Region string `yaml:"region,omitempty"` Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
Region string `yaml:"region,omitempty"`
} }
// ProxmoxPool is a Proxmox client. // ProxmoxPool is a Proxmox client.
@@ -56,6 +58,24 @@ func NewProxmoxPool(config []*ProxmoxCluster, hClient *http.Client) (*ProxmoxPoo
clients := make(map[string]*proxmox.Client, clusters) clients := make(map[string]*proxmox.Client, clusters)
for _, cfg := range config { 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} tlsconf := &tls.Config{InsecureSkipVerify: true}
if !cfg.Insecure { if !cfg.Insecure {
tlsconf = nil tlsconf = nil
@@ -261,3 +281,16 @@ func (c *ProxmoxPool) getSMBSetting(vmInfo map[string]interface{}, name string)
return "" 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
}

View File

@@ -19,6 +19,7 @@ package proxmoxpool_test
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"os"
"testing" "testing"
"github.com/jarcoal/httpmock" "github.com/jarcoal/httpmock"
@@ -28,7 +29,6 @@ import (
) )
func newClusterEnv() []*pxpool.ProxmoxCluster { func newClusterEnv() []*pxpool.ProxmoxCluster {
// copilot convert the cfg call to an array of []*proxmox_pool.ProxmoxCluster:
cfg := []*pxpool.ProxmoxCluster{ cfg := []*pxpool.ProxmoxCluster{
{ {
URL: "https://127.0.0.1:8006/api2/json", URL: "https://127.0.0.1:8006/api2/json",
@@ -49,6 +49,20 @@ func newClusterEnv() []*pxpool.ProxmoxCluster {
return cfg 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) { func TestNewClient(t *testing.T) {
cfg := newClusterEnv() cfg := newClusterEnv()
assert.NotNil(t, cfg) assert.NotNil(t, cfg)
@@ -62,6 +76,29 @@ func TestNewClient(t *testing.T) {
assert.NotNil(t, pClient) 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) { func TestCheckClusters(t *testing.T) {
cfg := newClusterEnv() cfg := newClusterEnv()
assert.NotNil(t, cfg) assert.NotNil(t, cfg)