diff --git a/pkg/asset/cluster/cluster.go b/pkg/asset/cluster/cluster.go index 2222db57cde..c379a50a11e 100644 --- a/pkg/asset/cluster/cluster.go +++ b/pkg/asset/cluster/cluster.go @@ -1,7 +1,6 @@ package cluster import ( - "encoding/json" "io/ioutil" "os" "path/filepath" @@ -12,7 +11,6 @@ import ( "github.com/openshift/installer/data" "github.com/openshift/installer/pkg/asset" "github.com/openshift/installer/pkg/terraform" - "github.com/openshift/installer/pkg/types/config" ) const ( @@ -59,12 +57,7 @@ func (c *Cluster) Generate(parents map[asset.Asset]*asset.State) (*asset.State, return nil, errors.Wrap(err, "failed to write terraform.tfvars file") } - var tfvars config.Cluster - if err := json.Unmarshal(state.Contents[0].Data, &tfvars); err != nil { - return nil, errors.Wrap(err, "failed to Unmarshal terraform.tfvars file") - } - - platform := string(tfvars.Platform) + platform := string(state.Contents[1].Data) if err := data.Unpack(tmpDir, platform); err != nil { return nil, err } diff --git a/pkg/asset/cluster/tfvars.go b/pkg/asset/cluster/tfvars.go index 5baf6a5582e..2726eeaafc6 100644 --- a/pkg/asset/cluster/tfvars.go +++ b/pkg/asset/cluster/tfvars.go @@ -3,7 +3,7 @@ package cluster import ( "github.com/openshift/installer/pkg/asset" "github.com/openshift/installer/pkg/asset/installconfig" - "github.com/openshift/installer/pkg/types/config" + "github.com/openshift/installer/pkg/tfvars" "github.com/pkg/errors" ) @@ -58,18 +58,7 @@ func (t *TerraformVariables) Generate(parents map[asset.Asset]*asset.State) (*as } } - cluster, err := config.ConvertInstallConfigToTFVars(installCfg, contents[t.bootstrapIgnition][0], contents[t.masterIgnition], contents[t.workerIgnition][0]) - if err != nil { - return nil, err - } - - if cluster.Platform == config.PlatformLibvirt { - if err := cluster.Libvirt.UseCachedImage(); err != nil { - return nil, err - } - } - - data, err := cluster.TFVars() + data, err := tfvars.TFVars(installCfg, contents[t.bootstrapIgnition][0], contents[t.masterIgnition], contents[t.workerIgnition][0]) if err != nil { return nil, errors.Wrap(err, "failed to get Tfvars") } @@ -80,6 +69,10 @@ func (t *TerraformVariables) Generate(parents map[asset.Asset]*asset.State) (*as Name: tfvarsFilename, Data: []byte(data), }, + { + Name: "platform", + Data: []byte(installCfg.Platform.Name()), + }, }, }, nil } diff --git a/pkg/asset/ignition/bootstrap/bootstrap.go b/pkg/asset/ignition/bootstrap/bootstrap.go index 1ce9f5c8718..678f44056d9 100644 --- a/pkg/asset/ignition/bootstrap/bootstrap.go +++ b/pkg/asset/ignition/bootstrap/bootstrap.go @@ -191,7 +191,7 @@ func (a *bootstrap) getTemplateData(installConfig *types.InstallConfig) (*bootst } etcdEndpoints := make([]string, installConfig.MasterCount()) for i := range etcdEndpoints { - etcdEndpoints[i] = fmt.Sprintf("https://%s-etcd-%d.%s:2379", installConfig.Name, i, installConfig.BaseDomain) + etcdEndpoints[i] = fmt.Sprintf("https://%s-etcd-%d.%s:2379", installConfig.ObjectMeta.Name, i, installConfig.BaseDomain) } releaseImage := defaultReleaseImage diff --git a/pkg/asset/ignition/machine/node.go b/pkg/asset/ignition/machine/node.go index eacbf82903b..2f9798bd08a 100644 --- a/pkg/asset/ignition/machine/node.go +++ b/pkg/asset/ignition/machine/node.go @@ -22,7 +22,7 @@ func pointerIgnitionConfig(installConfig *types.InstallConfig, rootCA []byte, ro Source: func() *url.URL { return &url.URL{ Scheme: "https", - Host: fmt.Sprintf("%s-api.%s:49500", installConfig.Name, installConfig.BaseDomain), + Host: fmt.Sprintf("%s-api.%s:49500", installConfig.ObjectMeta.Name, installConfig.BaseDomain), Path: fmt.Sprintf("/config/%s", role), RawQuery: query, } diff --git a/pkg/asset/installconfig/stock.go b/pkg/asset/installconfig/stock.go index 9f54450a467..d7640984f69 100644 --- a/pkg/asset/installconfig/stock.go +++ b/pkg/asset/installconfig/stock.go @@ -4,7 +4,7 @@ import ( survey "gopkg.in/AlecAivazis/survey.v1" "github.com/openshift/installer/pkg/asset" - "github.com/openshift/installer/pkg/types/config" + "github.com/openshift/installer/pkg/validate" ) // Stock is the stock of InstallConfig assets that can be generated. @@ -58,7 +58,7 @@ func (s *StockImpl) EstablishStock() { Help: "The email address of the cluster administrator. This will be used to log in to the console.", }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - return config.ValidateEmail(ans.(string)) + return validate.Email(ans.(string)) }), }, EnvVarName: "OPENSHIFT_INSTALL_EMAIL_ADDRESS", @@ -81,7 +81,7 @@ func (s *StockImpl) EstablishStock() { Help: "The base domain of the cluster. All DNS records will be sub-domains of this base.\n\nFor AWS, this must be a previously-existing public Route 53 zone. You can check for any already in your account with:\n\n $ aws route53 list-hosted-zones --query 'HostedZones[? !(Config.PrivateZone)].Name' --output text", }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - return config.ValidateDomainName(ans.(string)) + return validate.DomainName(ans.(string)) }), }, EnvVarName: "OPENSHIFT_INSTALL_BASE_DOMAIN", @@ -94,7 +94,7 @@ func (s *StockImpl) EstablishStock() { Help: "The name of the cluster. This will be used when generating sub-domains.", }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - return config.ValidateDomainName(ans.(string)) + return validate.ClusterName(ans.(string)) }), }, EnvVarName: "OPENSHIFT_INSTALL_CLUSTER_NAME", @@ -107,7 +107,7 @@ func (s *StockImpl) EstablishStock() { Help: "The container registry pull secret for this cluster.", }, Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error { - return config.ValidateJSON([]byte(ans.(string))) + return validate.JSON([]byte(ans.(string))) }), }, EnvVarName: "OPENSHIFT_INSTALL_PULL_SECRET", diff --git a/pkg/asset/kubeconfig/kubeconfig.go b/pkg/asset/kubeconfig/kubeconfig.go index 271fbcfa5c9..8237b9b80f2 100644 --- a/pkg/asset/kubeconfig/kubeconfig.go +++ b/pkg/asset/kubeconfig/kubeconfig.go @@ -50,9 +50,9 @@ func (k *Kubeconfig) Generate(parents map[asset.Asset]*asset.State) (*asset.Stat kubeconfig := clientcmd.Config{ Clusters: []clientcmd.NamedCluster{ { - Name: installConfig.Name, + Name: installConfig.ObjectMeta.Name, Cluster: clientcmd.Cluster{ - Server: fmt.Sprintf("https://%s-api.%s:6443", installConfig.Name, installConfig.BaseDomain), + Server: fmt.Sprintf("https://%s-api.%s:6443", installConfig.ObjectMeta.Name, installConfig.BaseDomain), CertificateAuthorityData: parents[k.rootCA].Contents[tls.CertIndex].Data, }, }, @@ -70,7 +70,7 @@ func (k *Kubeconfig) Generate(parents map[asset.Asset]*asset.State) (*asset.Stat { Name: k.userName, Context: clientcmd.Context{ - Cluster: installConfig.Name, + Cluster: installConfig.ObjectMeta.Name, AuthInfo: k.userName, }, }, diff --git a/pkg/asset/manifests/kube-addon-operator.go b/pkg/asset/manifests/kube-addon-operator.go index 684d421f209..9b593236cf5 100644 --- a/pkg/asset/manifests/kube-addon-operator.go +++ b/pkg/asset/manifests/kube-addon-operator.go @@ -74,5 +74,5 @@ func (kao *kubeAddonOperator) addonConfig() ([]byte, error) { } func (kao *kubeAddonOperator) getAPIServerURL() string { - return fmt.Sprintf("https://%s-api.%s:6443", kao.installConfig.Name, kao.installConfig.BaseDomain) + return fmt.Sprintf("https://%s-api.%s:6443", kao.installConfig.ObjectMeta.Name, kao.installConfig.BaseDomain) } diff --git a/pkg/asset/manifests/kube-core-operator.go b/pkg/asset/manifests/kube-core-operator.go index 006cf38f32a..2b7e5726a4e 100644 --- a/pkg/asset/manifests/kube-core-operator.go +++ b/pkg/asset/manifests/kube-core-operator.go @@ -100,23 +100,23 @@ func (kco *kubeCoreOperator) coreConfig() ([]byte, error) { } func (kco *kubeCoreOperator) getAPIServerURL() string { - return fmt.Sprintf("https://%s-api.%s:6443", kco.installConfig.Name, kco.installConfig.BaseDomain) + return fmt.Sprintf("https://%s-api.%s:6443", kco.installConfig.ObjectMeta.Name, kco.installConfig.BaseDomain) } func (kco *kubeCoreOperator) getEtcdServersURLs() []string { var urls []string for i := 0; i < kco.installConfig.MasterCount(); i++ { - urls = append(urls, fmt.Sprintf("https://%s-etcd-%d.%s:2379", kco.installConfig.Name, i, kco.installConfig.BaseDomain)) + urls = append(urls, fmt.Sprintf("https://%s-etcd-%d.%s:2379", kco.installConfig.ObjectMeta.Name, i, kco.installConfig.BaseDomain)) } return urls } func (kco *kubeCoreOperator) getOicdIssuerURL() string { - return fmt.Sprintf("https://%s.%s/identity", kco.installConfig.Name, kco.installConfig.BaseDomain) + return fmt.Sprintf("https://%s.%s/identity", kco.installConfig.ObjectMeta.Name, kco.installConfig.BaseDomain) } func (kco *kubeCoreOperator) getBaseAddress() string { - return fmt.Sprintf("%s.%s", kco.installConfig.Name, kco.installConfig.BaseDomain) + return fmt.Sprintf("%s.%s", kco.installConfig.ObjectMeta.Name, kco.installConfig.BaseDomain) } // Converts a platform to the cloudProvider that k8s understands diff --git a/pkg/asset/manifests/machine-api-operator.go b/pkg/asset/manifests/machine-api-operator.go index f839caef9ca..c17628af189 100644 --- a/pkg/asset/manifests/machine-api-operator.go +++ b/pkg/asset/manifests/machine-api-operator.go @@ -124,7 +124,7 @@ func (mao *machineAPIOperator) maoConfig(dependencies map[asset.Asset]*asset.Sta } cfg.AWS = &awsConfig{ - ClusterName: mao.installConfig.Name, + ClusterName: mao.installConfig.ObjectMeta.Name, ClusterID: mao.installConfig.ClusterID, Region: mao.installConfig.Platform.AWS.Region, AvailabilityZone: "", @@ -133,7 +133,7 @@ func (mao *machineAPIOperator) maoConfig(dependencies map[asset.Asset]*asset.Sta } } else if mao.installConfig.Platform.Libvirt != nil { cfg.Libvirt = &libvirtConfig{ - ClusterName: mao.installConfig.Name, + ClusterName: mao.installConfig.ObjectMeta.Name, URI: mao.installConfig.Platform.Libvirt.URI, NetworkName: mao.installConfig.Platform.Libvirt.Network.Name, IPRange: mao.installConfig.Platform.Libvirt.Network.IPRange, @@ -141,7 +141,7 @@ func (mao *machineAPIOperator) maoConfig(dependencies map[asset.Asset]*asset.Sta } } else if mao.installConfig.Platform.OpenStack != nil { cfg.OpenStack = &openstackConfig{ - ClusterName: mao.installConfig.Name, + ClusterName: mao.installConfig.ObjectMeta.Name, ClusterID: mao.installConfig.ClusterID, Region: mao.installConfig.Platform.OpenStack.Region, Replicas: int(*mao.installConfig.Machines[1].Replicas), diff --git a/pkg/asset/metadata/metadata.go b/pkg/asset/metadata/metadata.go index 115d63c24ab..af44fe38dcf 100644 --- a/pkg/asset/metadata/metadata.go +++ b/pkg/asset/metadata/metadata.go @@ -43,7 +43,7 @@ func (m *Metadata) Generate(parents map[asset.Asset]*asset.State) (*asset.State, } cm := &types.ClusterMetadata{ - ClusterName: installCfg.Name, + ClusterName: installCfg.ObjectMeta.Name, } switch { case installCfg.Platform.AWS != nil: diff --git a/pkg/asset/tls/helper.go b/pkg/asset/tls/helper.go index 5dc4be64458..40d31bf69ad 100644 --- a/pkg/asset/tls/helper.go +++ b/pkg/asset/tls/helper.go @@ -19,7 +19,7 @@ func assetFilePath(filename string) string { } func getBaseAddress(cfg *types.InstallConfig) string { - return fmt.Sprintf("%s.%s", cfg.Name, cfg.BaseDomain) + return fmt.Sprintf("%s.%s", cfg.ObjectMeta.Name, cfg.BaseDomain) } func cidrhost(network net.IPNet, hostNum int) (string, error) { @@ -45,7 +45,7 @@ func genDNSNamesForIngressCertKey(cfg *types.InstallConfig) ([]string, error) { func genDNSNamesForAPIServerCertKey(cfg *types.InstallConfig) ([]string, error) { return []string{ - fmt.Sprintf("%s-api.%s", cfg.Name, cfg.BaseDomain), + fmt.Sprintf("%s-api.%s", cfg.ObjectMeta.Name, cfg.BaseDomain), "kubernetes", "kubernetes.default", "kubernetes.default.svc", "kubernetes.default.svc.cluster.local", @@ -63,7 +63,7 @@ func genIPAddressesForAPIServerCertKey(cfg *types.InstallConfig) ([]net.IP, erro func genDNSNamesForOpenshiftAPIServerCertKey(cfg *types.InstallConfig) ([]string, error) { return []string{ - fmt.Sprintf("%s-api.%s", cfg.Name, cfg.BaseDomain), + fmt.Sprintf("%s-api.%s", cfg.ObjectMeta.Name, cfg.BaseDomain), "openshift-apiserver", "openshift-apiserver.kube-system", "openshift-apiserver.kube-system.svc", @@ -81,9 +81,9 @@ func genIPAddressesForOpenshiftAPIServerCertKey(cfg *types.InstallConfig) ([]net } func genDNSNamesForMCSCertKey(cfg *types.InstallConfig) ([]string, error) { - return []string{fmt.Sprintf("%s-api.%s", cfg.Name, cfg.BaseDomain)}, nil + return []string{fmt.Sprintf("%s-api.%s", cfg.ObjectMeta.Name, cfg.BaseDomain)}, nil } func genSubjectForMCSCertKey(cfg *types.InstallConfig) (pkix.Name, error) { - return pkix.Name{CommonName: fmt.Sprintf("%s-api.%s", cfg.Name, cfg.BaseDomain)}, nil + return pkix.Name{CommonName: fmt.Sprintf("%s-api.%s", cfg.ObjectMeta.Name, cfg.BaseDomain)}, nil } diff --git a/pkg/types/config/aws/aws.go b/pkg/tfvars/aws/aws.go similarity index 100% rename from pkg/types/config/aws/aws.go rename to pkg/tfvars/aws/aws.go diff --git a/pkg/types/config/libvirt/cache.go b/pkg/tfvars/libvirt/cache.go similarity index 100% rename from pkg/types/config/libvirt/cache.go rename to pkg/tfvars/libvirt/cache.go diff --git a/pkg/types/config/libvirt/libvirt.go b/pkg/tfvars/libvirt/libvirt.go similarity index 100% rename from pkg/types/config/libvirt/libvirt.go rename to pkg/tfvars/libvirt/libvirt.go diff --git a/pkg/types/config/openstack/openstack.go b/pkg/tfvars/openstack/openstack.go similarity index 100% rename from pkg/types/config/openstack/openstack.go rename to pkg/tfvars/openstack/openstack.go diff --git a/pkg/tfvars/tfvars.go b/pkg/tfvars/tfvars.go new file mode 100644 index 00000000000..d5f5543e773 --- /dev/null +++ b/pkg/tfvars/tfvars.go @@ -0,0 +1,136 @@ +// Package tfvars converts an InstallConfig to Terraform variables. +package tfvars + +import ( + "context" + "encoding/json" + "time" + + "github.com/openshift/installer/pkg/rhcos" + "github.com/openshift/installer/pkg/tfvars/aws" + "github.com/openshift/installer/pkg/tfvars/libvirt" + "github.com/openshift/installer/pkg/tfvars/openstack" + "github.com/openshift/installer/pkg/types" + "github.com/pkg/errors" +) + +type config struct { + ClusterID string `json:"tectonic_cluster_id,omitempty"` + Name string `json:"tectonic_cluster_name,omitempty"` + BaseDomain string `json:"tectonic_base_domain,omitempty"` + Masters int `json:"tectonic_master_count,omitempty"` + Workers int `json:"tectonic_worker_count,omitempty"` + + IgnitionBootstrap string `json:"ignition_bootstrap,omitempty"` + IgnitionMasters []string `json:"ignition_masters,omitempty"` + IgnitionWorker string `json:"ignition_worker,omitempty"` + + aws.AWS `json:",inline"` + libvirt.Libvirt `json:",inline"` + openstack.OpenStack `json:",inline"` +} + +// TFVars converts the InstallConfig and Ignition content to +// terraform.tfvar JSON. +func TFVars(cfg *types.InstallConfig, bootstrapIgn string, masterIgns []string, workerIgn string) ([]byte, error) { + config := &config{ + ClusterID: cfg.ClusterID, + Name: cfg.ObjectMeta.Name, + BaseDomain: cfg.BaseDomain, + + IgnitionMasters: masterIgns, + IgnitionWorker: workerIgn, + IgnitionBootstrap: bootstrapIgn, + } + + for _, m := range cfg.Machines { + var replicas int + if m.Replicas == nil { + replicas = 1 + } else { + replicas = int(*m.Replicas) + } + + switch m.Name { + case "master": + config.Masters += replicas + if m.Platform.AWS != nil { + config.AWS.Master = aws.Master{ + EC2Type: m.Platform.AWS.InstanceType, + IAMRoleName: m.Platform.AWS.IAMRoleName, + MasterRootVolume: aws.MasterRootVolume{ + IOPS: m.Platform.AWS.EC2RootVolume.IOPS, + Size: m.Platform.AWS.EC2RootVolume.Size, + Type: m.Platform.AWS.EC2RootVolume.Type, + }, + } + } + case "worker": + config.Workers += replicas + if m.Platform.AWS != nil { + config.AWS.Worker = aws.Worker{ + EC2Type: m.Platform.AWS.InstanceType, + IAMRoleName: m.Platform.AWS.IAMRoleName, + WorkerRootVolume: aws.WorkerRootVolume{ + IOPS: m.Platform.AWS.EC2RootVolume.IOPS, + Size: m.Platform.AWS.EC2RootVolume.Size, + Type: m.Platform.AWS.EC2RootVolume.Type, + }, + } + } + default: + return nil, errors.Errorf("unrecognized machine pool %q", m.Name) + } + } + + if cfg.Platform.AWS != nil { + ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) + defer cancel() + ami, err := rhcos.AMI(ctx, rhcos.DefaultChannel, cfg.Platform.AWS.Region) + if err != nil { + return nil, errors.Wrap(err, "failed to determine default AMI") + } + + config.AWS = aws.AWS{ + Endpoints: aws.EndpointsAll, // Default value for endpoints. + Profile: aws.DefaultProfile, // Default value for profile. + Region: cfg.Platform.AWS.Region, + ExtraTags: cfg.Platform.AWS.UserTags, + External: aws.External{ + VPCID: cfg.Platform.AWS.VPCID, + }, + VPCCIDRBlock: cfg.Platform.AWS.VPCCIDRBlock, + EC2AMIOverride: ami, + } + } else if cfg.Platform.Libvirt != nil { + masterIPs := make([]string, len(cfg.Platform.Libvirt.MasterIPs)) + for i, ip := range cfg.Platform.Libvirt.MasterIPs { + masterIPs[i] = ip.String() + } + config.Libvirt = libvirt.Libvirt{ + URI: cfg.Platform.Libvirt.URI, + Network: libvirt.Network{ + Name: cfg.Platform.Libvirt.Network.Name, + IfName: cfg.Platform.Libvirt.Network.IfName, + IPRange: cfg.Platform.Libvirt.Network.IPRange, + }, + Image: cfg.Platform.Libvirt.DefaultMachinePlatform.Image, + MasterIPs: masterIPs, + } + if err := config.Libvirt.TFVars(config.Masters, config.Workers); err != nil { + return nil, errors.Wrap(err, "failed to insert libvirt variables") + } + if err := config.Libvirt.UseCachedImage(); err != nil { + return nil, errors.Wrap(err, "failed to use cached libvirt image") + } + } else if cfg.Platform.OpenStack != nil { + config.OpenStack = openstack.OpenStack{ + Region: cfg.Platform.OpenStack.Region, + NetworkCIDRBlock: cfg.Platform.OpenStack.NetworkCIDRBlock, + } + config.OpenStack.Credentials.Cloud = cfg.Platform.OpenStack.Cloud + config.OpenStack.ExternalNetwork = cfg.Platform.OpenStack.ExternalNetwork + } + + return json.MarshalIndent(config, "", " ") +} diff --git a/pkg/types/config/cluster.go b/pkg/types/config/cluster.go deleted file mode 100644 index 70c42d50dd7..00000000000 --- a/pkg/types/config/cluster.go +++ /dev/null @@ -1,282 +0,0 @@ -package config - -import ( - "context" - "encoding/json" - "time" - - "github.com/coreos/tectonic-config/config/tectonic-network" - "github.com/pkg/errors" - "gopkg.in/yaml.v2" - - "github.com/openshift/installer/pkg/rhcos" - "github.com/openshift/installer/pkg/types" - "github.com/openshift/installer/pkg/types/config/aws" - "github.com/openshift/installer/pkg/types/config/libvirt" - "github.com/openshift/installer/pkg/types/config/openstack" -) - -const ( - // PlatformAWS is the platform for a cluster launched on AWS. - PlatformAWS Platform = "aws" - // PlatformLibvirt is the platform for a cluster launched on libvirt. - PlatformLibvirt Platform = "libvirt" - // PlatformOpenStack is the platform for a cluster launched on OpenStack. - PlatformOpenStack Platform = "openstack" -) - -// Platform indicates the target platform of the cluster. -type Platform string - -// UnmarshalYAML unmarshals and verifies the platform. -func (p *Platform) UnmarshalYAML(unmarshal func(interface{}) error) error { - var data string - if err := unmarshal(&data); err != nil { - return err - } - - platform := Platform(data) - switch platform { - case PlatformAWS, PlatformLibvirt, PlatformOpenStack: - default: - return errors.Errorf("invalid platform specified (%s); must be one of %s", platform, []Platform{PlatformAWS, PlatformLibvirt, PlatformOpenStack}) - } - - *p = platform - return nil -} - -var defaultCluster = Cluster{ - AWS: aws.AWS{ - Endpoints: aws.EndpointsAll, - Profile: aws.DefaultProfile, - Region: aws.DefaultRegion, - VPCCIDRBlock: aws.DefaultVPCCIDRBlock, - }, - Libvirt: libvirt.Libvirt{ - Network: libvirt.Network{ - IfName: libvirt.DefaultIfName, - }, - }, - Networking: Networking{ - MTU: "1480", - PodCIDR: "10.2.0.0/16", - ServiceCIDR: "10.3.0.0/16", - Type: tectonicnetwork.NetworkFlannel, - }, - OpenStack: openstack.OpenStack{ - Region: openstack.DefaultRegion, - NetworkCIDRBlock: openstack.DefaultNetworkCIDRBlock, - }, -} - -// Cluster defines the config for a cluster. -type Cluster struct { - Admin `json:",inline" yaml:"admin,omitempty"` - aws.AWS `json:",inline" yaml:"aws,omitempty"` - BaseDomain string `json:"tectonic_base_domain,omitempty" yaml:"baseDomain,omitempty"` - - IgnitionBootstrap string `json:"ignition_bootstrap,omitempty" yaml:"-"` - IgnitionMasters []string `json:"ignition_masters,omitempty" yaml:"-"` - IgnitionWorker string `json:"ignition_worker,omitempty" yaml:"-"` - - Internal `json:",inline" yaml:"-"` - libvirt.Libvirt `json:",inline" yaml:"libvirt,omitempty"` - Master `json:",inline" yaml:"master,omitempty"` - Name string `json:"tectonic_cluster_name,omitempty" yaml:"name,omitempty"` - Networking `json:",inline" yaml:"networking,omitempty"` - NodePools `json:"-" yaml:"nodePools"` - openstack.OpenStack `json:",inline" yaml:"openstack,omitempty"` - Platform Platform `json:"tectonic_platform" yaml:"platform,omitempty"` - PullSecret string `json:"tectonic_pull_secret,omitempty" yaml:"pullSecret,omitempty"` - PullSecretPath string `json:"-" yaml:"pullSecretPath,omitempty"` // Deprecated: remove after openshift/release is ported to pullSecret - Worker `json:",inline" yaml:"worker,omitempty"` -} - -// NodeCount will return the number of nodes specified in NodePools with matching names. -// If no matching NodePools are found, then 0 is returned. -func (c Cluster) NodeCount(names []string) int { - var count int - for _, name := range names { - for _, n := range c.NodePools { - if n.Name == name { - count += n.Count - break - } - } - } - return count -} - -// TFVars will return the config for the cluster in tfvars format. -func (c *Cluster) TFVars() (string, error) { - c.Master.Count = c.NodeCount(c.Master.NodePools) - c.Worker.Count = c.NodeCount(c.Worker.NodePools) - - // Fill in master ips - if c.Platform == PlatformLibvirt { - if err := c.Libvirt.TFVars(c.Master.Count, c.Worker.Count); err != nil { - return "", errors.Wrap(err, "failed to create TFVars for libvirt platfrom") - } - } - - data, err := json.MarshalIndent(&c, "", " ") - if err != nil { - return "", errors.Wrap(err, "failed to marshal TFVars") - } - - return string(data), nil -} - -// YAML will return the config for the cluster in yaml format. -func (c *Cluster) YAML() (string, error) { - c.NodePools = append(c.NodePools, NodePool{ - Count: c.Master.Count, - Name: "master", - }) - c.Master.NodePools = []string{"master"} - - c.NodePools = append(c.NodePools, NodePool{ - Count: c.Worker.Count, - Name: "worker", - }) - c.Worker.NodePools = []string{"worker"} - - yaml, err := yaml.Marshal(c) - if err != nil { - return "", errors.Wrap(err, "failed to marshal config.Cluster") - } - - return string(yaml), nil -} - -// ConvertInstallConfigToTFVars converts the installconfig to the Cluster struct -// that represents the terraform.tfvar file. -// TODO(yifan): Clean up the Cluster struct to trim unnecessary fields. -func ConvertInstallConfigToTFVars(cfg *types.InstallConfig, bootstrapIgn string, masterIgns []string, workerIgn string) (*Cluster, error) { - cluster := &Cluster{ - Admin: Admin{ - Email: cfg.Admin.Email, - Password: cfg.Admin.Password, - SSHKey: cfg.Admin.SSHKey, - }, - - IgnitionMasters: masterIgns, - IgnitionWorker: workerIgn, - IgnitionBootstrap: bootstrapIgn, - - Internal: Internal{ - ClusterID: cfg.ClusterID, - }, - - Networking: Networking{ - Type: tectonicnetwork.NetworkType(cfg.Networking.Type), - ServiceCIDR: cfg.Networking.ServiceCIDR.String(), - PodCIDR: cfg.Networking.PodCIDR.String(), - // TODO(yifan): Remove this when we drop the old installer binary. - MTU: "1480", - }, - BaseDomain: cfg.BaseDomain, - Name: cfg.Name, - PullSecret: cfg.PullSecret, - } - - if cfg.Platform.AWS != nil { - ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) - defer cancel() - ami, err := rhcos.AMI(ctx, rhcos.DefaultChannel, cfg.Platform.AWS.Region) - if err != nil { - return nil, errors.Wrap(err, "failed to determine default AMI") - } - - cluster.Platform = PlatformAWS - cluster.AWS = aws.AWS{ - Endpoints: aws.EndpointsAll, // Default value for endpoints. - Profile: aws.DefaultProfile, // Default value for profile. - Region: cfg.Platform.AWS.Region, - ExtraTags: cfg.Platform.AWS.UserTags, - External: aws.External{ - VPCID: cfg.Platform.AWS.VPCID, - }, - VPCCIDRBlock: cfg.Platform.AWS.VPCCIDRBlock, - EC2AMIOverride: ami, - } - } else if cfg.Platform.Libvirt != nil { - cluster.Platform = PlatformLibvirt - masterIPs := make([]string, len(cfg.Platform.Libvirt.MasterIPs)) - for i, ip := range cfg.Platform.Libvirt.MasterIPs { - masterIPs[i] = ip.String() - } - cluster.Libvirt = libvirt.Libvirt{ - URI: cfg.Platform.Libvirt.URI, - Network: libvirt.Network{ - Name: cfg.Platform.Libvirt.Network.Name, - IfName: cfg.Platform.Libvirt.Network.IfName, - IPRange: cfg.Platform.Libvirt.Network.IPRange, - }, - Image: cfg.Platform.Libvirt.DefaultMachinePlatform.Image, - MasterIPs: masterIPs, - } - } else if cfg.Platform.OpenStack != nil { - cluster.Platform = PlatformOpenStack - cluster.OpenStack = openstack.OpenStack{ - Region: cfg.Platform.OpenStack.Region, - NetworkCIDRBlock: cfg.Platform.OpenStack.NetworkCIDRBlock, - } - cluster.OpenStack.Credentials.Cloud = cfg.Platform.OpenStack.Cloud - cluster.OpenStack.ExternalNetwork = cfg.Platform.OpenStack.ExternalNetwork - } - - for _, m := range cfg.Machines { - nodePool := NodePool{ - Name: m.Name, - } - if m.Replicas == nil { - nodePool.Count = 1 - } else { - nodePool.Count = int(*m.Replicas) - } - cluster.NodePools = append(cluster.NodePools, nodePool) - - switch m.Name { - case "master": - cluster.Master.Count += nodePool.Count - cluster.Master.NodePools = append(cluster.Master.NodePools, m.Name) - if m.Platform.AWS != nil { - cluster.AWS.Master = aws.Master{ - EC2Type: m.Platform.AWS.InstanceType, - IAMRoleName: m.Platform.AWS.IAMRoleName, - MasterRootVolume: aws.MasterRootVolume{ - IOPS: m.Platform.AWS.EC2RootVolume.IOPS, - Size: m.Platform.AWS.EC2RootVolume.Size, - Type: m.Platform.AWS.EC2RootVolume.Type, - }, - } - } - case "worker": - cluster.Worker.Count += nodePool.Count - cluster.Worker.NodePools = append(cluster.Worker.NodePools, m.Name) - if m.Platform.AWS != nil { - cluster.AWS.Worker = aws.Worker{ - EC2Type: m.Platform.AWS.InstanceType, - IAMRoleName: m.Platform.AWS.IAMRoleName, - WorkerRootVolume: aws.WorkerRootVolume{ - IOPS: m.Platform.AWS.EC2RootVolume.IOPS, - Size: m.Platform.AWS.EC2RootVolume.Size, - Type: m.Platform.AWS.EC2RootVolume.Type, - }, - } - } - default: - return nil, errors.Errorf("unrecognized machine pool %q", m.Name) - } - - } - - // Validate the TFVars. - if err := cluster.ValidateAndLog(); err != nil { - return nil, errors.Wrap(err, "failed to validate TFVars") - } - - return cluster, nil -} diff --git a/pkg/types/config/fixtures/ign.ign b/pkg/types/config/fixtures/ign.ign deleted file mode 100644 index caf0a6cbd54..00000000000 --- a/pkg/types/config/fixtures/ign.ign +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ignition": { "version": "2.2.0" }, - "systemd": { - "units": [{ - "name": "example.service", - "contents": "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target" - }] - } -} diff --git a/pkg/types/config/fixtures/invalid-ign.ign b/pkg/types/config/fixtures/invalid-ign.ign deleted file mode 100644 index 1cf9ba4acad..00000000000 --- a/pkg/types/config/fixtures/invalid-ign.ign +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ignition": { "version": "2.2.0" }, - "systemd": { - "units": [{ - "name": "example.service", - "contents": "[Service]\nType=oneshot\nExecStart=/usr/bin/echo Hello World\n\n[Install]\nWantedBy=multi-user.target" - }] - } - "invalid": "" -} diff --git a/pkg/types/config/types.go b/pkg/types/config/types.go deleted file mode 100644 index 70f64d19f89..00000000000 --- a/pkg/types/config/types.go +++ /dev/null @@ -1,53 +0,0 @@ -package config - -import "github.com/coreos/tectonic-config/config/tectonic-network" - -// Admin converts admin related config. -type Admin struct { - Email string `json:"tectonic_admin_email" yaml:"email,omitempty"` - Password string `json:"tectonic_admin_password" yaml:"password,omitempty"` - SSHKey string `json:"tectonic_admin_ssh_key,omitempty" yaml:"sshKey,omitempty"` -} - -// NodePool converts node pool related config. -type NodePool struct { - Count int `json:"-" yaml:"count"` - Name string `json:"-" yaml:"name"` -} - -// NodePools converts node pools related config. -type NodePools []NodePool - -// Map returns a nodePools' map equivalent. -func (n NodePools) Map() map[string]int { - m := make(map[string]int) - for i := range n { - m[n[i].Name] = n[i].Count - } - return m -} - -// Master converts master related config. -type Master struct { - Count int `json:"tectonic_master_count,omitempty" yaml:"-"` - NodePools []string `json:"-" yaml:"nodePools"` -} - -// Networking converts networking related config. -type Networking struct { - Type tectonicnetwork.NetworkType `json:"tectonic_networking,omitempty" yaml:"type,omitempty"` - MTU string `json:"-" yaml:"mtu,omitempty"` - ServiceCIDR string `json:"tectonic_service_cidr,omitempty" yaml:"serviceCIDR,omitempty"` - PodCIDR string `json:"tectonic_cluster_cidr,omitempty" yaml:"podCIDR,omitempty"` -} - -// Worker converts worker related config. -type Worker struct { - Count int `json:"tectonic_worker_count,omitempty" yaml:"-"` - NodePools []string `json:"-" yaml:"nodePools"` -} - -// Internal converts internal related config. -type Internal struct { - ClusterID string `json:"tectonic_cluster_id,omitempty" yaml:"clusterId"` -} diff --git a/pkg/types/config/validate.go b/pkg/types/config/validate.go deleted file mode 100644 index d2b81ad76bd..00000000000 --- a/pkg/types/config/validate.go +++ /dev/null @@ -1,573 +0,0 @@ -package config - -import ( - "encoding/json" - "errors" - "fmt" - "net" - "regexp" - "strconv" - "strings" - "unicode/utf8" - - "github.com/openshift/installer/pkg/types/config/aws" - - "github.com/coreos/tectonic-config/config/tectonic-network" - "github.com/sirupsen/logrus" -) - -const ( - maxS3BucketNameLength = 63 -) - -// ErrUnmatchedNodePool is returned when a nodePool was specified but not found in the nodePools list. -type ErrUnmatchedNodePool struct { - name string -} - -// ErrUnmatchedNodePool implements the error interface. -func (e *ErrUnmatchedNodePool) Error() string { - return fmt.Sprintf("no node pool named %q was found", e.name) -} - -// ErrMissingNodePool is returned when a field that requires a nodePool does not specify one. -type ErrMissingNodePool struct { - field string -} - -// ErrMissingNodePool implements the error interface. -func (e *ErrMissingNodePool) Error() string { - return fmt.Sprintf("the %s field requires at least one node pool to be specified", e.field) -} - -// ErrMoreThanOneNodePool is returned when a field specifies more than one node pool. -type ErrMoreThanOneNodePool struct { - field string -} - -// ErrMoreThanOneNodePool implements the error interface. -func (e *ErrMoreThanOneNodePool) Error() string { - return fmt.Sprintf("the %s field specifies more than one node pool; this is not currently allowed", e.field) -} - -// ErrSharedNodePool is returned when two or more fields are defined to use the same nodePool. -type ErrSharedNodePool struct { - name string - fields []string -} - -// ErrSharedNodePool implements the error interface. -func (e *ErrSharedNodePool) Error() string { - return fmt.Sprintf("node pools cannot be shared, but %q is used by %s", e.name, strings.Join(e.fields, ", ")) -} - -// ErrInvalidIgnConfig is returned when a invalid ign config is given. -type ErrInvalidIgnConfig struct { - filePath string - rpt string -} - -// ErrInvalidIgnConfig implements the error interface. -func (e *ErrInvalidIgnConfig) Error() string { - return fmt.Sprintf("failed to parse ignition file %s: %s", e.filePath, e.rpt) -} - -// Validate ensures that the Cluster is semantically correct and returns an error if not. -func (c *Cluster) Validate() []error { - var errs []error - errs = append(errs, c.validateNodePools()...) - errs = append(errs, c.validateNetworking()...) - errs = append(errs, c.validateAWS()...) - errs = append(errs, c.validateOpenStack()...) - errs = append(errs, c.validatePullSecret()...) - errs = append(errs, c.validateLibvirt()...) - if err := prefixError("cluster name", validateClusterName(c.Name)); err != nil { - errs = append(errs, err) - } - if err := prefixError("base domain", ValidateDomainName(c.BaseDomain)); err != nil { - errs = append(errs, err) - } - if err := prefixError("admin password", validateNonEmpty(c.Admin.Password)); err != nil { - errs = append(errs, err) - } - if err := prefixError("admin email", ValidateEmail(c.Admin.Email)); err != nil { - errs = append(errs, err) - } - return errs -} - -// validateAWS validates all fields specific to AWS. -func (c *Cluster) validateAWS() []error { - var errs []error - if c.Platform != PlatformAWS { - return errs - } - if err := c.validateAWSEndpoints(); err != nil { - errs = append(errs, err) - } - if err := c.validateS3Bucket(); err != nil { - errs = append(errs, err) - } - if err := prefixError("aws vpcCIDRBlock", validateSubnetCIDR(c.AWS.VPCCIDRBlock)); err != nil { - errs = append(errs, err) - } - errs = append(errs, c.validateOverlapWithPodOrServiceCIDR(c.AWS.VPCCIDRBlock, "aws vpcCIDRBlock")...) - if err := prefixError("aws profile", validateNonEmpty(c.AWS.Profile)); err != nil { - errs = append(errs, err) - } - if err := prefixError("aws region", validateNonEmpty(c.AWS.Region)); err != nil { - errs = append(errs, err) - } - return errs -} - -// validateOpenStack validates all fields specific to OpenStack. -func (c *Cluster) validateOpenStack() []error { - var errs []error - if c.Platform != PlatformOpenStack { - return errs - } - return errs -} - -// validateOverlapWithPodOrServiceCIDR ensures that the given CIDR does not -// overlap with the pod or service CIDRs of the cluster config. -func (c *Cluster) validateOverlapWithPodOrServiceCIDR(cidr, name string) []error { - var errs []error - if err := prefixError(fmt.Sprintf("%s and podCIDR", name), validateCIDRsDontOverlap(cidr, c.Networking.PodCIDR)); err != nil { - errs = append(errs, err) - } - if err := prefixError(fmt.Sprintf("%s and serviceCIDR", name), validateCIDRsDontOverlap(cidr, c.Networking.ServiceCIDR)); err != nil { - errs = append(errs, err) - } - return errs -} - -// validateLibvirt validates all fields specific to libvirt. -func (c *Cluster) validateLibvirt() []error { - var errs []error - if c.Platform != PlatformLibvirt { - return errs - } - if err := prefixError("libvirt network ipRange", validateSubnetCIDR(c.Libvirt.Network.IPRange)); err != nil { - errs = append(errs, err) - } - if len(c.Libvirt.MasterIPs) > 0 { - if len(c.Libvirt.MasterIPs) != c.NodeCount(c.Master.NodePools) { - errs = append(errs, fmt.Errorf("length of masterIPs does't match master count")) - } - for i, ip := range c.Libvirt.MasterIPs { - if err := prefixError(fmt.Sprintf("libvirt masterIPs[%d] %q", i, ip), validateIPv4(ip)); err != nil { - errs = append(errs, err) - } - } - } - if err := prefixError("libvirt uri", validateNonEmpty(c.Libvirt.URI)); err != nil { - errs = append(errs, err) - } - if err := prefixError("libvirt network name", validateNonEmpty(c.Libvirt.Network.Name)); err != nil { - errs = append(errs, err) - } - if err := prefixError("libvirt network ifName", validateNonEmpty(c.Libvirt.Network.IfName)); err != nil { - errs = append(errs, err) - } - errs = append(errs, c.validateOverlapWithPodOrServiceCIDR(c.Libvirt.Network.IPRange, "libvirt ipRange")...) - return errs -} - -func (c *Cluster) validateNetworking() []error { - var errs []error - // https://en.wikipedia.org/wiki/Maximum_transmission_unit#MTUs_for_common_media - if err := prefixError("mtu", validateIntRange(c.Networking.MTU, 68, 64*1024)); err != nil { - errs = append(errs, err) - } - if err := prefixError("podCIDR", validateSubnetCIDR(c.Networking.PodCIDR)); err != nil { - errs = append(errs, err) - } - if err := prefixError("serviceCIDR", validateSubnetCIDR(c.Networking.ServiceCIDR)); err != nil { - errs = append(errs, err) - } - if err := c.validateNetworkType(); err != nil { - errs = append(errs, err) - } - if err := prefixError("pod and service CIDRs", validateCIDRsDontOverlap(c.Networking.PodCIDR, c.Networking.ServiceCIDR)); err != nil { - errs = append(errs, err) - } - return errs -} - -func (c *Cluster) validateNetworkType() error { - switch c.Networking.Type { - case tectonicnetwork.NetworkNone: - fallthrough - case tectonicnetwork.NetworkCanal: - fallthrough - case tectonicnetwork.NetworkFlannel: - fallthrough - case tectonicnetwork.NetworkCalicoIPIP: - return nil - default: - return fmt.Errorf("invalid network type %q", c.Networking.Type) - } -} - -// ValidateAndLog performs cluster configuration validation using `Validate` -// but rather than return a slice of errors, it logs any errors and returns -// a single error for convenience. -func (c *Cluster) ValidateAndLog() error { - if errs := c.Validate(); len(errs) != 0 { - logrus.Errorf("Found %d error(s) in the cluster definition:", len(errs)) - for i, err := range errs { - logrus.Errorf(" Error %d: %v", i+1, err) - } - return fmt.Errorf("found %d cluster definition error(s)", len(errs)) - } - return nil -} - -// validateAWSEndpoints ensures that the value of the endpoints field is one of: -// 'all', 'public', or 'private'. -func (c *Cluster) validateAWSEndpoints() error { - switch c.AWS.Endpoints { - case aws.EndpointsAll: - fallthrough - case aws.EndpointsPrivate: - fallthrough - case aws.EndpointsPublic: - return nil - default: - return fmt.Errorf("invalid AWS endpoints %q", c.AWS.Endpoints) - } -} - -// validateS3Bucket does some basic validation to ensure that the S3 bucket -// matches the S3 bucket naming rules. Not all rules are checked -// because Tectonic controls the generation of S3 bucket names, creating -// buckets of the form: . -// If domain-name contains a trailing dot, it's removed from the bucket name. -func (c *Cluster) validateS3Bucket() error { - bucket := fmt.Sprintf("%s.%s", c.Name, strings.TrimRight(c.BaseDomain, ".")) - if len(bucket) > maxS3BucketNameLength { - return fmt.Errorf("the S3 bucket name %q, generated from the cluster name and base domain, is too long; S3 bucket names must be less than 63 characters; please choose a shorter cluster name or base domain", bucket) - } - if !regexp.MustCompile("^[a-z0-9][a-z0-9-.]{1,61}[a-z0-9]$").MatchString(bucket) { - return errors.New("invalid characters in S3 bucket name") - } - return nil -} - -func (c *Cluster) validatePullSecret() []error { - var errs []error - if err := ValidateJSON([]byte(c.PullSecret)); err != nil { - errs = append(errs, prefixError("pull secret", err)) - } - return errs -} - -func (c *Cluster) validateNodePools() []error { - var errs []error - n := c.NodePools.Map() - fields := []struct { - pools []string - field string - }{ - {pools: c.Master.NodePools, field: "master"}, - {pools: c.Worker.NodePools, field: "worker"}, - } - for _, f := range fields { - var found bool - for _, p := range f.pools { - if p == "" { - continue - } - found = true - if _, ok := n[p]; !ok { - errs = append(errs, &ErrUnmatchedNodePool{p}) - } - } - if !found { - errs = append(errs, &ErrMissingNodePool{f.field}) - } - if len(f.pools) > 1 { - errs = append(errs, &ErrMoreThanOneNodePool{f.field}) - } - } - - errs = append(errs, c.validateNoSharedNodePools()...) - - return errs -} - -func (c *Cluster) validateNoSharedNodePools() []error { - var errs []error - fields := make(map[string]map[string]struct{}) - for i := range c.Master.NodePools { - if c.Master.NodePools[i] != "" { - for j := range c.Worker.NodePools { - if c.Master.NodePools[i] == c.Worker.NodePools[j] { - if fields[c.Master.NodePools[i]] == nil { - fields[c.Master.NodePools[i]] = make(map[string]struct{}) - } - fields[c.Master.NodePools[i]]["master"] = struct{}{} - fields[c.Master.NodePools[i]]["worker"] = struct{}{} - } - } - } - } - for k, v := range fields { - err := &ErrSharedNodePool{name: k} - for f := range v { - err.fields = append(err.fields, f) - } - errs = append(errs, err) - } - return errs -} - -// ValidateDomainName checks if the given string is a valid domain name and returns an error if not. -func ValidateDomainName(v string) error { - if err := validateNonEmpty(v); err != nil { - return err - } - - split := strings.Split(v, ".") - for i, segment := range split { - // Trailing dot is OK - if len(segment) == 0 && i == len(split)-1 { - continue - } - if !isMatch("^[a-zA-Z0-9-]{1,63}$", segment) { - return errors.New("invalid domain name") - } - } - return nil -} - -// ValidateEmail checks if the given string is a valid email address and returns an error if not. -func ValidateEmail(v string) error { - if err := validateNonEmpty(v); err != nil { - return err - } - - invalidError := errors.New("invalid email address") - - split := strings.Split(v, "@") - if len(split) != 2 { - return invalidError - } - localPart := split[0] - domain := split[1] - - if validateNonEmpty(localPart) != nil { - return invalidError - } - - // No whitespace allowed in local-part - if isMatch(`\s`, localPart) { - return invalidError - } - - return ValidateDomainName(domain) -} - -// ValidateJSON validates that the given data is valid JSON. -func ValidateJSON(data []byte) error { - var dummy interface{} - return json.Unmarshal(data, &dummy) -} - -// prefixError wraps an error with a prefix or returns nil if there was no error. -// This is useful for wrapping errors returned by generic error funcs like `validateNonEmpty` so that the error includes the offending field name. -func prefixError(prefix string, err error) error { - if err != nil { - return fmt.Errorf("%s: %v", prefix, err) - } - return nil -} - -func isMatch(re string, v string) bool { - return regexp.MustCompile(re).MatchString(v) -} - -// validateClusterName checks if the given string is a valid name for a cluster and returns an error if not. -func validateClusterName(v string) error { - if err := validateNonEmpty(v); err != nil { - return err - } - - if length := utf8.RuneCountInString(v); length < 1 || length > 253 { - return errors.New("must be between 1 and 253 characters") - } - - if strings.ToLower(v) != v { - return errors.New("must be lower case") - } - - if !isMatch("^[a-z0-9-.]*$", v) { - return errors.New("only lower case alphanumeric [a-z0-9], dashes and dots are allowed") - } - - isAlphaNum := regexp.MustCompile("^[a-z0-9]$").MatchString - - // If we got this far, we know the string is ASCII and has at least one character - if !isAlphaNum(v[:1]) || !isAlphaNum(v[len(v)-1:]) { - return errors.New("must start and end with a lower case alphanumeric character [a-z0-9]") - } - - for _, segment := range strings.Split(v, ".") { - // Each segment can have up to 63 characters - if utf8.RuneCountInString(segment) > 63 { - return errors.New("no segment between dots can be more than 63 characters") - } - if !isAlphaNum(segment[:1]) || !isAlphaNum(segment[len(segment)-1:]) { - return errors.New("segments between dots must start and end with a lower case alphanumeric character [a-z0-9]") - } - } - - return nil -} - -// validateNonEmpty checks if the given string contains at least one non-whitespace character and returns an error if not. -func validateNonEmpty(v string) error { - if utf8.RuneCountInString(strings.TrimSpace(v)) == 0 { - return errors.New("cannot be empty") - } - return nil -} - -// validateSubnetCIDR checks if the given string is a valid CIDR for a master nodes or worker nodes subnet and returns an error if not. -func validateSubnetCIDR(v string) error { - if err := validateNonEmpty(v); err != nil { - return err - } - - split := strings.Split(v, "/") - - if len(split) == 1 { - return errors.New("must provide a CIDR netmask (eg, /24)") - } - - if len(split) != 2 { - return errors.New("invalid IPv4 address") - } - - ip := split[0] - - if err := validateIPv4(ip); err != nil { - return errors.New("invalid IPv4 address") - } - - if mask, err := strconv.Atoi(split[1]); err != nil || mask < 0 || mask > 32 { - return errors.New("invalid netmask size (must be between 0 and 32)") - } - - // Catch any invalid CIDRs not caught by the checks above - if _, _, err := net.ParseCIDR(v); err != nil { - return errors.New("invalid CIDR") - } - - if strings.HasPrefix(ip, "172.17.") { - return errors.New("overlaps with default Docker Bridge subnet (172.17.0.0/16)") - } - - return nil -} - -// validateCIDRsDontOverlap ensures two given CIDRs don't overlap -// with one another. CIDR starting IPs are canonicalized -// before being compared. -func validateCIDRsDontOverlap(acidr, bcidr string) error { - _, a, err := net.ParseCIDR(acidr) - if err != nil { - return fmt.Errorf("invalid CIDR %q: %v", acidr, err) - } - if err := canonicalizeIP(&a.IP); err != nil { - return fmt.Errorf("invalid CIDR %q: %v", acidr, err) - } - _, b, err := net.ParseCIDR(bcidr) - if err != nil { - return fmt.Errorf("invalid CIDR %q: %v", bcidr, err) - } - if err := canonicalizeIP(&b.IP); err != nil { - return fmt.Errorf("invalid CIDR %q: %v", bcidr, err) - } - err = fmt.Errorf("%q and %q overlap", acidr, bcidr) - // IPs are of different families. - if len(a.IP) != len(b.IP) { - return nil - } - if a.Contains(b.IP) { - return err - } - if a.Contains(lastIP(b)) { - return err - } - if b.Contains(a.IP) { - return err - } - if b.Contains(lastIP(a)) { - return err - } - return nil -} - -// validateIPv4 checks if the given string is a valid IP v4 address and returns an error if not. -// Based on net.ParseIP. -func validateIPv4(v string) error { - if err := validateNonEmpty(v); err != nil { - return err - } - if ip := net.ParseIP(v); ip == nil || !strings.Contains(v, ".") { - return errors.New("invalid IPv4 address") - } - return nil -} - -// validateIntRange checks if the given string is a valid integer between `min` and `max` and returns an error if not. -func validateIntRange(v string, min int, max int) error { - i, err := strconv.Atoi(v) - if err != nil { - return validateInt(v) - } - if i < min { - return fmt.Errorf("cannot be less than %v", min) - } - if i > max { - return fmt.Errorf("cannot be greater than %v", max) - } - return nil -} - -// validateInt checks if the given string is a valid integer and returns an error if not. -func validateInt(v string) error { - if err := validateNonEmpty(v); err != nil { - return err - } - - if _, err := strconv.Atoi(v); err != nil { - return errors.New("invalid integer") - } - return nil -} - -// canonicalizeIP ensures that the given IP is in standard form -// and returns an error otherwise. -func canonicalizeIP(ip *net.IP) error { - if ip.To4() != nil { - *ip = ip.To4() - return nil - } - if ip.To16() != nil { - *ip = ip.To16() - return nil - } - return fmt.Errorf("IP %q is of unknown type", ip) -} - -func lastIP(cidr *net.IPNet) net.IP { - var last net.IP - for i := 0; i < len(cidr.IP); i++ { - last = append(last, cidr.IP[i]|^cidr.Mask[i]) - } - return last -} diff --git a/pkg/types/config/validate_test.go b/pkg/types/config/validate_test.go deleted file mode 100644 index e335e7296ad..00000000000 --- a/pkg/types/config/validate_test.go +++ /dev/null @@ -1,918 +0,0 @@ -package config - -import ( - "net" - "strings" - "testing" - - "github.com/openshift/installer/pkg/types/config/aws" - "github.com/openshift/installer/pkg/types/config/libvirt" -) - -func TestMissingNodePool(t *testing.T) { - cases := []struct { - cluster Cluster - errs int - }{ - { - cluster: Cluster{}, - errs: 2, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"", "", ""}, - }, - }, - errs: 2, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - }, - errs: 1, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - Worker: Worker{ - NodePools: []string{"worker"}, - }, - }, - errs: 0, - }, - } - - for i, c := range cases { - var n int - errs := c.cluster.Validate() - for _, err := range errs { - if _, ok := err.(*ErrMissingNodePool); ok { - n++ - } - } - - if n != c.errs { - t.Errorf("test case %d: expected %d missing node pool errors, got %d", i, c.errs, n) - } - } -} - -func TestMoreThanOneNodePool(t *testing.T) { - cases := []struct { - cluster Cluster - errs int - }{ - { - cluster: Cluster{}, - errs: 0, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - }, - errs: 0, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - Worker: Worker{ - NodePools: []string{"worker"}, - }, - }, - errs: 0, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master", "master2"}, - }, - Worker: Worker{ - NodePools: []string{"worker"}, - }, - }, - errs: 1, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master", "master2"}, - }, - Worker: Worker{ - NodePools: []string{"worker", "worker2"}, - }, - }, - errs: 2, - }, - } - - for i, c := range cases { - var n int - errs := c.cluster.Validate() - for _, err := range errs { - if _, ok := err.(*ErrMoreThanOneNodePool); ok { - n++ - } - } - - if n != c.errs { - t.Errorf("test case %d: expected %d more-than-one node pool errors, got %d", i, c.errs, n) - } - } -} - -func TestUnmatchedNodePool(t *testing.T) { - cases := []struct { - cluster Cluster - errs int - }{ - { - cluster: Cluster{}, - errs: 0, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - }, - errs: 1, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - Worker: Worker{ - NodePools: []string{"worker"}, - }, - }, - errs: 2, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master", "master2"}, - }, - Worker: Worker{ - NodePools: []string{"worker"}, - }, - NodePools: NodePools{ - { - Name: "master", - Count: 1, - }, - { - Name: "worker", - Count: 1, - }, - }, - }, - errs: 1, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - Worker: Worker{ - NodePools: []string{"worker"}, - }, - NodePools: NodePools{ - { - Name: "master", - Count: 1, - }, - { - Name: "worker", - Count: 1, - }, - }, - }, - errs: 0, - }, - } - - for i, c := range cases { - var n int - errs := c.cluster.Validate() - for _, err := range errs { - if _, ok := err.(*ErrUnmatchedNodePool); ok { - n++ - } - } - - if n != c.errs { - t.Errorf("test case %d: expected %d unmatched node pool errors, got %d", i, c.errs, n) - } - } -} - -func TestSharedNodePool(t *testing.T) { - cases := []struct { - cluster Cluster - errs int - }{ - { - cluster: Cluster{}, - errs: 0, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"master"}, - }, - }, - errs: 0, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"shared"}, - }, - Worker: Worker{ - NodePools: []string{"shared"}, - }, - }, - errs: 1, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"shared"}, - }, - Worker: Worker{ - NodePools: []string{"shared"}, - }, - }, - errs: 1, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"shared", "shared2"}, - }, - Worker: Worker{ - NodePools: []string{"shared", "shared2"}, - }, - }, - errs: 2, - }, - { - cluster: Cluster{ - Master: Master{ - NodePools: []string{"shared", "shared2"}, - }, - Worker: Worker{ - NodePools: []string{"shared", "shared2", "shared3"}, - }, - }, - errs: 2, - }, - } - - for i, c := range cases { - var n int - errs := c.cluster.Validate() - for _, err := range errs { - if _, ok := err.(*ErrSharedNodePool); ok { - n++ - } - } - - if n != c.errs { - t.Errorf("test case %d: expected %d shared node pool errors, got %d", i, c.errs, n) - } - } -} - -func TestAWSEndpoints(t *testing.T) { - cases := []struct { - cluster Cluster - err bool - }{ - { - cluster: Cluster{}, - err: true, - }, - { - cluster: defaultCluster, - err: false, - }, - { - cluster: Cluster{ - AWS: aws.AWS{ - Endpoints: "foo", - }, - }, - err: true, - }, - { - cluster: Cluster{ - AWS: aws.AWS{ - Endpoints: aws.EndpointsAll, - }, - }, - err: false, - }, - { - cluster: Cluster{ - AWS: aws.AWS{ - Endpoints: aws.EndpointsPrivate, - }, - }, - err: false, - }, - { - cluster: Cluster{ - AWS: aws.AWS{ - Endpoints: aws.EndpointsPublic, - }, - }, - err: false, - }, - } - - for i, c := range cases { - if err := c.cluster.validateAWSEndpoints(); (err != nil) != c.err { - no := "no" - if c.err { - no = "an" - } - t.Errorf("test case %d: expected %s error, got %v", i, no, err) - } - } -} - -func TestS3BucketNames(t *testing.T) { - cases := []struct { - cluster Cluster - err bool - }{ - { - cluster: defaultCluster, - err: true, - }, - { - cluster: Cluster{}, - err: true, - }, - { - cluster: Cluster{ - Name: "foo", - BaseDomain: "example.com", - }, - err: false, - }, - { - cluster: Cluster{ - Name: ".foo", - BaseDomain: "example.com", - }, - err: true, - }, - { - cluster: Cluster{ - Name: "foo", - BaseDomain: "example.com.", - }, - err: false, - }, - { - cluster: Cluster{ - Name: "foo", - BaseDomain: "012345678901234567890123456789012345678901234567890123456789.com", - }, - err: true, - }, - } - - for i, c := range cases { - if err := c.cluster.validateS3Bucket(); (err != nil) != c.err { - no := "no" - if c.err { - no = "an" - } - t.Errorf("test case %d: expected %s error, got %v", i, no, err) - } - } -} - -func TestValidateLibvirt(t *testing.T) { - cases := []struct { - cluster Cluster - err bool - }{ - { - cluster: Cluster{}, - err: true, - }, - { - cluster: defaultCluster, - err: true, - }, - { - cluster: Cluster{ - Libvirt: libvirt.Libvirt{ - Network: libvirt.Network{}, - Image: "", - URI: "", - }, - Networking: defaultCluster.Networking, - }, - err: true, - }, - { - cluster: Cluster{ - Libvirt: libvirt.Libvirt{ - Network: libvirt.Network{ - Name: "tectonic", - IfName: libvirt.DefaultIfName, - IPRange: "10.0.1.0/24", - }, - Image: "file:///foo", - URI: "baz", - }, - Networking: defaultCluster.Networking, - }, - err: false, - }, - { - cluster: Cluster{ - Libvirt: libvirt.Libvirt{ - Network: libvirt.Network{ - Name: "tectonic", - IfName: libvirt.DefaultIfName, - IPRange: "10.2.1.0/24", - }, - Image: "file:///foo", - URI: "baz", - }, - Networking: defaultCluster.Networking, - }, - err: true, - }, - { - cluster: Cluster{ - Libvirt: libvirt.Libvirt{ - Network: libvirt.Network{ - Name: "tectonic", - IfName: libvirt.DefaultIfName, - IPRange: "x", - }, - Image: "file:///foo", - URI: "baz", - }, - Networking: defaultCluster.Networking, - }, - err: true, - }, - { - cluster: Cluster{ - Libvirt: libvirt.Libvirt{ - Network: libvirt.Network{ - Name: "tectonic", - IfName: libvirt.DefaultIfName, - IPRange: "192.168.0.1/24", - }, - Image: "file:///foo", - URI: "baz", - }, - Networking: defaultCluster.Networking, - }, - err: false, - }, - } - - for i, c := range cases { - c.cluster.Platform = PlatformLibvirt - if err := c.cluster.validateLibvirt(); (err != nil) != c.err { - no := "no" - if c.err { - no = "an" - } - t.Errorf("test case %d: expected %s error, got %v", i, no, err) - } - } -} - -func TestValidateAWS(t *testing.T) { - d1 := defaultCluster - d1.Platform = PlatformAWS - d2 := d1 - d2.Name = "test" - d2.BaseDomain = "example.com" - cases := []struct { - cluster Cluster - err bool - }{ - { - cluster: Cluster{}, - err: false, - }, - { - cluster: Cluster{ - Platform: PlatformAWS, - }, - err: true, - }, - { - cluster: d1, - err: true, - }, - { - cluster: d2, - err: false, - }, - } - - for i, c := range cases { - if err := c.cluster.validateAWS(); (err != nil) != c.err { - no := "no" - if c.err { - no = "an" - } - t.Errorf("test case %d: expected %s error, got %v", i, no, err) - } - } -} - -func TestValidateOverlapWithPodOrServiceCIDR(t *testing.T) { - cases := []struct { - cidr string - cluster Cluster - err bool - }{ - { - cidr: "192.168.0.1/24", - cluster: Cluster{}, - err: true, - }, - { - cidr: "192.168.0.1/24", - cluster: defaultCluster, - err: false, - }, - { - cidr: "10.1.0.0/16", - cluster: defaultCluster, - err: false, - }, - { - cidr: "10.2.0.0/16", - cluster: defaultCluster, - err: true, - }, - { - cidr: "10.1.0.0/16", - cluster: Cluster{ - Networking: Networking{ - PodCIDR: "10.3.0.0/16", - ServiceCIDR: "10.4.0.0/16", - }, - }, - err: false, - }, - { - cidr: "10.3.0.0/24", - cluster: Cluster{ - Networking: Networking{ - PodCIDR: "10.3.0.0/16", - ServiceCIDR: "10.4.0.0/16", - }, - }, - err: true, - }, - { - cidr: "0.0.0.0/0", - cluster: Cluster{ - Networking: Networking{ - PodCIDR: "10.3.0.0/16", - ServiceCIDR: "10.4.0.0/16", - }, - }, - err: true, - }, - } - - for i, c := range cases { - if err := c.cluster.validateOverlapWithPodOrServiceCIDR(c.cidr, "test"); (err != nil) != c.err { - no := "no" - if c.err { - no = "an" - } - t.Errorf("test case %d: expected %s error, got %v", i, no, err) - } - } -} - -func TestLastIP(t *testing.T) { - cases := []struct { - in net.IPNet - out net.IP - }{ - { - in: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(24, 32), - }, - out: net.ParseIP("192.168.0.255"), - }, - { - in: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(22, 32), - }, - out: net.ParseIP("192.168.3.255"), - }, - { - in: net.IPNet{ - IP: net.ParseIP("192.168.0.0").To4(), - Mask: net.CIDRMask(32, 32), - }, - out: net.ParseIP("192.168.0.0"), - }, - { - in: net.IPNet{ - IP: net.ParseIP("0.0.0.0").To4(), - Mask: net.CIDRMask(0, 32), - }, - out: net.ParseIP("255.255.255.255"), - }, - } - - var out net.IP - for i, c := range cases { - if out = lastIP(&c.in); out.String() != c.out.String() { - t.Errorf("test case %d: expected %s but got %s", i, c.out, out) - } - } -} - -const caseMsg = "must be lower case" -const emptyMsg = "cannot be empty" -const invalidDomainMsg = "invalid domain name" -const invalidHostMsg = "invalid host (must be a domain name or IP address)" -const invalidIPMsg = "invalid IPv4 address" -const invalidIntMsg = "invalid integer" -const invalidPortMsg = "invalid port number" -const noCIDRNetmaskMsg = "must provide a CIDR netmask (eg, /24)" - -type test struct { - in string - expected string -} - -type validator func(string) error - -func runTests(t *testing.T, funcName string, fn validator, tests []test) { - for _, test := range tests { - err := fn(test.in) - if (err == nil && test.expected != "") || (err != nil && err.Error() != test.expected) { - t.Errorf("For %s(%q), expected %q, got %q", funcName, test.in, test.expected, err) - } - } -} - -func TestNonEmpty(t *testing.T) { - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"a", ""}, - {".", ""}, - {"日本語", ""}, - } - runTests(t, "NonEmpty", validateNonEmpty, tests) -} - -func TestInt(t *testing.T) { - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"2 3", invalidIntMsg}, - {"1.1", invalidIntMsg}, - {"abc", invalidIntMsg}, - {"日本語", invalidIntMsg}, - {"1 abc", invalidIntMsg}, - {"日本語2", invalidIntMsg}, - {"0", ""}, - {"1", ""}, - {"999999", ""}, - {"-1", ""}, - } - runTests(t, "Int", validateInt, tests) -} - -func TestIntRange(t *testing.T) { - tests := []struct { - in string - min int - max int - expected string - }{ - {"", 4, 6, emptyMsg}, - {" ", 4, 6, emptyMsg}, - {"2 3", 1, 2, invalidIntMsg}, - {"1.1", 0, 0, invalidIntMsg}, - {"abc", -2, -1, invalidIntMsg}, - {"日本語", 99, 100, invalidIntMsg}, - {"5", 4, 6, ""}, - {"5", 5, 5, ""}, - {"5", 6, 8, "cannot be less than 6"}, - {"5", 6, 4, "cannot be less than 6"}, - {"5", 2, 4, "cannot be greater than 4"}, - } - - for _, test := range tests { - err := validateIntRange(test.in, test.min, test.max) - if (err == nil && test.expected != "") || (err != nil && err.Error() != test.expected) { - t.Errorf("For IntRange(%q, %v, %v), expected %q, got %q", test.in, test.min, test.max, test.expected, err) - } - } -} - -func TestClusterName(t *testing.T) { - const charsMsg = "only lower case alphanumeric [a-z0-9], dashes and dots are allowed" - const lengthMsg = "must be between 1 and 253 characters" - const segmentLengthMsg = "no segment between dots can be more than 63 characters" - const startEndCharMsg = "must start and end with a lower case alphanumeric character [a-z0-9]" - const segmentStartEndCharMsg = "segments between dots must start and end with a lower case alphanumeric character [a-z0-9]" - - maxSizeName := strings.Repeat("123456789.", 25) + "123" - maxSizeSegment := strings.Repeat("1234567890", 6) + "123" - - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"a", ""}, - {"A", caseMsg}, - {"abc D", caseMsg}, - {"1", ""}, - {".", startEndCharMsg}, - {"a.", startEndCharMsg}, - {".a", startEndCharMsg}, - {"a.a", ""}, - {"-a", startEndCharMsg}, - {"a-", startEndCharMsg}, - {"a.-a", segmentStartEndCharMsg}, - {"a-.a", segmentStartEndCharMsg}, - {"a%a", charsMsg}, - {"日本語", charsMsg}, - {"a日本語a", charsMsg}, - {maxSizeName, ""}, - {maxSizeName + "a", lengthMsg}, - {maxSizeSegment + ".abc", ""}, - {maxSizeSegment + "a.abc", segmentLengthMsg}, - } - runTests(t, "ClusterName", validateClusterName, tests) -} - -func TestIPv4(t *testing.T) { - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"0.0.0.0", ""}, - {"1.2.3.4", ""}, - {"1.2.3.", invalidIPMsg}, - {"1.2.3.4.", invalidIPMsg}, - {"1.2.3.a", invalidIPMsg}, - {"255.255.255.255", ""}, - } - runTests(t, "IPv4", validateIPv4, tests) -} - -func TestSubnetCIDR(t *testing.T) { - const netmaskSizeMsg = "invalid netmask size (must be between 0 and 32)" - - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"/16", invalidIPMsg}, - {"0.0.0.0/0", ""}, - {"0.0.0.0/32", ""}, - {"1.2.3.4", noCIDRNetmaskMsg}, - {"1.2.3.", noCIDRNetmaskMsg}, - {"1.2.3.4.", noCIDRNetmaskMsg}, - {"1.2.3.4/0", ""}, - {"1.2.3.4/1", ""}, - {"1.2.3.4/31", ""}, - {"1.2.3.4/32", ""}, - {"1.2.3./16", invalidIPMsg}, - {"1.2.3.4./16", invalidIPMsg}, - {"1.2.3.4/33", netmaskSizeMsg}, - {"1.2.3.4/-1", netmaskSizeMsg}, - {"1.2.3.4/abc", netmaskSizeMsg}, - {"172.17.1.2", noCIDRNetmaskMsg}, - {"172.17.1.2/", netmaskSizeMsg}, - {"172.17.1.2/33", netmaskSizeMsg}, - {"172.17.1.2/20", "overlaps with default Docker Bridge subnet (172.17.0.0/16)"}, - {"255.255.255.255/1", ""}, - {"255.255.255.255/32", ""}, - } - runTests(t, "SubnetCIDR", validateSubnetCIDR, tests) -} - -func TestDomainName(t *testing.T) { - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"a", ""}, - {".", invalidDomainMsg}, - {"日本語", invalidDomainMsg}, - {"日本語.com", invalidDomainMsg}, - {"abc.日本語.com", invalidDomainMsg}, - {"a日本語a.com", invalidDomainMsg}, - {"abc", ""}, - {"ABC", ""}, - {"ABC123", ""}, - {"ABC123.COM123", ""}, - {"1", ""}, - {"0.0", ""}, - {"1.2.3.4", ""}, - {"1.2.3.4.", ""}, - {"abc.", ""}, - {"abc.com", ""}, - {"abc.com.", ""}, - {"a.b.c.d.e.f", ""}, - {".abc", invalidDomainMsg}, - {".abc.com", invalidDomainMsg}, - {".abc.com", invalidDomainMsg}, - } - runTests(t, "DomainName", ValidateDomainName, tests) -} - -func TestEmail(t *testing.T) { - const invalidMsg = "invalid email address" - tests := []test{ - {"", emptyMsg}, - {" ", emptyMsg}, - {"a", invalidMsg}, - {".", invalidMsg}, - {"日本語", invalidMsg}, - {"a@abc.com", ""}, - {"A@abc.com", ""}, - {"1@abc.com", ""}, - {"a.B.1.あ@abc.com", ""}, - {"ア@abc.com", ""}, - {"中文@abc.com", ""}, - {"a@abc.com", ""}, - {"a@ABC.com", ""}, - {"a@123.com", ""}, - {"a@日本語.com", invalidDomainMsg}, - {"a@.com", invalidDomainMsg}, - {"@abc.com", invalidMsg}, - } - runTests(t, "Email", ValidateEmail, tests) -} - -func TestCIDRsDontOverlap(t *testing.T) { - cases := []struct { - a string - b string - err bool - }{ - { - a: "192.168.0.0/24", - b: "192.168.0.0/24", - err: true, - }, - { - a: "192.168.0.0/24", - b: "192.168.0.3/24", - err: true, - }, - { - a: "192.168.0.0/30", - b: "192.168.0.3/30", - err: true, - }, - { - a: "192.168.0.0/30", - b: "192.168.0.4/30", - err: false, - }, - { - a: "0.0.0.0/0", - b: "192.168.0.0/24", - err: true, - }, - } - - for i, c := range cases { - if err := validateCIDRsDontOverlap(c.a, c.b); (err != nil) != c.err { - no := "no" - if c.err { - no = "an" - } - t.Errorf("test case %d: expected %s error, got %v", i, no, err) - } - } -} diff --git a/pkg/types/installconfig.go b/pkg/types/installconfig.go index bcb5941c139..7daa719ea64 100644 --- a/pkg/types/installconfig.go +++ b/pkg/types/installconfig.go @@ -71,6 +71,25 @@ type Platform struct { OpenStack *OpenStackPlatform `json:"openstack,omitempty"` } +// Name returns a string representation of the platform (e.g. "aws" if +// AWS is non-nil). It returns an empty string if no platform is +// configured. +func (p *Platform) Name() string { + if p == nil { + return "" + } + if p.AWS != nil { + return "aws" + } + if p.Libvirt != nil { + return "libvirt" + } + if p.OpenStack != nil { + return "openstack" + } + return "" +} + // Networking defines the pod network provider in the cluster. type Networking struct { Type NetworkType `json:"type"` diff --git a/pkg/validate/validate.go b/pkg/validate/validate.go new file mode 100644 index 00000000000..a755de68889 --- /dev/null +++ b/pkg/validate/validate.go @@ -0,0 +1,256 @@ +// Package validate contains validation utilities for installer types. +package validate + +import ( + "encoding/json" + "errors" + "fmt" + "net" + "regexp" + "strconv" + "strings" + "unicode/utf8" +) + +const ( + maxS3BucketNameLength = 63 +) + +// S3Bucket does some basic validation to ensure that the S3 bucket +// matches the S3 bucket naming rules. Not all rules are checked +// because Tectonic controls the generation of S3 bucket names, creating +// buckets of the form: . +// If domain-name contains a trailing dot, it's removed from the bucket name. +func S3Bucket(name string) error { + if len(name) < 3 { + return fmt.Errorf("the S3 bucket name %q is too short; S3 bucket names must contain at least three characters", name) + } + if len(name) > maxS3BucketNameLength { + return fmt.Errorf("the S3 bucket name %q is too long; S3 bucket names must be less than 63 characters", name) + } + if !regexp.MustCompile("^[a-z0-9][a-z0-9-.]{1,61}[a-z0-9]$").MatchString(name) { + return fmt.Errorf("invalid characters in S3 bucket name: %q", name) + } + return nil +} + +// DomainName checks if the given string is a valid domain name and returns an error if not. +func DomainName(v string) error { + if err := nonEmpty(v); err != nil { + return err + } + + split := strings.Split(v, ".") + for i, segment := range split { + // Trailing dot is OK + if len(segment) == 0 && i == len(split)-1 { + continue + } + if !isMatch("^[a-zA-Z0-9-]{1,63}$", segment) { + return errors.New("invalid domain name") + } + } + return nil +} + +// Email checks if the given string is a valid email address and returns an error if not. +func Email(v string) error { + if err := nonEmpty(v); err != nil { + return err + } + + invalidError := errors.New("invalid email address") + + split := strings.Split(v, "@") + if len(split) != 2 { + return invalidError + } + localPart := split[0] + domain := split[1] + + if nonEmpty(localPart) != nil { + return invalidError + } + + // No whitespace allowed in local-part + if isMatch(`\s`, localPart) { + return invalidError + } + + return DomainName(domain) +} + +// JSON validates that the given data is valid JSON. +func JSON(data []byte) error { + var dummy interface{} + return json.Unmarshal(data, &dummy) +} + +// prefixError wraps an error with a prefix or returns nil if there was no error. +// This is useful for wrapping errors returned by generic error funcs like `nonEmpty` so that the error includes the offending field name. +func prefixError(prefix string, err error) error { + if err != nil { + return fmt.Errorf("%s: %v", prefix, err) + } + return nil +} + +func isMatch(re string, v string) bool { + return regexp.MustCompile(re).MatchString(v) +} + +// ClusterName checks if the given string is a valid name for a cluster and returns an error if not. +func ClusterName(v string) error { + if err := nonEmpty(v); err != nil { + return err + } + + if length := utf8.RuneCountInString(v); length < 1 || length > 253 { + return errors.New("must be between 1 and 253 characters") + } + + if strings.ToLower(v) != v { + return errors.New("must be lower case") + } + + if !isMatch("^[a-z0-9-.]*$", v) { + return errors.New("only lower case alphanumeric [a-z0-9], dashes and dots are allowed") + } + + isAlphaNum := regexp.MustCompile("^[a-z0-9]$").MatchString + + // If we got this far, we know the string is ASCII and has at least one character + if !isAlphaNum(v[:1]) || !isAlphaNum(v[len(v)-1:]) { + return errors.New("must start and end with a lower case alphanumeric character [a-z0-9]") + } + + for _, segment := range strings.Split(v, ".") { + // Each segment can have up to 63 characters + if utf8.RuneCountInString(segment) > 63 { + return errors.New("no segment between dots can be more than 63 characters") + } + if !isAlphaNum(segment[:1]) || !isAlphaNum(segment[len(segment)-1:]) { + return errors.New("segments between dots must start and end with a lower case alphanumeric character [a-z0-9]") + } + } + + return nil +} + +// nonEmpty checks if the given string contains at least one non-whitespace character and returns an error if not. +func nonEmpty(v string) error { + if utf8.RuneCountInString(strings.TrimSpace(v)) == 0 { + return errors.New("cannot be empty") + } + return nil +} + +// SubnetCIDR checks if the given string is a valid CIDR for a master nodes or worker nodes subnet and returns an error if not. +func SubnetCIDR(v string) error { + if err := nonEmpty(v); err != nil { + return err + } + + split := strings.Split(v, "/") + + if len(split) == 1 { + return errors.New("must provide a CIDR netmask (eg, /24)") + } + + if len(split) != 2 { + return errors.New("invalid IPv4 address") + } + + ip := split[0] + + if err := IPv4(ip); err != nil { + return errors.New("invalid IPv4 address") + } + + if mask, err := strconv.Atoi(split[1]); err != nil || mask < 0 || mask > 32 { + return errors.New("invalid netmask size (must be between 0 and 32)") + } + + // Catch any invalid CIDRs not caught by the checks above + if _, _, err := net.ParseCIDR(v); err != nil { + return errors.New("invalid CIDR") + } + + if strings.HasPrefix(ip, "172.17.") { + return errors.New("overlaps with default Docker Bridge subnet (172.17.0.0/16)") + } + + return nil +} + +// CIDRsDontOverlap ensures two given CIDRs don't overlap +// with one another. CIDR starting IPs are canonicalized +// before being compared. +func CIDRsDontOverlap(acidr, bcidr string) error { + _, a, err := net.ParseCIDR(acidr) + if err != nil { + return fmt.Errorf("invalid CIDR %q: %v", acidr, err) + } + if err := canonicalizeIP(&a.IP); err != nil { + return fmt.Errorf("invalid CIDR %q: %v", acidr, err) + } + _, b, err := net.ParseCIDR(bcidr) + if err != nil { + return fmt.Errorf("invalid CIDR %q: %v", bcidr, err) + } + if err := canonicalizeIP(&b.IP); err != nil { + return fmt.Errorf("invalid CIDR %q: %v", bcidr, err) + } + err = fmt.Errorf("%q and %q overlap", acidr, bcidr) + // IPs are of different families. + if len(a.IP) != len(b.IP) { + return nil + } + if a.Contains(b.IP) { + return err + } + if a.Contains(lastIP(b)) { + return err + } + if b.Contains(a.IP) { + return err + } + if b.Contains(lastIP(a)) { + return err + } + return nil +} + +// IPv4 checks if the given string is a valid IP v4 address and returns an error if not. +// Based on net.ParseIP. +func IPv4(v string) error { + if err := nonEmpty(v); err != nil { + return err + } + if ip := net.ParseIP(v); ip == nil || !strings.Contains(v, ".") { + return errors.New("invalid IPv4 address") + } + return nil +} + +// canonicalizeIP ensures that the given IP is in standard form +// and returns an error otherwise. +func canonicalizeIP(ip *net.IP) error { + if ip.To4() != nil { + *ip = ip.To4() + return nil + } + if ip.To16() != nil { + *ip = ip.To16() + return nil + } + return fmt.Errorf("IP %q is of unknown type", ip) +} + +func lastIP(cidr *net.IPNet) net.IP { + var last net.IP + for i := 0; i < len(cidr.IP); i++ { + last = append(last, cidr.IP[i]|^cidr.Mask[i]) + } + return last +} diff --git a/pkg/validate/validate_test.go b/pkg/validate/validate_test.go new file mode 100644 index 00000000000..e42559dfc85 --- /dev/null +++ b/pkg/validate/validate_test.go @@ -0,0 +1,310 @@ +package validate + +import ( + "fmt" + "net" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestS3BucketNames(t *testing.T) { + cases := []struct { + name string + err *regexp.Regexp + }{ + { + name: "a.example.com", + }, + { + name: "", + err: regexp.MustCompile("^the S3 bucket name \"\" is too short; S3 bucket names must contain at least three characters$"), + }, + { + name: ".a.example.com", + err: regexp.MustCompile("^invalid characters in S3 bucket name: \".a.example.com\"$"), + }, + { + name: "a.example.com.", + err: regexp.MustCompile("^invalid characters in S3 bucket name: \"a.example.com.\"$"), + }, + { + name: "a.012345678901234567890123456789012345678901234567890123456789.com", + err: regexp.MustCompile("^the S3 bucket name \"a.012345678901234567890123456789012345678901234567890123456789.com\" is too long; S3 bucket names must be less than 63 characters$"), + }, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + err := S3Bucket(testCase.name) + if testCase.err == nil { + if err != nil { + t.Fatal(err) + } + } else { + assert.Regexp(t, testCase.err, err) + } + }) + } +} + +func TestLastIP(t *testing.T) { + cases := []struct { + in net.IPNet + out net.IP + }{ + { + in: net.IPNet{ + IP: net.ParseIP("192.168.0.0").To4(), + Mask: net.CIDRMask(24, 32), + }, + out: net.ParseIP("192.168.0.255"), + }, + { + in: net.IPNet{ + IP: net.ParseIP("192.168.0.0").To4(), + Mask: net.CIDRMask(22, 32), + }, + out: net.ParseIP("192.168.3.255"), + }, + { + in: net.IPNet{ + IP: net.ParseIP("192.168.0.0").To4(), + Mask: net.CIDRMask(32, 32), + }, + out: net.ParseIP("192.168.0.0"), + }, + { + in: net.IPNet{ + IP: net.ParseIP("0.0.0.0").To4(), + Mask: net.CIDRMask(0, 32), + }, + out: net.ParseIP("255.255.255.255"), + }, + } + + var out net.IP + for i, c := range cases { + if out = lastIP(&c.in); out.String() != c.out.String() { + t.Errorf("test case %d: expected %s but got %s", i, c.out, out) + } + } +} + +const caseMsg = "must be lower case" +const emptyMsg = "cannot be empty" +const invalidDomainMsg = "invalid domain name" +const invalidHostMsg = "invalid host (must be a domain name or IP address)" +const invalidIPMsg = "invalid IPv4 address" +const invalidIntMsg = "invalid integer" +const invalidPortMsg = "invalid port number" +const noCIDRNetmaskMsg = "must provide a CIDR netmask (eg, /24)" + +type test struct { + in string + expected string +} + +type validator func(string) error + +func runTests(t *testing.T, funcName string, fn validator, tests []test) { + for _, test := range tests { + err := fn(test.in) + if (err == nil && test.expected != "") || (err != nil && err.Error() != test.expected) { + t.Errorf("For %s(%q), expected %q, got %q", funcName, test.in, test.expected, err) + } + } +} + +func TestNonEmpty(t *testing.T) { + tests := []test{ + {"", emptyMsg}, + {" ", emptyMsg}, + {"a", ""}, + {".", ""}, + {"日本語", ""}, + } + runTests(t, "NonEmpty", nonEmpty, tests) +} + +func TestClusterName(t *testing.T) { + const charsMsg = "only lower case alphanumeric [a-z0-9], dashes and dots are allowed" + const lengthMsg = "must be between 1 and 253 characters" + const segmentLengthMsg = "no segment between dots can be more than 63 characters" + const startEndCharMsg = "must start and end with a lower case alphanumeric character [a-z0-9]" + const segmentStartEndCharMsg = "segments between dots must start and end with a lower case alphanumeric character [a-z0-9]" + + maxSizeName := strings.Repeat("123456789.", 25) + "123" + maxSizeSegment := strings.Repeat("1234567890", 6) + "123" + + tests := []test{ + {"", emptyMsg}, + {" ", emptyMsg}, + {"a", ""}, + {"A", caseMsg}, + {"abc D", caseMsg}, + {"1", ""}, + {".", startEndCharMsg}, + {"a.", startEndCharMsg}, + {".a", startEndCharMsg}, + {"a.a", ""}, + {"-a", startEndCharMsg}, + {"a-", startEndCharMsg}, + {"a.-a", segmentStartEndCharMsg}, + {"a-.a", segmentStartEndCharMsg}, + {"a%a", charsMsg}, + {"日本語", charsMsg}, + {"a日本語a", charsMsg}, + {maxSizeName, ""}, + {maxSizeName + "a", lengthMsg}, + {maxSizeSegment + ".abc", ""}, + {maxSizeSegment + "a.abc", segmentLengthMsg}, + } + runTests(t, "ClusterName", ClusterName, tests) +} + +func TestIPv4(t *testing.T) { + tests := []test{ + {"", emptyMsg}, + {" ", emptyMsg}, + {"0.0.0.0", ""}, + {"1.2.3.4", ""}, + {"1.2.3.", invalidIPMsg}, + {"1.2.3.4.", invalidIPMsg}, + {"1.2.3.a", invalidIPMsg}, + {"255.255.255.255", ""}, + } + runTests(t, "IPv4", IPv4, tests) +} + +func TestSubnetCIDR(t *testing.T) { + const netmaskSizeMsg = "invalid netmask size (must be between 0 and 32)" + + tests := []test{ + {"", emptyMsg}, + {" ", emptyMsg}, + {"/16", invalidIPMsg}, + {"0.0.0.0/0", ""}, + {"0.0.0.0/32", ""}, + {"1.2.3.4", noCIDRNetmaskMsg}, + {"1.2.3.", noCIDRNetmaskMsg}, + {"1.2.3.4.", noCIDRNetmaskMsg}, + {"1.2.3.4/0", ""}, + {"1.2.3.4/1", ""}, + {"1.2.3.4/31", ""}, + {"1.2.3.4/32", ""}, + {"1.2.3./16", invalidIPMsg}, + {"1.2.3.4./16", invalidIPMsg}, + {"1.2.3.4/33", netmaskSizeMsg}, + {"1.2.3.4/-1", netmaskSizeMsg}, + {"1.2.3.4/abc", netmaskSizeMsg}, + {"172.17.1.2", noCIDRNetmaskMsg}, + {"172.17.1.2/", netmaskSizeMsg}, + {"172.17.1.2/33", netmaskSizeMsg}, + {"172.17.1.2/20", "overlaps with default Docker Bridge subnet (172.17.0.0/16)"}, + {"255.255.255.255/1", ""}, + {"255.255.255.255/32", ""}, + } + runTests(t, "SubnetCIDR", SubnetCIDR, tests) +} + +func TestDomainName(t *testing.T) { + tests := []test{ + {"", emptyMsg}, + {" ", emptyMsg}, + {"a", ""}, + {".", invalidDomainMsg}, + {"日本語", invalidDomainMsg}, + {"日本語.com", invalidDomainMsg}, + {"abc.日本語.com", invalidDomainMsg}, + {"a日本語a.com", invalidDomainMsg}, + {"abc", ""}, + {"ABC", ""}, + {"ABC123", ""}, + {"ABC123.COM123", ""}, + {"1", ""}, + {"0.0", ""}, + {"1.2.3.4", ""}, + {"1.2.3.4.", ""}, + {"abc.", ""}, + {"abc.com", ""}, + {"abc.com.", ""}, + {"a.b.c.d.e.f", ""}, + {".abc", invalidDomainMsg}, + {".abc.com", invalidDomainMsg}, + {".abc.com", invalidDomainMsg}, + } + runTests(t, "DomainName", DomainName, tests) +} + +func TestEmail(t *testing.T) { + const invalidMsg = "invalid email address" + tests := []test{ + {"", emptyMsg}, + {" ", emptyMsg}, + {"a", invalidMsg}, + {".", invalidMsg}, + {"日本語", invalidMsg}, + {"a@abc.com", ""}, + {"A@abc.com", ""}, + {"1@abc.com", ""}, + {"a.B.1.あ@abc.com", ""}, + {"ア@abc.com", ""}, + {"中文@abc.com", ""}, + {"a@abc.com", ""}, + {"a@ABC.com", ""}, + {"a@123.com", ""}, + {"a@日本語.com", invalidDomainMsg}, + {"a@.com", invalidDomainMsg}, + {"@abc.com", invalidMsg}, + } + runTests(t, "Email", Email, tests) +} + +func TestCIDRsDontOverlap(t *testing.T) { + cases := []struct { + a string + b string + err *regexp.Regexp + }{ + { + a: "192.168.0.0/24", + b: "192.168.0.0/24", + err: regexp.MustCompile("^\"192.168.0.0/24\" and \"192.168.0.0/24\" overlap$"), + }, + { + a: "192.168.0.0/24", + b: "192.168.0.3/24", + err: regexp.MustCompile("^\"192.168.0.0/24\" and \"192.168.0.3/24\" overlap$"), + }, + { + a: "192.168.0.0/30", + b: "192.168.0.3/30", + err: regexp.MustCompile("^\"192.168.0.0/30\" and \"192.168.0.3/30\" overlap$"), + }, + { + a: "192.168.0.0/30", + b: "192.168.0.4/30", + }, + { + a: "0.0.0.0/0", + b: "192.168.0.0/24", + err: regexp.MustCompile("^\"0.0.0.0/0\" and \"192.168.0.0/24\" overlap$"), + }, + } + + for _, testCase := range cases { + t.Run(fmt.Sprintf("%s %s", testCase.a, testCase.b), func(t *testing.T) { + err := CIDRsDontOverlap(testCase.a, testCase.b) + if testCase.err == nil { + if err != nil { + t.Fatal(err) + } + } else { + assert.Regexp(t, testCase.err, err) + } + }) + } +}