diff --git a/data/data/manifests/openshift/cloud-creds-secret.yaml.template b/data/data/manifests/openshift/cloud-creds-secret.yaml.template index 90b595eb02f..1d97ed93dfd 100644 --- a/data/data/manifests/openshift/cloud-creds-secret.yaml.template +++ b/data/data/manifests/openshift/cloud-creds-secret.yaml.template @@ -6,6 +6,8 @@ metadata: name: aws-creds {{- else if .CloudCreds.OpenStack}} name: openstack-credentials +{{- else if .CloudCreds.VSphere}} + name: vsphere-creds {{- end}} data: {{- if .CloudCreds.AWS}} @@ -13,4 +15,9 @@ data: aws_secret_access_key: {{.CloudCreds.AWS.Base64encodeSecretAccessKey}} {{- else if .CloudCreds.OpenStack}} clouds.yaml: {{.CloudCreds.OpenStack.Base64encodeCloudCreds}} +{{- else if .CloudCreds.VSphere}} +{{- range .CloudCreds.VSphere.VirtualCenters}} + {{.Name}}.username: {{.Base64encodeUsername}} + {{.Name}}.password: {{.Base64encodePassword}} +{{- end}} {{- end}} diff --git a/pkg/asset/manifests/cloudproviderconfig.go b/pkg/asset/manifests/cloudproviderconfig.go new file mode 100644 index 00000000000..043fd0a8663 --- /dev/null +++ b/pkg/asset/manifests/cloudproviderconfig.go @@ -0,0 +1,123 @@ +package manifests + +import ( + "path/filepath" + + "github.com/ghodss/yaml" + ospclientconfig "github.com/gophercloud/utils/openstack/clientconfig" + "github.com/pkg/errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openshift/installer/pkg/asset" + "github.com/openshift/installer/pkg/asset/installconfig" + osmachine "github.com/openshift/installer/pkg/asset/machines/openstack" + vspheremanifests "github.com/openshift/installer/pkg/asset/manifests/vsphere" + awstypes "github.com/openshift/installer/pkg/types/aws" + libvirttypes "github.com/openshift/installer/pkg/types/libvirt" + nonetypes "github.com/openshift/installer/pkg/types/none" + openstacktypes "github.com/openshift/installer/pkg/types/openstack" + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" +) + +var ( + cloudProviderConfigFileName = filepath.Join(manifestDir, "cloud-provider-config.yaml") +) + +const ( + cloudProviderConfigDataKey = "config" +) + +// CloudProviderConfig generates the cloud-provider-config.yaml files. +type CloudProviderConfig struct { + ConfigMap *corev1.ConfigMap + File *asset.File +} + +var _ asset.WritableAsset = (*CloudProviderConfig)(nil) + +// Name returns a human friendly name for the asset. +func (*CloudProviderConfig) Name() string { + return "Cloud Provider Config" +} + +// Dependencies returns all of the dependencies directly needed to generate +// the asset. +func (*CloudProviderConfig) Dependencies() []asset.Asset { + return []asset.Asset{ + &installconfig.InstallConfig{}, + // PlatformCredsCheck just checks the creds (and asks, if needed) + // We do not actually use it in this asset directly, hence + // it is put in the dependencies but not fetched in Generate + &installconfig.PlatformCredsCheck{}, + } +} + +// Generate generates the CloudProviderConfig. +func (cpc *CloudProviderConfig) Generate(dependencies asset.Parents) error { + installConfig := &installconfig.InstallConfig{} + dependencies.Get(installConfig) + + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "openshift-config", + Name: "cloud-provider-config", + }, + Data: map[string]string{}, + } + + switch installConfig.Config.Platform.Name() { + case awstypes.Name, libvirttypes.Name, nonetypes.Name: + return nil + case openstacktypes.Name: + opts := &ospclientconfig.ClientOpts{} + opts.Cloud = installConfig.Config.Platform.OpenStack.Cloud + cloud, err := ospclientconfig.GetCloudFromYAML(opts) + if err != nil { + return errors.Wrap(err, "failed to get cloud config for openstack") + } + clouds := make(map[string]map[string]*ospclientconfig.Cloud) + clouds["clouds"] = map[string]*ospclientconfig.Cloud{ + osmachine.CloudName: cloud, + } + marshalled, err := yaml.Marshal(clouds) + if err != nil { + return err + } + cm.Data[cloudProviderConfigDataKey] = string(marshalled) + case vspheretypes.Name: + vsphereConfig, err := vspheremanifests.CloudProviderConfig(installConfig.Config.Platform.VSphere) + if err != nil { + return errors.Wrap(err, "could not create cloud provider config") + } + cm.Data[cloudProviderConfigDataKey] = vsphereConfig + default: + return errors.New("invalid Platform") + } + + cmData, err := yaml.Marshal(cm) + if err != nil { + return errors.Wrapf(err, "failed to create %s manifest", cpc.Name()) + } + cpc.ConfigMap = cm + cpc.File = &asset.File{ + Filename: cloudProviderConfigFileName, + Data: cmData, + } + return nil +} + +// Files returns the files generated by the asset. +func (cpc *CloudProviderConfig) Files() []*asset.File { + return []*asset.File{cpc.File} +} + +// Load loads the already-rendered files back from disk. +func (cpc *CloudProviderConfig) Load(f asset.FileFetcher) (bool, error) { + return false, nil +} diff --git a/pkg/asset/manifests/infrastructure.go b/pkg/asset/manifests/infrastructure.go index 091892dd129..1c0e69858f4 100644 --- a/pkg/asset/manifests/infrastructure.go +++ b/pkg/asset/manifests/infrastructure.go @@ -41,6 +41,7 @@ func (*Infrastructure) Dependencies() []asset.Asset { return []asset.Asset{ &installconfig.ClusterID{}, &installconfig.InstallConfig{}, + &CloudProviderConfig{}, } } @@ -48,7 +49,8 @@ func (*Infrastructure) Dependencies() []asset.Asset { func (i *Infrastructure) Generate(dependencies asset.Parents) error { clusterID := &installconfig.ClusterID{} installConfig := &installconfig.InstallConfig{} - dependencies.Get(clusterID, installConfig) + cloudproviderconfig := &CloudProviderConfig{} + dependencies.Get(clusterID, installConfig, cloudproviderconfig) var platform configv1.PlatformType switch installConfig.Config.Platform.Name() { @@ -83,18 +85,20 @@ func (i *Infrastructure) Generate(dependencies asset.Parents) error { }, } + if cloudproviderconfig.ConfigMap != nil { + // set the configmap reference. + config.Spec.CloudConfig = configv1.ConfigMapFileReference{Name: cloudproviderconfig.ConfigMap.Name, Key: cloudProviderConfigDataKey} + i.FileList = append(i.FileList, cloudproviderconfig.File) + } + configData, err := yaml.Marshal(config) if err != nil { return errors.Wrapf(err, "failed to marshal config: %#v", config) } - - i.FileList = []*asset.File{ - { - Filename: infraCfgFilename, - Data: configData, - }, - } - + i.FileList = append(i.FileList, &asset.File{ + Filename: infraCfgFilename, + Data: configData, + }) return nil } diff --git a/pkg/asset/manifests/openshift.go b/pkg/asset/manifests/openshift.go index 4289440faaa..152f3284bd7 100644 --- a/pkg/asset/manifests/openshift.go +++ b/pkg/asset/manifests/openshift.go @@ -14,6 +14,9 @@ import ( osmachine "github.com/openshift/installer/pkg/asset/machines/openstack" "github.com/openshift/installer/pkg/asset/password" "github.com/openshift/installer/pkg/asset/templates/content/openshift" + awstypes "github.com/openshift/installer/pkg/types/aws" + openstacktypes "github.com/openshift/installer/pkg/types/openstack" + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" ) const ( @@ -56,7 +59,7 @@ func (o *Openshift) Generate(dependencies asset.Parents) error { var cloudCreds cloudCredsSecretData platform := installConfig.Config.Platform.Name() switch platform { - case "aws": + case awstypes.Name: ssn := session.Must(session.NewSessionWithOptions(session.Options{ SharedConfigState: session.SharedConfigEnable, })) @@ -70,7 +73,7 @@ func (o *Openshift) Generate(dependencies asset.Parents) error { Base64encodeSecretAccessKey: base64.StdEncoding.EncodeToString([]byte(creds.SecretAccessKey)), }, } - case "openstack": + case openstacktypes.Name: opts := new(clientconfig.ClientOpts) opts.Cloud = installConfig.Config.Platform.OpenStack.Cloud cloud, err := clientconfig.GetCloudFromYAML(opts) @@ -93,6 +96,18 @@ func (o *Openshift) Generate(dependencies asset.Parents) error { Base64encodeCloudCreds: credsEncoded, }, } + case vspheretypes.Name: + vcCreds := make([]VSphereVirtualCenterCredsSecretData, len(installConfig.Config.VSphere.VirtualCenters)) + for i, vc := range installConfig.Config.VSphere.VirtualCenters { + vcCreds[i].Name = vc.Name + vcCreds[i].Base64encodeUsername = base64.StdEncoding.EncodeToString([]byte(vc.Username)) + vcCreds[i].Base64encodePassword = base64.StdEncoding.EncodeToString([]byte(vc.Password)) + } + cloudCreds = cloudCredsSecretData{ + VSphere: &VSphereCredsSecretData{ + VirtualCenters: vcCreds, + }, + } } templateData := &openshiftTemplateData{ @@ -116,7 +131,7 @@ func (o *Openshift) Generate(dependencies asset.Parents) error { } switch platform { - case "aws", "openstack": + case awstypes.Name, openstacktypes.Name, vspheretypes.Name: assetData["99_cloud-creds-secret.yaml"] = applyTemplateData(cloudCredsSecret.Files()[0].Data, templateData) assetData["99_role-cloud-creds-secret-reader.yaml"] = applyTemplateData(roleCloudCredsSecretReader.Files()[0].Data, templateData) } diff --git a/pkg/asset/manifests/operators.go b/pkg/asset/manifests/operators.go index 2ca28b73321..bf5d0e70c58 100644 --- a/pkg/asset/manifests/operators.go +++ b/pkg/asset/manifests/operators.go @@ -17,6 +17,7 @@ import ( "github.com/openshift/installer/pkg/asset/templates/content/bootkube" "github.com/openshift/installer/pkg/asset/tls" "github.com/openshift/installer/pkg/types" + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" ) const ( @@ -166,27 +167,26 @@ func (m *Manifests) generateBootKubeManifests(dependencies asset.Parents) []*ass } templateData := &bootkubeTemplateData{ - Base64encodeCloudProviderConfig: "", // FIXME - CVOClusterID: clusterID.UUID, - EtcdCaBundle: base64.StdEncoding.EncodeToString(etcdCABundle.Cert()), - EtcdCaCert: string(etcdCA.Cert()), - EtcdClientCaCert: base64.StdEncoding.EncodeToString(etcdCA.Cert()), - EtcdClientCaKey: base64.StdEncoding.EncodeToString(etcdCA.Key()), - EtcdClientCert: base64.StdEncoding.EncodeToString(etcdClientCertKey.Cert()), - EtcdClientKey: base64.StdEncoding.EncodeToString(etcdClientCertKey.Key()), - EtcdEndpointDNSSuffix: installConfig.Config.ClusterDomain(), - EtcdEndpointHostnames: etcdEndpointHostnames, - EtcdMetricCaCert: string(etcdMetricCABundle.Cert()), - EtcdMetricClientCert: base64.StdEncoding.EncodeToString(etcdMetricSignerClientCertKey.Cert()), - EtcdMetricClientKey: base64.StdEncoding.EncodeToString(etcdMetricSignerClientCertKey.Key()), - EtcdSignerCert: base64.StdEncoding.EncodeToString(etcdSignerCertKey.Cert()), - EtcdSignerClientCert: base64.StdEncoding.EncodeToString(etcdSignerClientCertKey.Cert()), - EtcdSignerClientKey: base64.StdEncoding.EncodeToString(etcdSignerClientCertKey.Key()), - EtcdSignerKey: base64.StdEncoding.EncodeToString(etcdSignerCertKey.Key()), - McsTLSCert: base64.StdEncoding.EncodeToString(mcsCertKey.Cert()), - McsTLSKey: base64.StdEncoding.EncodeToString(mcsCertKey.Key()), - PullSecretBase64: base64.StdEncoding.EncodeToString([]byte(installConfig.Config.PullSecret)), - RootCaCert: string(rootCA.Cert()), + CVOClusterID: clusterID.UUID, + EtcdCaBundle: base64.StdEncoding.EncodeToString(etcdCABundle.Cert()), + EtcdCaCert: string(etcdCA.Cert()), + EtcdClientCaCert: base64.StdEncoding.EncodeToString(etcdCA.Cert()), + EtcdClientCaKey: base64.StdEncoding.EncodeToString(etcdCA.Key()), + EtcdClientCert: base64.StdEncoding.EncodeToString(etcdClientCertKey.Cert()), + EtcdClientKey: base64.StdEncoding.EncodeToString(etcdClientCertKey.Key()), + EtcdEndpointDNSSuffix: installConfig.Config.ClusterDomain(), + EtcdEndpointHostnames: etcdEndpointHostnames, + EtcdMetricCaCert: string(etcdMetricCABundle.Cert()), + EtcdMetricClientCert: base64.StdEncoding.EncodeToString(etcdMetricSignerClientCertKey.Cert()), + EtcdMetricClientKey: base64.StdEncoding.EncodeToString(etcdMetricSignerClientCertKey.Key()), + EtcdSignerCert: base64.StdEncoding.EncodeToString(etcdSignerCertKey.Cert()), + EtcdSignerClientCert: base64.StdEncoding.EncodeToString(etcdSignerClientCertKey.Cert()), + EtcdSignerClientKey: base64.StdEncoding.EncodeToString(etcdSignerClientCertKey.Key()), + EtcdSignerKey: base64.StdEncoding.EncodeToString(etcdSignerCertKey.Key()), + McsTLSCert: base64.StdEncoding.EncodeToString(mcsCertKey.Cert()), + McsTLSKey: base64.StdEncoding.EncodeToString(mcsCertKey.Key()), + PullSecretBase64: base64.StdEncoding.EncodeToString([]byte(installConfig.Config.PullSecret)), + RootCaCert: string(rootCA.Cert()), } files := []*asset.File{} @@ -264,6 +264,16 @@ func (m *Manifests) Load(f asset.FileFetcher) (bool, error) { func redactedInstallConfig(config types.InstallConfig) ([]byte, error) { config.PullSecret = "" + if config.Platform.VSphere != nil { + p := *config.Platform.VSphere + p.VirtualCenters = make([]vspheretypes.VirtualCenter, len(config.Platform.VSphere.VirtualCenters)) + for i, vc := range config.Platform.VSphere.VirtualCenters { + vc.Username = "" + vc.Password = "" + p.VirtualCenters[i] = vc + } + config.Platform.VSphere = &p + } return yaml.Marshal(config) } diff --git a/pkg/asset/manifests/operators_test.go b/pkg/asset/manifests/operators_test.go new file mode 100644 index 00000000000..0db150f1dd6 --- /dev/null +++ b/pkg/asset/manifests/operators_test.go @@ -0,0 +1,126 @@ +package manifests + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + + "github.com/openshift/installer/pkg/ipnet" + "github.com/openshift/installer/pkg/types" + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" +) + +// TestRedactedInstallConfig tests the redactedInstallConfig function. +func TestRedactedInstallConfig(t *testing.T) { + createInstallConfig := func() *types.InstallConfig { + return &types.InstallConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + }, + SSHKey: "test-ssh-key", + BaseDomain: "test-domain", + Networking: &types.Networking{ + MachineCIDR: ipnet.MustParseCIDR("1.2.3.4/5"), + NetworkType: "test-network-type", + ClusterNetwork: []types.ClusterNetworkEntry{ + { + CIDR: *ipnet.MustParseCIDR("1.2.3.4/5"), + HostPrefix: 6, + }, + }, + ServiceNetwork: []ipnet.IPNet{*ipnet.MustParseCIDR("1.2.3.4/5")}, + }, + ControlPlane: &types.MachinePool{ + Name: "control-plane", + Replicas: pointer.Int64Ptr(3), + }, + Compute: []types.MachinePool{ + { + Name: "compute", + Replicas: pointer.Int64Ptr(3), + }, + }, + Platform: types.Platform{ + VSphere: &vspheretypes.Platform{ + VirtualCenters: []vspheretypes.VirtualCenter{ + { + Name: "test-server-1", + Username: "test-user-1", + Password: "test-pass-1", + Datacenters: []string{"test-datacenter-1"}, + }, + { + Name: "test-server-2", + Username: "test-user-2", + Password: "test-pass-2", + Datacenters: []string{"test-datacenter-2"}, + }, + }, + Workspace: vspheretypes.Workspace{ + Server: "test-server-1", + Datacenter: "test-datacenter-1", + DefaultDatastore: "test-datastore", + ResourcePoolPath: "test-resource-pool", + Folder: "test-folder", + }, + SCSIControllerType: "test-controller-type", + PublicNetwork: "test-network", + }, + }, + PullSecret: "test-pull-secret", + } + } + expectedConfig := createInstallConfig() + expectedYaml := `baseDomain: test-domain +compute: +- name: compute + platform: {} + replicas: 3 +controlPlane: + name: control-plane + platform: {} + replicas: 3 +metadata: + creationTimestamp: null + name: test-cluster +networking: + clusterNetwork: + - cidr: 1.2.3.4/5 + hostPrefix: 6 + machineCIDR: 1.2.3.4/5 + networkType: test-network-type + serviceNetwork: + - 1.2.3.4/5 +platform: + vsphere: + publicNetwork: test-network + scsiControllerType: test-controller-type + virtualCenters: + - datacenters: + - test-datacenter-1 + name: test-server-1 + password: "" + username: "" + - datacenters: + - test-datacenter-2 + name: test-server-2 + password: "" + username: "" + workspace: + datacenter: test-datacenter-1 + defaultDatastore: test-datastore + folder: test-folder + resourcePoolPath: test-resource-pool + server: test-server-1 +pullSecret: "" +sshKey: test-ssh-key +` + ic := createInstallConfig() + actualYaml, err := redactedInstallConfig(*ic) + if assert.NoError(t, err, "unexpected error") { + assert.Equal(t, expectedYaml, string(actualYaml), "unexpected yaml") + } + assert.Equal(t, expectedConfig, ic, "install config was unexpectedly modified") +} diff --git a/pkg/asset/manifests/template.go b/pkg/asset/manifests/template.go index a2a437b57ab..c6714707d70 100644 --- a/pkg/asset/manifests/template.go +++ b/pkg/asset/manifests/template.go @@ -11,34 +11,46 @@ type OpenStackCredsSecretData struct { Base64encodeCloudCreds string } +// VSphereCredsSecretData holds encoded credentials and is used to generated cloud-creds secret +type VSphereCredsSecretData struct { + VirtualCenters []VSphereVirtualCenterCredsSecretData +} + +// VSphereVirtualCenterCredsSecretData holds the encoded credentials for a vSphere vCenter. +type VSphereVirtualCenterCredsSecretData struct { + Name string + Base64encodeUsername string + Base64encodePassword string +} + type cloudCredsSecretData struct { AWS *AwsCredsSecretData OpenStack *OpenStackCredsSecretData + VSphere *VSphereCredsSecretData } type bootkubeTemplateData struct { - Base64encodeCloudProviderConfig string - CVOClusterID string - EtcdCaBundle string - EtcdCaCert string - EtcdClientCaCert string - EtcdClientCaKey string - EtcdClientCert string - EtcdClientKey string - EtcdEndpointDNSSuffix string - EtcdEndpointHostnames []string - EtcdMetricCaCert string - EtcdMetricClientCert string - EtcdMetricClientKey string - EtcdSignerCert string - EtcdSignerClientCert string - EtcdSignerClientKey string - EtcdSignerKey string - McsTLSCert string - McsTLSKey string - PullSecretBase64 string - RootCaCert string - WorkerIgnConfig string + CVOClusterID string + EtcdCaBundle string + EtcdCaCert string + EtcdClientCaCert string + EtcdClientCaKey string + EtcdClientCert string + EtcdClientKey string + EtcdEndpointDNSSuffix string + EtcdEndpointHostnames []string + EtcdMetricCaCert string + EtcdMetricClientCert string + EtcdMetricClientKey string + EtcdSignerCert string + EtcdSignerClientCert string + EtcdSignerClientKey string + EtcdSignerKey string + McsTLSCert string + McsTLSKey string + PullSecretBase64 string + RootCaCert string + WorkerIgnConfig string } type openshiftTemplateData struct { diff --git a/pkg/asset/manifests/vsphere/cloudproviderconfig.go b/pkg/asset/manifests/vsphere/cloudproviderconfig.go new file mode 100644 index 00000000000..0bc0d7a5287 --- /dev/null +++ b/pkg/asset/manifests/vsphere/cloudproviderconfig.go @@ -0,0 +1,87 @@ +package vsphere + +import ( + "bytes" + "fmt" + + "github.com/pkg/errors" + ini "gopkg.in/ini.v1" + + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" +) + +type config struct { + Global global + Workspace workspace + Disk disk + Network network +} + +type global struct { + SecretName string `ini:"secret-name"` + SecretNamespace string `ini:"secret-namespace"` +} + +type workspace struct { + Server string `ini:"server"` + Datacenter string `ini:"datacenter"` + DefaultDatastore string `ini:"default-datastore"` + ResourcePoolPath string `ini:"resourcepool-path,omitempty"` + Folder string `ini:"folder"` +} + +type disk struct { + SCSIControllerType string `ini:"scsicontrollertype"` +} + +type network struct { + PublicNetwork string `ini:"public-network"` +} + +type virtualCenter struct { + Datacenters []string `ini:"datacenters"` +} + +// CloudProviderConfig generates the cloud provider config for the vSphere platform. +func CloudProviderConfig(p *vspheretypes.Platform) (string, error) { + file := ini.Empty() + config := &config{ + Global: global{ + SecretName: "vsphere-creds", + SecretNamespace: "kube-system", + }, + Workspace: workspace{ + Server: p.Workspace.Server, + Datacenter: p.Workspace.Datacenter, + DefaultDatastore: p.Workspace.DefaultDatastore, + ResourcePoolPath: p.Workspace.ResourcePoolPath, + Folder: p.Workspace.Folder, + }, + Disk: disk{ + SCSIControllerType: p.SCSIControllerType, + }, + Network: network{ + PublicNetwork: p.PublicNetwork, + }, + } + if err := file.ReflectFrom(config); err != nil { + return "", errors.Wrap(err, "failed to reflect from config") + } + for _, vc := range p.VirtualCenters { + s, err := file.NewSection(fmt.Sprintf("VirtualCenter %q", vc.Name)) + if err != nil { + return "", errors.Wrapf(err, "failed to create section for virtual center %q", vc.Name) + } + if err := s.ReflectFrom( + &virtualCenter{ + Datacenters: vc.Datacenters, + }); err != nil { + return "", errors.Wrapf(err, "failed to reflect from virtual center %q", vc.Name) + } + } + buf := &bytes.Buffer{} + if _, err := file.WriteTo(buf); err != nil { + return "", errors.Wrap(err, "failed to write out cloud provider config") + } + return buf.String(), nil +} diff --git a/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go b/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go new file mode 100644 index 00000000000..f2a967f1fbf --- /dev/null +++ b/pkg/asset/manifests/vsphere/cloudproviderconfig_test.go @@ -0,0 +1,204 @@ +package vsphere + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" +) + +func TestCloudProviderConfig(t *testing.T) { + cases := []struct { + name string + platform *vspheretypes.Platform + expectedConfig string + }{ + { + name: "single virtualcenter", + platform: &vspheretypes.Platform{ + VirtualCenters: []vspheretypes.VirtualCenter{ + { + Name: "test-name", + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-datacenter"}, + }, + }, + Workspace: vspheretypes.Workspace{ + Server: "test-name", + Datacenter: "test-datacenter", + DefaultDatastore: "test-datastore", + ResourcePoolPath: "test-resource-pool", + Folder: "test-folder", + }, + SCSIControllerType: "test-scsi", + PublicNetwork: "test-network", + }, + expectedConfig: `[Global] +secret-name = vsphere-creds +secret-namespace = kube-system + +[Workspace] +server = test-name +datacenter = test-datacenter +default-datastore = test-datastore +resourcepool-path = test-resource-pool +folder = test-folder + +[Disk] +scsicontrollertype = test-scsi + +[Network] +public-network = test-network + +[VirtualCenter "test-name"] +datacenters = test-datacenter + +`, + }, + { + name: "multiple datacenters", + platform: &vspheretypes.Platform{ + VirtualCenters: []vspheretypes.VirtualCenter{ + { + Name: "test-name", + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-dc1", "test-dc2"}, + }, + }, + Workspace: vspheretypes.Workspace{ + Server: "test-name", + Datacenter: "test-dc1", + DefaultDatastore: "test-datastore", + ResourcePoolPath: "test-resource-pool", + Folder: "test-folder", + }, + SCSIControllerType: "test-scsi", + PublicNetwork: "test-network", + }, + expectedConfig: `[Global] +secret-name = vsphere-creds +secret-namespace = kube-system + +[Workspace] +server = test-name +datacenter = test-dc1 +default-datastore = test-datastore +resourcepool-path = test-resource-pool +folder = test-folder + +[Disk] +scsicontrollertype = test-scsi + +[Network] +public-network = test-network + +[VirtualCenter "test-name"] +datacenters = test-dc1,test-dc2 + +`, + }, + { + name: "multiple virtualcenters", + platform: &vspheretypes.Platform{ + VirtualCenters: []vspheretypes.VirtualCenter{ + { + Name: "test-name1", + Username: "test-username1", + Password: "test-password1", + Datacenters: []string{"test-datacenter1"}, + }, + { + Name: "test-name2", + Username: "test-username2", + Password: "test-password2", + Datacenters: []string{"test-datacenter2"}, + }, + }, + Workspace: vspheretypes.Workspace{ + Server: "test-name", + Datacenter: "test-datacenter", + DefaultDatastore: "test-datastore", + ResourcePoolPath: "test-resource-pool", + Folder: "test-folder", + }, + SCSIControllerType: "test-scsi", + PublicNetwork: "test-network", + }, + expectedConfig: `[Global] +secret-name = vsphere-creds +secret-namespace = kube-system + +[Workspace] +server = test-name +datacenter = test-datacenter +default-datastore = test-datastore +resourcepool-path = test-resource-pool +folder = test-folder + +[Disk] +scsicontrollertype = test-scsi + +[Network] +public-network = test-network + +[VirtualCenter "test-name1"] +datacenters = test-datacenter1 + +[VirtualCenter "test-name2"] +datacenters = test-datacenter2 + +`, + }, + { + name: "empty resource pool path", + platform: &vspheretypes.Platform{ + VirtualCenters: []vspheretypes.VirtualCenter{ + { + Name: "test-name", + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-datacenter"}, + }, + }, + Workspace: vspheretypes.Workspace{ + Server: "test-name", + Datacenter: "test-datacenter", + DefaultDatastore: "test-datastore", + Folder: "test-folder", + }, + SCSIControllerType: "test-scsi", + PublicNetwork: "test-network", + }, + expectedConfig: `[Global] +secret-name = vsphere-creds +secret-namespace = kube-system + +[Workspace] +server = test-name +datacenter = test-datacenter +default-datastore = test-datastore +folder = test-folder + +[Disk] +scsicontrollertype = test-scsi + +[Network] +public-network = test-network + +[VirtualCenter "test-name"] +datacenters = test-datacenter + +`, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + config, err := CloudProviderConfig(tc.platform) + assert.NoError(t, err, "failed to create cloud provider config") + assert.Equal(t, tc.expectedConfig, config, "unexpected cloud provider config") + }) + } +} diff --git a/pkg/types/defaults/installconfig.go b/pkg/types/defaults/installconfig.go index fca1eefbdda..cec0bb48a91 100644 --- a/pkg/types/defaults/installconfig.go +++ b/pkg/types/defaults/installconfig.go @@ -7,6 +7,7 @@ import ( libvirtdefaults "github.com/openshift/installer/pkg/types/libvirt/defaults" nonedefaults "github.com/openshift/installer/pkg/types/none/defaults" openstackdefaults "github.com/openshift/installer/pkg/types/openstack/defaults" + vspheredefaults "github.com/openshift/installer/pkg/types/vsphere/defaults" ) var ( @@ -72,6 +73,8 @@ func SetInstallConfigDefaults(c *types.InstallConfig) { libvirtdefaults.SetPlatformDefaults(c.Platform.Libvirt) case c.Platform.OpenStack != nil: openstackdefaults.SetPlatformDefaults(c.Platform.OpenStack) + case c.Platform.VSphere != nil: + vspheredefaults.SetPlatformDefaults(c.Platform.VSphere, c) case c.Platform.None != nil: nonedefaults.SetPlatformDefaults(c.Platform.None) } diff --git a/pkg/types/vsphere/defaults/platform.go b/pkg/types/vsphere/defaults/platform.go index 407bc58eee8..3898d822b87 100644 --- a/pkg/types/vsphere/defaults/platform.go +++ b/pkg/types/vsphere/defaults/platform.go @@ -1,9 +1,31 @@ package defaults import ( + "github.com/openshift/installer/pkg/types" "github.com/openshift/installer/pkg/types/vsphere" ) // SetPlatformDefaults sets the defaults for the platform. -func SetPlatformDefaults(p *vsphere.Platform) { +func SetPlatformDefaults(p *vsphere.Platform, installConfig *types.InstallConfig) { + if p.Workspace.Server == "" { + if len(p.VirtualCenters) == 1 { + p.Workspace.Server = p.VirtualCenters[0].Name + } + } + if p.Workspace.Datacenter == "" { + for _, vc := range p.VirtualCenters { + if p.Workspace.Server == vc.Name { + if len(vc.Datacenters) == 1 { + p.Workspace.Datacenter = vc.Datacenters[0] + } + break + } + } + } + if p.Workspace.Folder == "" { + p.Workspace.Folder = installConfig.ObjectMeta.Name + } + if p.SCSIControllerType == "" { + p.SCSIControllerType = "pvscsi" + } } diff --git a/pkg/types/vsphere/defaults/platform_test.go b/pkg/types/vsphere/defaults/platform_test.go index dd03ee76d77..241b05ececf 100644 --- a/pkg/types/vsphere/defaults/platform_test.go +++ b/pkg/types/vsphere/defaults/platform_test.go @@ -4,12 +4,21 @@ import ( "testing" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/openshift/installer/pkg/types" "github.com/openshift/installer/pkg/types/vsphere" ) +const testClusterName = "test-cluster" + func defaultPlatform() *vsphere.Platform { - return &vsphere.Platform{} + return &vsphere.Platform{ + Workspace: vsphere.Workspace{ + Folder: testClusterName, + }, + SCSIControllerType: "pvscsi", + } } func TestSetPlatformDefaults(t *testing.T) { @@ -23,10 +32,161 @@ func TestSetPlatformDefaults(t *testing.T) { platform: &vsphere.Platform{}, expected: defaultPlatform(), }, + { + name: "single vCenter", + platform: &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{{Name: "test-server"}}, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.VirtualCenters = []vsphere.VirtualCenter{{Name: "test-server"}} + p.Workspace.Server = "test-server" + return p + }(), + }, + { + name: "multiple vCenters", + platform: &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{ + {Name: "test-server1"}, + {Name: "test-server2"}, + }, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.VirtualCenters = []vsphere.VirtualCenter{ + {Name: "test-server1"}, + {Name: "test-server2"}, + } + return p + }(), + }, + { + name: "vCenter set", + platform: &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{{Name: "test-server"}}, + Workspace: vsphere.Workspace{ + Server: "other-server", + }, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.VirtualCenters = []vsphere.VirtualCenter{{Name: "test-server"}} + p.Workspace.Server = "other-server" + return p + }(), + }, + { + name: "single datacenter", + platform: &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{ + { + Name: "test-server", + Datacenters: []string{"test-datacenter"}, + }, + }, + Workspace: vsphere.Workspace{ + Server: "test-server", + }, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.VirtualCenters = []vsphere.VirtualCenter{ + { + Name: "test-server", + Datacenters: []string{"test-datacenter"}, + }, + } + p.Workspace.Server = "test-server" + p.Workspace.Datacenter = "test-datacenter" + return p + }(), + }, + { + name: "multiple datacenter", + platform: &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{ + { + Name: "test-server", + Datacenters: []string{"test-datacenter1", "test-datacenter2"}, + }, + }, + Workspace: vsphere.Workspace{ + Server: "test-server", + }, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.VirtualCenters = []vsphere.VirtualCenter{ + { + Name: "test-server", + Datacenters: []string{"test-datacenter1", "test-datacenter2"}, + }, + } + p.Workspace.Server = "test-server" + return p + }(), + }, + { + name: "datacenter set", + platform: &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{ + { + Name: "test-server", + Datacenters: []string{"test-datacenter"}, + }, + }, + Workspace: vsphere.Workspace{ + Server: "test-server", + Datacenter: "other-datacenter", + }, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.VirtualCenters = []vsphere.VirtualCenter{ + { + Name: "test-server", + Datacenters: []string{"test-datacenter"}, + }, + } + p.Workspace.Server = "test-server" + p.Workspace.Datacenter = "other-datacenter" + return p + }(), + }, + { + name: "folder set", + platform: &vsphere.Platform{ + Workspace: vsphere.Workspace{ + Folder: "test-folder", + }, + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.Workspace.Folder = "test-folder" + return p + }(), + }, + { + name: "SCSI controller type set", + platform: &vsphere.Platform{ + SCSIControllerType: "test-controller-type", + }, + expected: func() *vsphere.Platform { + p := defaultPlatform() + p.SCSIControllerType = "test-controller-type" + return p + }(), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - SetPlatformDefaults(tc.platform) + ic := &types.InstallConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: testClusterName, + }, + } + SetPlatformDefaults(tc.platform, ic) assert.Equal(t, tc.expected, tc.platform, "unexpected platform") }) } diff --git a/pkg/types/vsphere/platform.go b/pkg/types/vsphere/platform.go index a7a1cce6b12..7e4ea564f6c 100644 --- a/pkg/types/vsphere/platform.go +++ b/pkg/types/vsphere/platform.go @@ -1,5 +1,50 @@ package vsphere -// Platform stores any global configuration used for vsphere -// platforms. -type Platform struct{} +// Platform stores any global configuration used for vsphere platforms. +type Platform struct { + // VirtualCenters are the configurations for the vCenters. + VirtualCenters []VirtualCenter `json:"virtualCenters"` + // Workspace is the configuration for the workspace. + Workspace Workspace `json:"workspace"` + // SCSIControllerType is the SCSI controller type in use. + // +optional + // Default is pvscsi. + SCSIControllerType string `json:"scsiControllerType"` + // PublicNetwork is the name of the VM network to use. + PublicNetwork string `json:"publicNetwork"` +} + +// VirtualCenter is the configuration of a vCenter. +type VirtualCenter struct { + // Name of the vCenter. This is the domain name or the IP address of the vCenter. + Name string `json:"name"` + // Username is the name of the user to use to connect to the vCenter. + Username string `json:"username"` + // Password is the password for the user to use to connect to the vCenter. + Password string `json:"password"` + // Datacenters are the names of the datacenters to use in the vCenter. + Datacenters []string `json:"datacenters"` +} + +// Workspace is the configuration of the vSphere workspace. +type Workspace struct { + // Server is the server to use for provisioning. + // +optional + // Default is the name of the vCenter, if there is only a single vCenter. + Server string `json:"server"` + // Datacenter is the datacenter to use for provisioning. + // +optional + // Default is the datacenter in Server, if that vCenter has only a single + // datacenter. + Datacenter string `json:"datacenter"` + // DefaultDatastore is the default datastore to use for provisioning volumes. + DefaultDatastore string `json:"defaultDatastore"` + // ResourcePoolPath is the resource pool to use in the datacenter. + // +optional + // Default is the name of the cluster. + ResourcePoolPath string `json:"resourcePoolPath"` + // Folder is the vCenter VM folder path in the datacenter. + // +optional + // Default is the name of the cluster. + Folder string `json:"folder"` +} diff --git a/pkg/types/vsphere/validation/platform.go b/pkg/types/vsphere/validation/platform.go index cfe8ce00c29..05288c8d745 100644 --- a/pkg/types/vsphere/validation/platform.go +++ b/pkg/types/vsphere/validation/platform.go @@ -8,5 +8,75 @@ import ( // ValidatePlatform checks that the specified platform is valid. func ValidatePlatform(p *vsphere.Platform, fldPath *field.Path) field.ErrorList { - return field.ErrorList{} + allErrs := field.ErrorList{} + if len(p.VirtualCenters) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("virtualCenters"), "must include at least one vCenter")) + } + foundServer := false + vcNames := map[string]bool{} + for i, vc := range p.VirtualCenters { + allErrs = append(allErrs, validateVirtualCenter(&vc, fldPath.Child("virtualCenters").Index(i))...) + if vcNames[vc.Name] { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("virtualCenters").Index(i), vc.Name)) + } + vcNames[vc.Name] = true + if vc.Name == p.Workspace.Server { + foundDatacenter := false + for _, dc := range vc.Datacenters { + if dc == p.Workspace.Datacenter { + foundDatacenter = true + break + } + } + if p.Workspace.Datacenter != "" && !foundDatacenter { + allErrs = append(allErrs, field.Invalid(fldPath.Child("workspace").Child("datacenter"), p.Workspace.Datacenter, "workspace datacenter must be a datacenter in the workspace server")) + } + foundServer = true + } + } + if len(p.Workspace.Server) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("workspace").Child("server"), "must specify the workspace server")) + } else if !foundServer { + allErrs = append(allErrs, field.Invalid(fldPath.Child("workspace").Child("server"), p.Workspace.Server, "workspace server must be a specified vCenter")) + } + if len(p.Workspace.Datacenter) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("workspace").Child("datacenter"), "must specify the workspace datacenter")) + } + if len(p.Workspace.DefaultDatastore) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("workspace").Child("defaultDatastore"), "must specify the default datastore")) + } + if len(p.Workspace.Folder) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("workspace").Child("folder"), "must specify the VM folder")) + } + if len(p.SCSIControllerType) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("scsiControllerType"), "must specify the SCSI controller type")) + } + if len(p.PublicNetwork) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("publicNetwork"), "must specify the public VM network")) + } + return allErrs +} + +func validateVirtualCenter(vc *vsphere.VirtualCenter, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(vc.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "vCenter must have a name")) + } + if len(vc.Username) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("username"), "username required for each vCenter")) + } + if len(vc.Password) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("password"), "password required for each vCenter")) + } + if len(vc.Datacenters) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("datacenters"), "must include at least one datacenter")) + } + dcs := map[string]bool{} + for i, dc := range vc.Datacenters { + if dcs[dc] { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("datacenters").Index(i), dc)) + } + dcs[dc] = true + } + return allErrs } diff --git a/pkg/types/vsphere/validation/platform_test.go b/pkg/types/vsphere/validation/platform_test.go index c78cb34191b..30d03aa8398 100644 --- a/pkg/types/vsphere/validation/platform_test.go +++ b/pkg/types/vsphere/validation/platform_test.go @@ -10,28 +10,179 @@ import ( ) func validPlatform() *vsphere.Platform { - return &vsphere.Platform{} + return &vsphere.Platform{ + VirtualCenters: []vsphere.VirtualCenter{ + { + Name: "test-server-name", + Username: "test-username", + Password: "test-password", + Datacenters: []string{"test-datacenter"}, + }, + }, + Workspace: vsphere.Workspace{ + Server: "test-server-name", + Datacenter: "test-datacenter", + DefaultDatastore: "test-datastore", + ResourcePoolPath: "test-resource-pool", + Folder: "test-folder", + }, + SCSIControllerType: "test-controller-type", + PublicNetwork: "test-network", + } } func TestValidatePlatform(t *testing.T) { cases := []struct { - name string - platform *vsphere.Platform - valid bool + name string + platform *vsphere.Platform + expectedError string }{ { name: "minimal", platform: validPlatform(), - valid: true, + }, + { + name: "empty vCenters", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters = nil + return p + }(), + expectedError: `^\[test-path\.virtualCenters: Required value: must include at least one vCenter, test-path\.workspace\.server: Invalid value: "test-server-name": workspace server must be a specified vCenter]$`, + }, + { + name: "missing vCenter name", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters[0].Name = "" + return p + }(), + expectedError: `^\[test-path\.virtualCenters\[0]\.name: Required value: vCenter must have a name, test-path\.workspace\.server: Invalid value: "test-server-name": workspace server must be a specified vCenter]$`, + }, + { + name: "missing vCenter username", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters[0].Username = "" + return p + }(), + expectedError: `^test-path\.virtualCenters\[0]\.username: Required value: username required for each vCenter$`, + }, + { + name: "missing vCenter password", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters[0].Password = "" + return p + }(), + expectedError: `^test-path\.virtualCenters\[0]\.password: Required value: password required for each vCenter$`, + }, + { + name: "empty vCenter datacenters", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters[0].Datacenters = nil + return p + }(), + expectedError: `^\[test-path\.virtualCenters\[0]\.datacenters: Required value: must include at least one datacenter, test-path\.workspace\.datacenter: Invalid value: "test-datacenter": workspace datacenter must be a datacenter in the workspace server]$`, + }, + { + name: "multiple vCenter datacenters", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters[0].Datacenters = []string{"test-datacenter", "other-datacenter"} + return p + }(), + }, + { + name: "duplicate vCenter datacenters", + platform: func() *vsphere.Platform { + p := validPlatform() + p.VirtualCenters[0].Datacenters = []string{"test-datacenter", "test-datacenter"} + return p + }(), + expectedError: `^test-path\.virtualCenters\[0]\.datacenters\[1]: Duplicate value: "test-datacenter"$`, + }, + { + name: "missing workspace server", + platform: func() *vsphere.Platform { + p := validPlatform() + p.Workspace.Server = "" + return p + }(), + expectedError: `^test-path\.workspace\.server: Required value: must specify the workspace server$`, + }, + { + name: "no vCenter for workspace server", + platform: func() *vsphere.Platform { + p := validPlatform() + p.Workspace.Server = "other-server-name" + return p + }(), + expectedError: `^test-path\.workspace\.server: Invalid value: "other-server-name": workspace server must be a specified vCenter$`, + }, + { + name: "missing workspace datacenter", + platform: func() *vsphere.Platform { + p := validPlatform() + p.Workspace.Datacenter = "" + return p + }(), + expectedError: `^test-path\.workspace\.datacenter: Required value: must specify the workspace datacenter$`, + }, + { + name: "missing workspace datacenter", + platform: func() *vsphere.Platform { + p := validPlatform() + p.Workspace.Datacenter = "" + return p + }(), + expectedError: `^test-path\.workspace\.datacenter: Required value: must specify the workspace datacenter$`, + }, + { + name: "missing default datastore", + platform: func() *vsphere.Platform { + p := validPlatform() + p.Workspace.DefaultDatastore = "" + return p + }(), + expectedError: `^test-path\.workspace\.defaultDatastore: Required value: must specify the default datastore$`, + }, + { + name: "missing folder", + platform: func() *vsphere.Platform { + p := validPlatform() + p.Workspace.Folder = "" + return p + }(), + expectedError: `^test-path\.workspace\.folder: Required value: must specify the VM folder$`, + }, + { + name: "missing SCSI controller type", + platform: func() *vsphere.Platform { + p := validPlatform() + p.SCSIControllerType = "" + return p + }(), + expectedError: `^test-path\.scsiControllerType: Required value: must specify the SCSI controller type$`, + }, + { + name: "missing public network", + platform: func() *vsphere.Platform { + p := validPlatform() + p.PublicNetwork = "" + return p + }(), + expectedError: `^test-path\.publicNetwork: Required value: must specify the public VM network$`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { err := ValidatePlatform(tc.platform, field.NewPath("test-path")).ToAggregate() - if tc.valid { + if tc.expectedError == "" { assert.NoError(t, err) } else { - assert.Error(t, err) + assert.Regexp(t, tc.expectedError, err) } }) } diff --git a/upi/vsphere/machine/main.tf b/upi/vsphere/machine/main.tf index b1d4b89bacb..3b5639c94ca 100644 --- a/upi/vsphere/machine/main.tf +++ b/upi/vsphere/machine/main.tf @@ -60,6 +60,7 @@ resource "vsphere_virtual_machine" "vm" { memory = "8192" guest_id = "other26xLinux64Guest" folder = "${var.folder}" + enable_disk_uuid = "true" wait_for_guest_net_timeout = 0 wait_for_guest_net_routable = false