Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/imdario/mergo v0.3.7
github.com/moby/sys/user v0.4.0
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
github.com/onsi/gomega v1.38.2
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/selinux v1.13.0
github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb
Expand All @@ -35,6 +36,7 @@ require (
golang.org/x/sys v0.38.0
golang.org/x/time v0.9.0
gopkg.in/evanphx/json-patch.v4 v4.13.0
gopkg.in/gcfg.v1 v1.2.3
gopkg.in/natefinch/lumberjack.v2 v2.2.1
k8s.io/api v0.35.1
k8s.io/apiextensions-apiserver v0.35.1
Expand Down Expand Up @@ -130,6 +132,7 @@ require (
google.golang.org/grpc v1.72.2 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kms v0.35.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,14 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Expand Down
47 changes: 47 additions & 0 deletions pkg/cloudprovider/vsphere/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package vsphere

import (
"errors"
"fmt"

"k8s.io/klog/v2"

"sigs.k8s.io/yaml"
)

// ReadConfig parses vSphere cloud-config file and returns CPIConfig structure
// Accepts both YAML and INI formats as input.
// YAML format takes precedence, in case parsing YAML is not successful function falls back to the legacy INI format.
// Unlike 'cloud-provider-vsphere' version of a similar function, this does ignore environment variables.
func ReadConfig(config []byte) (*CPIConfig, error) {
if len(config) == 0 {
return nil, errors.New("vSphere config is empty")
}

klog.V(3).Info("Try to parse vSphere config, yaml format first")
cfg, err := readCPIConfigYAML(config)
if err != nil {
klog.V(3).Info("Parsing yaml config failed, fallback to ini")
klog.V(4).Infof("Yaml config parsing error:\n %s", err.Error())

cfg, err = readCPIConfigINI(config)
if err != nil {
return nil, fmt.Errorf("ini config parsing failed: %w", err)
}

klog.V(3).Info("ini config parsed successfully")
} else {
klog.V(3).Info("yaml config parsed successfully")
}

return cfg, nil
}

// MarshalConfig serializes CPIConfig instance into a YAML document
func MarshalConfig(config *CPIConfig) (string, error) {
yamlBytes, err := yaml.Marshal(config)
if err != nil {
return "", fmt.Errorf("can not marshal config into yaml: %w", err)
}
return string(yamlBytes), nil
}
226 changes: 226 additions & 0 deletions pkg/cloudprovider/vsphere/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package vsphere

import (
"fmt"
"strings"
"testing"

gmg "github.com/onsi/gomega"
)

const basicConfigINI = `
[Global]
server = 0.0.0.0
port = 443
user = user
password = password
insecure-flag = true
datacenters = us-west
ca-file = /some/path/to/a/ca.pem
`

const basicConfigYaml = `
global:
caFile: /some/path/to/a/ca.pem
datacenters:
- us-west
insecureFlag: true
password: password
port: 443
server: 0.0.0.0
user: user
`

const basicConfigVcenterSectionINI = `
[Global]
secret-name = "global-secret"
secret-namespace = "global-secret-ns"

[VirtualCenter "vc.rh.com"]
datacenters = "DC0,DC1"

[Labels]
region = "k8s-region"
zone = "k8s-zone"
`

const basicConfigVcenterSectionYAML = `
global:
secretName: global-secret
secretNamespace: global-secret-ns
labels:
region: k8s-region
zone: k8s-zone
vcenter:
vc.rh.com:
datacenters:
- DC0
- DC1
server: vc.rh.com
`

const multiVCDCsConfigINI = `
[Global]
port = 443
insecure-flag = true
secret-name = "global-secret"
secret-namespace = "global-secret-ns"

[VirtualCenter "t1"]
server = "10.0.0.1"
datacenters = "DC0,DC1,DC2"
secret-name = "tenant1-secret"
secret-namespace = "kube-system"

[VirtualCenter "10.0.0.2"]
datacenters = "DC3"

[VirtualCenter "10.0.0.3"]
datacenters = "DC5,DC6"
ip-family = "ipv6"
`

const invalidConfig = `boom[]{}`

const invalidConfigWrongGlobalPort = `
[Global]
port = -443
insecure-flag = true
`

const invalidConfigWrongVCPort = `
[Global]
port = 443
insecure-flag = true

[VirtualCenter "10.0.0.3"]
datacenters = "DC5,DC6"
port = -1
ip-family = "ipv6"
`

func TestINIConfigConversion(t *testing.T) {

t.Run("basic config yaml conversion", func(t *testing.T) {
g := gmg.NewWithT(t)
configStruct, err := ReadConfig([]byte(basicConfigINI))
g.Expect(err).ToNot(gmg.HaveOccurred())

convertedConfig, err := MarshalConfig(configStruct)
g.Expect(err).ToNot(gmg.HaveOccurred())
// Trim left emptyline just for keep constant more readable
g.Expect(convertedConfig).To(gmg.BeEquivalentTo(strings.TrimLeft(basicConfigYaml, "\n")))
})

t.Run("basic config yaml conversion with vc section", func(t *testing.T) {
g := gmg.NewWithT(t)
configStruct, err := ReadConfig([]byte(basicConfigVcenterSectionINI))
g.Expect(err).ToNot(gmg.HaveOccurred())

convertedConfig, err := MarshalConfig(configStruct)
g.Expect(err).ToNot(gmg.HaveOccurred())
// Trim left emptyline just for keep constant more readable
g.Expect(convertedConfig).To(gmg.BeEquivalentTo(strings.TrimLeft(basicConfigVcenterSectionYAML, "\n")))
})

t.Run("Test multi DCs config yaml conversion", func(t *testing.T) {
g := gmg.NewWithT(t)
configStruct, err := ReadConfig([]byte(multiVCDCsConfigINI))
g.Expect(err).ToNot(gmg.HaveOccurred())

g.Expect(len(configStruct.Vcenter)).To(gmg.Equal(3))

VC1 := configStruct.Vcenter["t1"]
g.Expect(len(VC1.Datacenters)).To(gmg.Equal(3))
g.Expect(VC1.Datacenters).To(gmg.BeComparableTo([]string{"DC0", "DC1", "DC2"}))
g.Expect(VC1.VCenterIP).To(gmg.Equal("10.0.0.1"))
g.Expect(VC1.SecretNamespace).To(gmg.Equal("kube-system"))
g.Expect(VC1.SecretName).To(gmg.Equal("tenant1-secret"))

VC2 := configStruct.Vcenter["10.0.0.2"]
g.Expect(len(VC2.Datacenters)).To(gmg.Equal(1))
g.Expect(VC2.Datacenters).To(gmg.BeComparableTo([]string{"DC3"}))
g.Expect(VC2.VCenterIP).To(gmg.Equal("10.0.0.2"))
g.Expect(VC2.IPFamilyPriority).To(gmg.Equal([]string{}))

VC3 := configStruct.Vcenter["10.0.0.3"]
g.Expect(len(VC3.Datacenters)).To(gmg.Equal(2))
g.Expect(VC3.Datacenters).To(gmg.BeComparableTo([]string{"DC5", "DC6"}))
g.Expect(VC3.IPFamilyPriority).To(gmg.Equal([]string{"ipv6"}))

_, err = MarshalConfig(configStruct)
g.Expect(err).ToNot(gmg.HaveOccurred())
})

invalidConfigTestCases := []struct {
name string
input string
errSubstring string
}{
{
"rubbish", invalidConfig, "expected section header",
},
{
"bad port", invalidConfigWrongGlobalPort, "invalid global port parameter: parsed int bigger than zero",
},
{
"bad vc port", invalidConfigWrongVCPort, "invalid port parameter for vc 10.0.0.3: parsed int bigger than zero",
},
{
"empty", "", "vSphere config is empty",
},
}

for _, tc := range invalidConfigTestCases {
t.Run(fmt.Sprintf("test invalid config: %s", tc.name), func(t *testing.T) {
g := gmg.NewWithT(t)
_, err := ReadConfig([]byte(tc.input))
g.Expect(err).To(gmg.MatchError(gmg.ContainSubstring(tc.errSubstring)))
})
}
}

func TestEmptyNodesOmittedInYAML(t *testing.T) {
g := gmg.NewWithT(t)

// Create a config with an empty Nodes struct
config := &CPIConfig{
CommonConfig: CommonConfig{
Global: Global{
User: "testuser",
Password: "testpass",
},
},
Nodes: Nodes{}, // Empty struct, should be omitted
}

yamlOutput, err := MarshalConfig(config)
g.Expect(err).ToNot(gmg.HaveOccurred())

// Verify that "nodes:" does not appear in the output
g.Expect(yamlOutput).ToNot(gmg.ContainSubstring("nodes:"))
}

func TestNodesWithValuesIncludedInYAML(t *testing.T) {
g := gmg.NewWithT(t)

// Create a config with a populated Nodes struct
config := &CPIConfig{
CommonConfig: CommonConfig{
Global: Global{
User: "testuser",
Password: "testpass",
},
},
Nodes: Nodes{
InternalNetworkSubnetCIDR: "192.168.1.0/24",
},
}

yamlOutput, err := MarshalConfig(config)
g.Expect(err).ToNot(gmg.HaveOccurred())

// Verify that "nodes:" DOES appear when the struct has values
g.Expect(yamlOutput).To(gmg.ContainSubstring("nodes:"))
g.Expect(yamlOutput).To(gmg.ContainSubstring("internalNetworkSubnetCidr: 192.168.1.0/24"))
}
Loading