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

@@ -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)
}

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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)