github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/integration-tests/provision.go (about)

     1  package integration_tests
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	"github.com/apprenda/kismatic/integration-tests/aws"
    11  	"github.com/apprenda/kismatic/integration-tests/packet"
    12  	"github.com/apprenda/kismatic/integration-tests/retry"
    13  
    14  	homedir "github.com/mitchellh/go-homedir"
    15  )
    16  
    17  const (
    18  	Ubuntu1604LTS = linuxDistro("ubuntu1604LTS")
    19  	CentOS7       = linuxDistro("centos7")
    20  	RedHat7       = linuxDistro("rhel7")
    21  
    22  	AWSTargetRegion    = "us-east-1"
    23  	AWSSubnetID        = "subnet-25e13d08"
    24  	AWSKeyName         = "kismatic-integration-testing"
    25  	AWSSecurityGroupID = "sg-d1dc4dab"
    26  	AWSHostedZoneID    = "Z1U99XHRIRLO81"
    27  )
    28  
    29  type infrastructureProvisioner interface {
    30  	ProvisionNodes(NodeCount, linuxDistro, ...string) (provisionedNodes, error)
    31  	TerminateNodes(provisionedNodes) error
    32  	TerminateNode(NodeDeets) error
    33  	SSHKey() string
    34  	ConfigureDNS(masterIPs []string) (*DNSRecord, error)
    35  	RemoveDNS(dnsRecord *DNSRecord) error
    36  	CreateNFSServers() ([]nfsServer, error)
    37  }
    38  
    39  type nfsServer struct {
    40  	IpAddress string
    41  }
    42  
    43  type linuxDistro string
    44  
    45  type NodeCount struct {
    46  	Etcd    uint16
    47  	Master  uint16
    48  	Worker  uint16
    49  	Ingress uint16
    50  	Storage uint16
    51  }
    52  
    53  func (nc NodeCount) Total() uint16 {
    54  	return nc.Etcd + nc.Master + nc.Worker + nc.Ingress + nc.Storage
    55  }
    56  
    57  type provisionedNodes struct {
    58  	etcd      []NodeDeets
    59  	master    []NodeDeets
    60  	worker    []NodeDeets
    61  	ingress   []NodeDeets
    62  	storage   []NodeDeets
    63  	dnsRecord *DNSRecord
    64  }
    65  
    66  func (p provisionedNodes) allNodes() []NodeDeets {
    67  	n := []NodeDeets{}
    68  	n = append(n, p.etcd...)
    69  	n = append(n, p.master...)
    70  	n = append(n, p.worker...)
    71  	n = append(n, p.ingress...)
    72  	n = append(n, p.storage...)
    73  	return n
    74  }
    75  
    76  type NodeDeets struct {
    77  	id        string
    78  	Hostname  string
    79  	PublicIP  string
    80  	PrivateIP string
    81  	SSHUser   string
    82  }
    83  
    84  type DNSRecord struct {
    85  	Name   string
    86  	Values []string
    87  }
    88  
    89  type sshMachineProvisioner struct {
    90  	sshKey string
    91  }
    92  
    93  func (p sshMachineProvisioner) SSHKey() string {
    94  	return p.sshKey
    95  }
    96  
    97  type awsProvisioner struct {
    98  	sshMachineProvisioner
    99  	client aws.Client
   100  }
   101  
   102  func AWSClientFromEnvironment() (infrastructureProvisioner, bool) {
   103  	accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
   104  	secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
   105  	if accessKeyID == "" || secretAccessKey == "" {
   106  		return nil, false
   107  	}
   108  	c := aws.Client{
   109  		Config: aws.ClientConfig{
   110  			Region:          AWSTargetRegion,
   111  			SubnetID:        AWSSubnetID,
   112  			Keyname:         AWSKeyName,
   113  			SecurityGroupID: AWSSecurityGroupID,
   114  			HostedZoneID:    AWSHostedZoneID,
   115  		},
   116  		Credentials: aws.Credentials{
   117  			ID:     accessKeyID,
   118  			Secret: secretAccessKey,
   119  		},
   120  	}
   121  	overrideRegion := os.Getenv("AWS_TARGET_REGION")
   122  	if overrideRegion != "" {
   123  		c.Config.Region = overrideRegion
   124  	}
   125  	overrideSubnet := os.Getenv("AWS_SUBNET_ID")
   126  	if overrideSubnet != "" {
   127  		c.Config.SubnetID = overrideSubnet
   128  	}
   129  	overrideKeyName := os.Getenv("AWS_KEY_NAME")
   130  	if overrideKeyName != "" {
   131  		c.Config.Keyname = overrideKeyName
   132  	}
   133  	overrideSecGroup := os.Getenv("AWS_SECURITY_GROUP_ID")
   134  	if overrideSecGroup != "" {
   135  		c.Config.SecurityGroupID = overrideSecGroup
   136  	}
   137  	overrideHostedZoneID := os.Getenv("AWS_HOSTED_ZONE_ID")
   138  	if overrideHostedZoneID != "" {
   139  		c.Config.HostedZoneID = overrideHostedZoneID
   140  	}
   141  	p := awsProvisioner{client: c}
   142  	p.sshKey = os.Getenv("AWS_SSH_KEY_PATH")
   143  	if p.sshKey == "" {
   144  		dir, _ := homedir.Dir()
   145  		p.sshKey = filepath.Join(dir, ".ssh", "kismatic-integration-testing.pem")
   146  	}
   147  	return p, true
   148  }
   149  
   150  func (p awsProvisioner) ProvisionNodes(nodeCount NodeCount, distro linuxDistro, opts ...string) (provisionedNodes, error) {
   151  	var ami aws.AMI
   152  	switch distro {
   153  	case Ubuntu1604LTS:
   154  		ami = aws.Ubuntu1604LTSEast
   155  	case CentOS7:
   156  		ami = aws.CentOS7East
   157  	case RedHat7:
   158  		ami = aws.RedHat7East
   159  	default:
   160  		panic(fmt.Sprintf("Used an unsupported distribution: %s", distro))
   161  	}
   162  	var addBlockDevice bool
   163  	for _, o := range opts {
   164  		if o == "block_device" {
   165  			addBlockDevice = true
   166  		}
   167  	}
   168  
   169  	// when AWS cloud-provider is enabled all nodes in a cluster must have a unique identifier,
   170  	// set via kubernetes.io/cluster/$UNIQUE_ID EC2 tag
   171  	uniqueTag := fmt.Sprintf("kubernetes.io/cluster/testcluster%d", rand.Intn(32767))
   172  	provisioned := provisionedNodes{}
   173  	var i uint16
   174  	for i = 0; i < nodeCount.Etcd; i++ {
   175  		nodeID, err := p.client.CreateNode(ami, aws.T2Medium, addBlockDevice, map[string]string{uniqueTag: ""})
   176  		if err != nil {
   177  			return provisioned, err
   178  		}
   179  		provisioned.etcd = append(provisioned.etcd, NodeDeets{id: nodeID})
   180  	}
   181  	for i = 0; i < nodeCount.Master; i++ {
   182  		nodeID, err := p.client.CreateNode(ami, aws.T2Medium, addBlockDevice, map[string]string{uniqueTag: ""})
   183  		if err != nil {
   184  			return provisioned, err
   185  		}
   186  		provisioned.master = append(provisioned.master, NodeDeets{id: nodeID})
   187  	}
   188  	for i = 0; i < nodeCount.Worker; i++ {
   189  		nodeID, err := p.client.CreateNode(ami, aws.T2Medium, addBlockDevice, map[string]string{uniqueTag: ""})
   190  		if err != nil {
   191  			return provisioned, err
   192  		}
   193  		provisioned.worker = append(provisioned.worker, NodeDeets{id: nodeID})
   194  	}
   195  	for i = 0; i < nodeCount.Ingress; i++ {
   196  		nodeID, err := p.client.CreateNode(ami, aws.T2Medium, addBlockDevice, map[string]string{uniqueTag: ""})
   197  		if err != nil {
   198  			return provisioned, err
   199  		}
   200  		provisioned.ingress = append(provisioned.ingress, NodeDeets{id: nodeID})
   201  	}
   202  	for i = 0; i < nodeCount.Storage; i++ {
   203  		nodeID, err := p.client.CreateNode(ami, aws.T2Medium, addBlockDevice, map[string]string{uniqueTag: ""})
   204  		if err != nil {
   205  			return provisioned, err
   206  		}
   207  		provisioned.storage = append(provisioned.storage, NodeDeets{id: nodeID})
   208  	}
   209  	// Wait until all instances have their public IPs assigned
   210  	for i := range provisioned.etcd {
   211  		etcd := &provisioned.etcd[i]
   212  		if err := p.updateNodeWithDeets(etcd.id, etcd); err != nil {
   213  			return provisioned, err
   214  		}
   215  	}
   216  	for i := range provisioned.master {
   217  		master := &provisioned.master[i]
   218  		if err := p.updateNodeWithDeets(master.id, master); err != nil {
   219  			return provisioned, err
   220  		}
   221  	}
   222  	for i := range provisioned.worker {
   223  		worker := &provisioned.worker[i]
   224  		if err := p.updateNodeWithDeets(worker.id, worker); err != nil {
   225  			return provisioned, err
   226  		}
   227  	}
   228  	for i := range provisioned.ingress {
   229  		ingress := &provisioned.ingress[i]
   230  		if err := p.updateNodeWithDeets(ingress.id, ingress); err != nil {
   231  			return provisioned, err
   232  		}
   233  	}
   234  	for i := range provisioned.storage {
   235  		storage := &provisioned.storage[i]
   236  		if err := p.updateNodeWithDeets(storage.id, storage); err != nil {
   237  			return provisioned, err
   238  		}
   239  	}
   240  	return provisioned, nil
   241  }
   242  
   243  func (p awsProvisioner) updateNodeWithDeets(nodeID string, node *NodeDeets) error {
   244  	for {
   245  		fmt.Print(".")
   246  		awsNode, err := p.client.GetNode(nodeID)
   247  		if err != nil {
   248  			return err
   249  		}
   250  		node.PublicIP = awsNode.PublicIP
   251  		node.PrivateIP = awsNode.PrivateIP
   252  		node.SSHUser = awsNode.SSHUser
   253  		node.Hostname = awsNode.PrivateDNSName
   254  		if node.PublicIP != "" && node.Hostname != "" && node.PrivateIP != "" {
   255  			return nil
   256  		}
   257  		time.Sleep(5 * time.Second)
   258  	}
   259  }
   260  
   261  func (p awsProvisioner) TerminateNodes(runningNodes provisionedNodes) error {
   262  	nodes := runningNodes.allNodes()
   263  	nodeIDs := []string{}
   264  	for _, n := range nodes {
   265  		nodeIDs = append(nodeIDs, n.id)
   266  	}
   267  	return p.client.DestroyNodes(nodeIDs)
   268  }
   269  
   270  // TerminateNode will attempt to terminate a node and wait for the state to not be available
   271  func (p awsProvisioner) TerminateNode(node NodeDeets) error {
   272  	err := retry.WithBackoff(func() error {
   273  		if err2 := p.client.DestroyNodes([]string{node.id}); err2 != nil {
   274  			return fmt.Errorf("Could not terminate node: %v", err2)
   275  		}
   276  
   277  		node, err3 := p.client.GetNode(node.id)
   278  		if err3 != nil {
   279  			return fmt.Errorf("Something went wrong after terminating node: %v", err3)
   280  		}
   281  		if node.State == aws.StateAvailable {
   282  			return fmt.Errorf("Terminating machine took too long")
   283  		}
   284  		return nil
   285  	}, 7)
   286  
   287  	return err
   288  }
   289  
   290  func (p awsProvisioner) ConfigureDNS(masterIPs []string) (*DNSRecord, error) {
   291  	// add DNS name
   292  	awsDNSRecord, err := p.client.CreateDNSRecords(masterIPs)
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  
   297  	return &DNSRecord{Name: awsDNSRecord.Name, Values: awsDNSRecord.Values}, nil
   298  }
   299  
   300  func (p awsProvisioner) RemoveDNS(dnsRecord *DNSRecord) error {
   301  	err := p.client.DeleteDNSRecords(&aws.DNSRecord{Name: dnsRecord.Name, Values: dnsRecord.Values})
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	return nil
   307  }
   308  
   309  type packetProvisioner struct {
   310  	sshMachineProvisioner
   311  	client packet.Client
   312  }
   313  
   314  func packetClientFromEnv() (infrastructureProvisioner, bool) {
   315  	token := os.Getenv("PACKET_AUTH_TOKEN")
   316  	projectID := os.Getenv("PACKET_PROJECT_ID")
   317  	if token == "" || projectID == "" {
   318  		return nil, false
   319  	}
   320  	p := packetProvisioner{
   321  		client: packet.Client{
   322  			Token:     token,
   323  			ProjectID: projectID,
   324  		},
   325  	}
   326  	p.sshKey = os.Getenv("PACKET_SSH_KEY_PATH")
   327  	if p.sshKey == "" {
   328  		dir, _ := homedir.Dir()
   329  		p.sshKey = filepath.Join(dir, ".ssh", "packet-kismatic-integration-testing.pem")
   330  	}
   331  	return p, true
   332  }
   333  
   334  func (p packetProvisioner) ProvisionNodes(nodeCount NodeCount, distro linuxDistro, _ ...string) (provisionedNodes, error) {
   335  	var packetDistro packet.OS
   336  	switch distro {
   337  	case Ubuntu1604LTS:
   338  		packetDistro = packet.Ubuntu1604LTS
   339  	case CentOS7:
   340  		packetDistro = packet.CentOS7
   341  	default:
   342  		panic(fmt.Sprintf("Used an unsupported distribution: %s", distro))
   343  	}
   344  	// Create all the nodes
   345  	nodes := provisionedNodes{}
   346  	for i := uint16(0); i < nodeCount.Etcd; i++ {
   347  		id, err := p.createNode(packetDistro, i)
   348  		if err != nil {
   349  			return nodes, err
   350  		}
   351  		nodes.etcd = append(nodes.etcd, NodeDeets{id: id})
   352  	}
   353  	for i := uint16(0); i < nodeCount.Master; i++ {
   354  		id, err := p.createNode(packetDistro, i)
   355  		if err != nil {
   356  			return nodes, err
   357  		}
   358  		nodes.master = append(nodes.master, NodeDeets{id: id})
   359  	}
   360  	for i := uint16(0); i < nodeCount.Worker; i++ {
   361  		id, err := p.createNode(packetDistro, i)
   362  		if err != nil {
   363  			return nodes, err
   364  		}
   365  		nodes.worker = append(nodes.worker, NodeDeets{id: id})
   366  	}
   367  	for i := uint16(0); i < nodeCount.Ingress; i++ {
   368  		id, err := p.createNode(packetDistro, i)
   369  		if err != nil {
   370  			return nodes, err
   371  		}
   372  		nodes.ingress = append(nodes.ingress, NodeDeets{id: id})
   373  	}
   374  	for i := uint16(0); i < nodeCount.Storage; i++ {
   375  		id, err := p.createNode(packetDistro, i)
   376  		if err != nil {
   377  			return nodes, err
   378  		}
   379  		nodes.storage = append(nodes.storage, NodeDeets{id: id})
   380  	}
   381  	// Wait until all nodes are ready
   382  	err := p.updateNodeUntilPublicIPAvailable(nodes.etcd)
   383  	if err != nil {
   384  		return nodes, err
   385  	}
   386  	err = p.updateNodeUntilPublicIPAvailable(nodes.master)
   387  	if err != nil {
   388  		return nodes, err
   389  	}
   390  	err = p.updateNodeUntilPublicIPAvailable(nodes.worker)
   391  	if err != nil {
   392  		return nodes, err
   393  	}
   394  	err = p.updateNodeUntilPublicIPAvailable(nodes.ingress)
   395  	if err != nil {
   396  		return nodes, err
   397  	}
   398  	err = p.updateNodeUntilPublicIPAvailable(nodes.storage)
   399  	if err != nil {
   400  		return nodes, err
   401  	}
   402  	return nodes, nil
   403  }
   404  
   405  func (p packetProvisioner) TerminateNodes(nodes provisionedNodes) error {
   406  	allNodes := append(nodes.etcd, nodes.master...)
   407  	allNodes = append(allNodes, nodes.worker...)
   408  	failedDeletes := []string{}
   409  	for _, n := range allNodes {
   410  		if err := p.client.DeleteNode(n.id); err != nil {
   411  			failedDeletes = append(failedDeletes, n.Hostname)
   412  		}
   413  	}
   414  	if len(failedDeletes) > 0 {
   415  		return fmt.Errorf("FAILED TO DELETE THE FOLLOWING NODES ON PACKET: %v", failedDeletes)
   416  	}
   417  	return nil
   418  }
   419  
   420  func (p packetProvisioner) TerminateNode(node NodeDeets) error {
   421  	return p.client.DeleteNode(node.id)
   422  }
   423  
   424  func (p packetProvisioner) ConfigureDNS(masterIPs []string) (*DNSRecord, error) {
   425  	// TODO
   426  	return nil, fmt.Errorf("ConfigureDNS not implemented")
   427  }
   428  
   429  func (p packetProvisioner) RemoveDNS(dnsRecord *DNSRecord) error {
   430  	// TODO
   431  	return fmt.Errorf("RemoveDNS not implemented")
   432  }
   433  
   434  func (p packetProvisioner) createNode(distro packet.OS, count uint16) (string, error) {
   435  	hostname := fmt.Sprintf("kismatic-integration-%d-%d", time.Now().UnixNano(), count)
   436  	node, err := p.client.CreateNode(hostname, distro, nil)
   437  	if err != nil {
   438  		return "", err
   439  	}
   440  	return node.ID, nil
   441  }
   442  
   443  func (p packetProvisioner) updateNodeUntilPublicIPAvailable(nodes []NodeDeets) error {
   444  	for i := range nodes {
   445  		node := &nodes[i]
   446  		nodeDeets, err := p.waitForPublicIP(node.id)
   447  		if err != nil {
   448  			return err
   449  		}
   450  		node.Hostname = nodeDeets.Host
   451  		node.PrivateIP = nodeDeets.PrivateIPv4
   452  		node.PublicIP = nodeDeets.PublicIPv4
   453  		node.SSHUser = nodeDeets.SSHUser
   454  	}
   455  	return nil
   456  }
   457  
   458  func (p packetProvisioner) waitForPublicIP(nodeID string) (*packet.Node, error) {
   459  	for {
   460  		fmt.Printf(".")
   461  		node, err := p.client.GetNode(nodeID)
   462  		if err != nil {
   463  			return nil, err
   464  		}
   465  		if node.PublicIPv4 != "" {
   466  			return node, nil
   467  		}
   468  		time.Sleep(1 * time.Minute)
   469  	}
   470  }
   471  
   472  func waitForSSH(provisionedNodes provisionedNodes, sshKey string) error {
   473  	nodes := provisionedNodes.allNodes()
   474  	for _, n := range nodes {
   475  		if open := WaitUntilSSHOpen(n.PublicIP, n.SSHUser, sshKey, 5*time.Minute); !open {
   476  			return fmt.Errorf("Timed out waiting for SSH at %q", n.PublicIP)
   477  		}
   478  	}
   479  	return nil
   480  }
   481  
   482  func (p packetProvisioner) CreateNFSServers() ([]nfsServer, error) {
   483  	return []nfsServer{}, fmt.Errorf("Packet NFS not yet supported")
   484  }
   485  
   486  func (a awsProvisioner) CreateNFSServers() ([]nfsServer, error) {
   487  	server := a.client.BuildEFSDisk("KismaticIntegrationTestPrimary")
   488  	if server == nil {
   489  		return nil, fmt.Errorf("Could not provision drives")
   490  	}
   491  	return []nfsServer{
   492  		{IpAddress: server.IpAddress},
   493  	}, nil
   494  }