diff --git a/Makefile b/Makefile index 3f7ade3421..bebae706c3 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,6 @@ codegen: GO15VENDOREXPERIMENT=1 go generate k8s.io/kops/upup/pkg/fi/fitasks test: - GO15VENDOREXPERIMENT=1 go test k8s.io/kops/cmd/... GO15VENDOREXPERIMENT=1 go test k8s.io/kops/upup/pkg/... godeps: diff --git a/cmd/kops/createcluster_test.go b/cmd/kops/createcluster_test.go deleted file mode 100644 index 97b7a1623e..0000000000 --- a/cmd/kops/createcluster_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package main - -import ( - "fmt" - "github.com/golang/glog" - "k8s.io/kops/upup/pkg/fi" - "k8s.io/kops/upup/pkg/fi/cloudup" - "k8s.io/kops/upup/pkg/fi/vfs" - "testing" -) - -// TODO: Refactor CreateClusterCmd into pkg/fi/cloudup - -func buildDefaultCreateCluster() *CreateClusterCmd { - var err error - - c := &CreateClusterCmd{} - - c.ClusterConfig = &cloudup.CloudConfig{} - c.ClusterConfig.ClusterName = "testcluster.mydomain.com" - c.ClusterConfig.NodeZones = []string{"us-east-1a", "us-east-1b", "us-east-1c"} - c.ClusterConfig.MasterZones = c.ClusterConfig.NodeZones - c.SSHPublicKey = "~/.ssh/id_rsa.pub" - - c.ClusterConfig.CloudProvider = "aws" - - dryrun := false - c.StateStore, err = fi.NewVFSStateStore(vfs.NewFSPath("test-state"), dryrun) - if err != nil { - glog.Fatalf("error building state store: %v", err) - } - - return c -} - -func expectErrorFromRun(t *testing.T, c *CreateClusterCmd, message string) { - err := c.Run() - if err == nil { - t.Fatalf("Expected error from run") - } - actualMessage := fmt.Sprintf("%v", err) - if actualMessage != message { - t.Fatalf("Expected error %q, got %q", message, actualMessage) - } -} - -func TestCreateCluster_DuplicateZones(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{"us-east-1a", "us-east-1b", "us-east-1b"} - c.ClusterConfig.MasterZones = []string{"us-east-1a"} - expectErrorFromRun(t, c, "NodeZones contained a duplicate value: us-east-1b") -} - -func TestCreateCluster_NoClusterName(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.ClusterName = "" - expectErrorFromRun(t, c, "-name is required (e.g. mycluster.myzone.com)") -} - -func TestCreateCluster_NoCloud(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.CloudProvider = "" - expectErrorFromRun(t, c, "-cloud is required (e.g. aws, gce)") -} - -func TestCreateCluster_ExtraMasterZone(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{"us-east-1a", "us-east-1c"} - c.ClusterConfig.MasterZones = []string{"us-east-1a", "us-east-1b", "us-east-1c"} - expectErrorFromRun(t, c, "All MasterZones must (currently) also be NodeZones") -} - -func TestCreateCluster_NoMasterZones(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.MasterZones = []string{} - expectErrorFromRun(t, c, "must specify at least one MasterZone") -} - -func TestCreateCluster_NoNodeZones(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{} - expectErrorFromRun(t, c, "must specify at least one NodeZone") -} - -func TestCreateCluster_RegionAsZone(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{"us-east-1"} - c.ClusterConfig.MasterZones = c.ClusterConfig.NodeZones - expectErrorFromRun(t, c, "Region is not a recognized EC2 region: \"us-east-\" (check you have specified valid zones?)") -} - -func TestCreateCluster_BadZone(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{"us-east-1z"} - c.ClusterConfig.MasterZones = c.ClusterConfig.NodeZones - expectErrorFromRun(t, c, "Zone is not a recognized AZ: \"us-east-1z\" (check you have specified a valid zone?)") -} - -func TestCreateCluster_MixedRegion(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{"us-west-1a", "us-west-2b", "us-west-2c"} - c.ClusterConfig.MasterZones = c.ClusterConfig.NodeZones - expectErrorFromRun(t, c, "Clusters cannot span multiple regions") -} - -func TestCreateCluster_EvenEtcdClusterSize(t *testing.T) { - c := buildDefaultCreateCluster() - c.ClusterConfig.NodeZones = []string{"us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d"} - c.ClusterConfig.MasterZones = c.ClusterConfig.NodeZones - expectErrorFromRun(t, c, "There should be an odd number of master-zones, for etcd's quorum. Hint: Use -zones and -master-zones to declare node zones and master zones separately.") -} diff --git a/upup/pkg/api/validation.go b/upup/pkg/api/validation.go index 5f114d7ed8..157b3b6b5d 100644 --- a/upup/pkg/api/validation.go +++ b/upup/pkg/api/validation.go @@ -138,7 +138,7 @@ func (c *Cluster) Validate() error { return fmt.Errorf("MasterKubelet CloudProvider did not match cluster CloudProvider") } if c.Spec.KubeAPIServer.CloudProvider != c.Spec.CloudProvider { - return fmt.Errorf("Errorf CloudProvider did not match cluster CloudProvider") + return fmt.Errorf("KubeAPIServer CloudProvider did not match cluster CloudProvider") } if c.Spec.KubeControllerManager.CloudProvider != c.Spec.CloudProvider { return fmt.Errorf("KubeControllerManager CloudProvider did not match cluster CloudProvider") diff --git a/upup/pkg/fi/cloudup/create_cluster.go b/upup/pkg/fi/cloudup/create_cluster.go index 73d70d9255..8987089414 100644 --- a/upup/pkg/fi/cloudup/create_cluster.go +++ b/upup/pkg/fi/cloudup/create_cluster.go @@ -112,7 +112,7 @@ func (c *CreateClusterCmd) Run() error { if len(c.Cluster.Spec.Zones) == 0 { // TODO: Auto choose zones from region? - return fmt.Errorf("must configuration at least one Zone (use --zones)") + return fmt.Errorf("must configure at least one Zone (use --zones)") } if len(c.InstanceGroups) == 0 { @@ -208,7 +208,7 @@ func (c *CreateClusterCmd) Run() error { if (len(etcdZones) % 2) == 0 { // Not technically a requirement, but doesn't really make sense to allow - return fmt.Errorf("There should be an odd number of master-zones, for etcd's quorum. Hint: Use --zone and --master-zone to declare node zones and master zones separately.") + return fmt.Errorf("There should be an odd number of master-zones, for etcd's quorum. Hint: Use --zones and --master-zones to declare node zones and master zones separately.") } } } diff --git a/upup/pkg/fi/cloudup/createcluster_test.go b/upup/pkg/fi/cloudup/createcluster_test.go new file mode 100644 index 0000000000..5a3b8db0c1 --- /dev/null +++ b/upup/pkg/fi/cloudup/createcluster_test.go @@ -0,0 +1,212 @@ +package cloudup + +import ( + "fmt" + "github.com/golang/glog" + "k8s.io/kops/upup/pkg/api" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/vfs" + k8sapi "k8s.io/kubernetes/pkg/api" + "os" + "path" + "strings" + "testing" +) + +func buildDefaultCreateCluster() *CreateClusterCmd { + var err error + + memfs := vfs.NewMemFSContext() + memfs.MarkClusterReadable() + + c := &CreateClusterCmd{} + + // TODO: We should actually just specify the minimums here, and run in though the default logic + c.Cluster = &api.Cluster{} + c.Cluster.Name = "testcluster.mydomain.com" + c.Cluster.Spec.Zones = []*api.ClusterZoneSpec{ + {Name: "us-east-1a", CIDR: "172.20.1.0/24"}, + {Name: "us-east-1b", CIDR: "172.20.2.0/24"}, + {Name: "us-east-1c", CIDR: "172.20.3.0/24"}, + {Name: "us-east-1d", CIDR: "172.20.4.0/24"}, + } + c.InstanceGroups = append(c.InstanceGroups, buildNodeInstanceGroup("us-east-1a")) + c.InstanceGroups = append(c.InstanceGroups, buildMasterInstanceGroup("us-east-1a")) + c.SSHPublicKey = path.Join(os.Getenv("HOME"), ".ssh", "id_rsa.pub") + + c.Cluster.Spec.Kubelet = &api.KubeletConfig{} + c.Cluster.Spec.KubeControllerManager = &api.KubeControllerManagerConfig{} + c.Cluster.Spec.KubeDNS = &api.KubeDNSConfig{} + c.Cluster.Spec.KubeAPIServer = &api.KubeAPIServerConfig{} + c.Cluster.Spec.KubeProxy = &api.KubeProxyConfig{} + c.Cluster.Spec.Docker = &api.DockerConfig{} + + c.Cluster.Spec.NetworkCIDR = "172.20.0.0/16" + + c.Cluster.Spec.NonMasqueradeCIDR = "100.64.0.0/10" + c.Cluster.Spec.Kubelet.NonMasqueradeCIDR = c.Cluster.Spec.NonMasqueradeCIDR + + c.Cluster.Spec.ServiceClusterIPRange = "100.64.1.0/24" + c.Cluster.Spec.KubeAPIServer.ServiceClusterIPRange = c.Cluster.Spec.ServiceClusterIPRange + + c.Cluster.Spec.KubeDNS.ServerIP = "100.64.1.10" + c.Cluster.Spec.Kubelet.ClusterDNS = c.Cluster.Spec.KubeDNS.ServerIP + + c.Cluster.Spec.CloudProvider = "aws" + c.Cluster.Spec.Kubelet.CloudProvider = c.Cluster.Spec.CloudProvider + c.Cluster.Spec.KubeAPIServer.CloudProvider = c.Cluster.Spec.CloudProvider + c.Cluster.Spec.KubeControllerManager.CloudProvider = c.Cluster.Spec.CloudProvider + + c.Target = "dryrun" + + dryrun := false + c.StateStore, err = fi.NewVFSStateStore(vfs.NewMemFSPath(memfs, "test-statestore"), c.Cluster.Name, dryrun) + if err != nil { + glog.Fatalf("error building state store: %v", err) + } + + return c +} + +func buildNodeInstanceGroup(zones ...string) *api.InstanceGroup { + g := &api.InstanceGroup{ + ObjectMeta: k8sapi.ObjectMeta{Name: "nodes-" + strings.Join(zones, "-")}, + Spec: api.InstanceGroupSpec{ + Role: api.InstanceGroupRoleNode, + Zones: zones, + }, + } + return g +} + +func buildMasterInstanceGroup(zones ...string) *api.InstanceGroup { + g := &api.InstanceGroup{ + ObjectMeta: k8sapi.ObjectMeta{Name: "master-" + strings.Join(zones, "-")}, + Spec: api.InstanceGroupSpec{ + Role: api.InstanceGroupRoleMaster, + Zones: zones, + }, + } + return g +} + +func expectErrorFromRun(t *testing.T, c *CreateClusterCmd, message string) { + err := c.Run() + if err == nil { + t.Fatalf("Expected error from run") + } + actualMessage := fmt.Sprintf("%v", err) + if !strings.Contains(actualMessage, message) { + t.Fatalf("Expected error %q, got %q", message, actualMessage) + } +} + +func TestCreateCluster_DuplicateZones(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Spec.Zones = []*api.ClusterZoneSpec{ + {Name: "us-east-1a", CIDR: "172.20.1.0/24"}, + {Name: "us-east-1a", CIDR: "172.20.2.0/24"}, + } + expectErrorFromRun(t, c, "Zones contained a duplicate value: us-east-1a") +} + +func TestCreateCluster_NoClusterName(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Name = "" + expectErrorFromRun(t, c, "ClusterName is required") +} + +func TestCreateCluster_NoCloud(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Spec.CloudProvider = "" + expectErrorFromRun(t, c, "-cloud is required (e.g. aws, gce)") +} + +func TestCreateCluster_ExtraMasterZone(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Spec.Zones = []*api.ClusterZoneSpec{ + {Name: "us-east-1a", CIDR: "172.20.1.0/24"}, + {Name: "us-east-1b", CIDR: "172.20.2.0/24"}, + } + c.InstanceGroups = []*api.InstanceGroup{} + c.InstanceGroups = append(c.InstanceGroups, buildNodeInstanceGroup("us-east-1a", "us-east-1b")) + c.InstanceGroups = append(c.InstanceGroups, buildMasterInstanceGroup("us-east-1a", "us-east-1b", "us-east-1c")) + expectErrorFromRun(t, c, "is not configured as a Zone in the cluster") +} + +func TestCreateCluster_NoMasterZones(t *testing.T) { + c := buildDefaultCreateCluster() + c.InstanceGroups = []*api.InstanceGroup{} + c.InstanceGroups = append(c.InstanceGroups, buildNodeInstanceGroup("us-east-1a")) + expectErrorFromRun(t, c, "must configure at least one Master InstanceGroup") +} + +func TestCreateCluster_NoNodeZones(t *testing.T) { + c := buildDefaultCreateCluster() + c.InstanceGroups = []*api.InstanceGroup{} + c.InstanceGroups = append(c.InstanceGroups, buildMasterInstanceGroup("us-east-1a")) + expectErrorFromRun(t, c, "must configure at least one Node InstanceGroup") +} + +func TestCreateCluster_RegionAsZone(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Spec.Zones = []*api.ClusterZoneSpec{ + {Name: "us-east-1", CIDR: "172.20.1.0/24"}, + } + c.InstanceGroups = []*api.InstanceGroup{ + buildNodeInstanceGroup("us-east-1"), + buildMasterInstanceGroup("us-east-1"), + } + expectErrorFromRun(t, c, "Region is not a recognized EC2 region: \"us-east-\" (check you have specified valid zones?)") +} + +func TestCreateCluster_NotIncludedZone(t *testing.T) { + c := buildDefaultCreateCluster() + c.InstanceGroups = []*api.InstanceGroup{ + buildNodeInstanceGroup("us-east-1e"), + buildMasterInstanceGroup("us-east-1a"), + } + expectErrorFromRun(t, c, "not configured as a Zone in the cluster") +} + +func TestCreateCluster_BadZone(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Spec.Zones = []*api.ClusterZoneSpec{ + {Name: "us-east-1z", CIDR: "172.20.1.0/24"}, + } + c.InstanceGroups = []*api.InstanceGroup{ + buildNodeInstanceGroup("us-east-1z"), + buildMasterInstanceGroup("us-east-1z"), + } + expectErrorFromRun(t, c, "Zone is not a recognized AZ: \"us-east-1z\" (check you have specified a valid zone?)") +} + +func TestCreateCluster_MixedRegion(t *testing.T) { + c := buildDefaultCreateCluster() + c.Cluster.Spec.Zones = []*api.ClusterZoneSpec{ + {Name: "us-east-1a", CIDR: "172.20.1.0/24"}, + {Name: "us-west-1b", CIDR: "172.20.2.0/24"}, + } + c.InstanceGroups = []*api.InstanceGroup{ + buildNodeInstanceGroup("us-east-1a", "us-west-1b"), + buildMasterInstanceGroup("us-east-1a"), + } + expectErrorFromRun(t, c, "Clusters cannot span multiple regions") +} + +func TestCreateCluster_EvenEtcdClusterSize(t *testing.T) { + c := buildDefaultCreateCluster() + c.InstanceGroups = []*api.InstanceGroup{} + c.InstanceGroups = append(c.InstanceGroups, buildNodeInstanceGroup("us-east-1a")) + c.InstanceGroups = append(c.InstanceGroups, buildMasterInstanceGroup("us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d")) + c.Cluster.Spec.EtcdClusters = []*api.EtcdClusterSpec{ + { + Name: "main", + Members: []*api.EtcdMemberSpec{ + {Name: "us-east-1a", Zone: "us-east-1a"}, + {Name: "us-east-1b", Zone: "us-east-1b"}, + }, + }, + } + expectErrorFromRun(t, c, "There should be an odd number of master-zones, for etcd's quorum. Hint: Use --zones and --master-zones to declare node zones and master zones separately.") +} diff --git a/upup/pkg/fi/vfs/memfs.go b/upup/pkg/fi/vfs/memfs.go new file mode 100644 index 0000000000..2a64da3091 --- /dev/null +++ b/upup/pkg/fi/vfs/memfs.go @@ -0,0 +1,127 @@ +package vfs + +import ( + "os" + "path" + "strings" +) + +type MemFSPath struct { + context *MemFSContext + location string + + contents []byte + children map[string]*MemFSPath +} + +var _ Path = &MemFSPath{} + +type MemFSContext struct { + clusterReadable bool + root *MemFSPath +} + +func NewMemFSContext() *MemFSContext { + c := &MemFSContext{} + c.root = &MemFSPath{ + context: c, + location: "", + } + return c +} + +// MarkClusterReadable pretends the current memfscontext is cluster readable; this is useful for tests +func (c *MemFSContext) MarkClusterReadable() { + c.clusterReadable = true +} + +func (c *MemFSPath) IsClusterReadable() bool { + return c.context.clusterReadable +} + +var _ HasClusterReadable = &MemFSPath{} + +func NewMemFSPath(context *MemFSContext, location string) *MemFSPath { + return context.root.Join(location).(*MemFSPath) +} + +func (p *MemFSPath) Join(relativePath ...string) Path { + joined := path.Join(relativePath...) + tokens := strings.Split(joined, "/") + current := p + for _, token := range tokens { + if current.children == nil { + current.children = make(map[string]*MemFSPath) + } + child := current.children[token] + if child == nil { + child = &MemFSPath{ + context: p.context, + location: path.Join(current.location, token), + } + current.children[token] = child + } + current = child + } + return current +} + +func (p *MemFSPath) WriteFile(data []byte) error { + p.contents = data + return nil +} + +func (p *MemFSPath) CreateFile(data []byte) error { + // Check if exists + if p.contents != nil { + return os.ErrExist + } + + return p.WriteFile(data) +} + +func (p *MemFSPath) ReadFile() ([]byte, error) { + if p.contents == nil { + return nil, os.ErrNotExist + } + // TODO: Copy? + return p.contents, nil +} + +func (p *MemFSPath) ReadDir() ([]Path, error) { + var paths []Path + for _, f := range p.children { + paths = append(paths, f) + } + return paths, nil +} + +func (p *MemFSPath) ReadTree() ([]Path, error) { + var paths []Path + p.readTree(&paths) + return paths, nil +} + +func (p *MemFSPath) readTree(dest *[]Path) { + for _, f := range p.children { + *dest = append(*dest, f) + f.readTree(dest) + } +} + +func (p *MemFSPath) Base() string { + return path.Base(p.location) +} + +func (p *MemFSPath) Path() string { + return p.location +} + +func (p *MemFSPath) String() string { + return p.Path() +} + +func (p *MemFSPath) Remove() error { + p.contents = nil + return nil +} diff --git a/upup/pkg/fi/vfs/vfs.go b/upup/pkg/fi/vfs/vfs.go index a2e8d85e86..d121088838 100644 --- a/upup/pkg/fi/vfs/vfs.go +++ b/upup/pkg/fi/vfs/vfs.go @@ -61,6 +61,10 @@ func RelativePath(base Path, child Path) (string, error) { } func IsClusterReadable(p Path) bool { + if hcr, ok := p.(HasClusterReadable); ok { + return hcr.IsClusterReadable() + } + switch p.(type) { case *S3Path: return true @@ -76,3 +80,7 @@ func IsClusterReadable(p Path) bool { return false } } + +type HasClusterReadable interface { + IsClusterReadable() bool +}