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 }