func (c *AWSCluster) fetchStack() error { stackID := aws.String(c.StackID) c.base.SendLog("Fetching stack") var res *cloudformation.DescribeStacksResult err := c.wrapRequest(func() error { var err error res, err = c.cf.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: stackID, }) return err }) if err != nil { return err } if len(res.Stacks) == 0 { return StackNotFoundError } stack := &res.Stacks[0] if strings.HasPrefix(*stack.StackStatus, "DELETE_") { return StackNotFoundError } c.stack = stack return nil }
func (c *AWSCluster) loadKeyPair(name string) error { keypair, err := loadSSHKey(name) if err != nil { return err } fingerprint, err := awsutil.FingerprintImportedKey(keypair.PrivateKey) if err != nil { return err } res, err := c.ec2.DescribeKeyPairs(&ec2.DescribeKeyPairsRequest{ Filters: []ec2.Filter{ { Name: aws.String("fingerprint"), Values: []string{fingerprint}, }, }, }) if err != nil { return err } if len(res.KeyPairs) == 0 { return errors.New("No matching key found") } c.base.SSHKey = keypair for _, p := range res.KeyPairs { if *p.KeyName == name { c.base.SSHKeyName = name return nil } } c.base.SSHKeyName = *res.KeyPairs[0].KeyName return saveSSHKey(c.base.SSHKeyName, keypair) }
func (c *AWSCluster) Delete() { c.cf = cloudformation.New(c.creds, c.Region, nil) stackEventsSince := time.Now() c.base.setState("deleting") if err := c.fetchStack(); err != StackNotFoundError { if err := c.wrapRequest(func() error { return c.cf.DeleteStack(&cloudformation.DeleteStackInput{ StackName: aws.String(c.StackName), }) }); err != nil { err = fmt.Errorf("Unable to delete stack %s: %s", c.StackName, err) c.base.SendError(err) if !c.base.YesNoPrompt(fmt.Sprintf("%s\nWould you like to remove it from the installer?", err.Error())) { c.base.setState("error") return } } else if err := c.waitForStackCompletion("DELETE", stackEventsSince); err != nil { c.base.SendError(err) } } if err := c.base.MarkDeleted(); err != nil { c.base.SendError(err) } c.base.sendEvent(&Event{ ClusterID: c.base.ID, Type: "cluster_state", Description: "deleted", }) }
func (c *AWSCluster) Delete() { c.cf = cloudformation.New(c.creds, c.Region, nil) stackEventsSince := time.Now() c.base.setState("deleting") if err := c.fetchStack(); err != StackNotFoundError { if err := c.wrapRequest(func() error { return c.cf.DeleteStack(&cloudformation.DeleteStackInput{ StackName: aws.String(c.StackName), }) }); err != nil { c.base.setState("error") c.base.SendError(fmt.Errorf("Unable to delete stack %s: %s", c.StackName, err)) } else { if err := c.waitForStackCompletion("DELETE", stackEventsSince); err != nil { c.base.SendError(err) } } } if err := c.base.MarkDeleted(); err != nil { c.base.SendError(err) } c.base.sendEvent(&Event{ ClusterID: c.base.ID, Type: "cluster_state", Description: "deleted", }) }
func (c *AWSCluster) configureDNS() error { // TODO(jvatic): Run directly after receiving zone create complete stack event c.base.SendLog("Configuring DNS") // Set region to us-east-1, since any other region will fail for global services like Route53 r53 := route53.New(c.creds, "us-east-1", nil) res, err := r53.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(c.DNSZoneID)}) if err != nil { return err } if err := c.base.Domain.Configure(res.DelegationSet.NameServers); err != nil { return err } return nil }
func (c *AWSCluster) waitForStackCompletion(action string, after time.Time) error { stackID := aws.String(c.StackID) const actionCompleteSuffix = "_COMPLETE" const actionFailureSuffix = "_FAILED" const actionInProgressSuffix = "_IN_PROGRESS" isComplete := false isFailed := false var stackEvents []cloudformation.StackEvent var nextToken aws.StringValue var fetchStackEvents func() error fetchStackEvents = func() error { res, err := c.cf.DescribeStackEvents(&cloudformation.DescribeStackEventsInput{ NextToken: nextToken, StackName: stackID, }) if err != nil { switch err.(type) { case *url.Error: return nil default: return err } } // some events are not returned in order sort.Sort(StackEventSort(res.StackEvents)) for _, se := range res.StackEvents { if !se.Timestamp.After(after) { continue } stackEventExists := false for _, e := range stackEvents { if *e.EventID == *se.EventID { stackEventExists = true break } } if stackEventExists { continue } stackEvents = append(stackEvents, se) if se.ResourceType != nil && se.ResourceStatus != nil { if *se.ResourceType == "AWS::CloudFormation::Stack" { if strings.HasSuffix(*se.ResourceStatus, actionInProgressSuffix) && !strings.HasPrefix(*se.ResourceStatus, action) { isFailed = true break } if strings.HasSuffix(*se.ResourceStatus, actionCompleteSuffix) { if strings.HasPrefix(*se.ResourceStatus, action) { isComplete = true } else { isFailed = true } } else if strings.HasSuffix(*se.ResourceStatus, actionFailureSuffix) { isFailed = true } } var desc string if se.ResourceStatusReason != nil { desc = fmt.Sprintf(" (%s)", *se.ResourceStatusReason) } name := *se.ResourceType if se.LogicalResourceID != nil { name = fmt.Sprintf("%s (%s)", name, *se.LogicalResourceID) } c.base.SendLog(fmt.Sprintf("%s\t%s%s", name, *se.ResourceStatus, desc)) } } if res.NextToken != nil { nextToken = res.NextToken fetchStackEvents() } return nil } for { if err := c.wrapRequest(fetchStackEvents); err != nil { return err } if isComplete { break } if isFailed { return fmt.Errorf("Failed to create stack %s", c.StackName) } time.Sleep(1 * time.Second) } return nil }
func (c *AWSCluster) createStack() error { c.base.SendLog("Generating start script") startScript, discoveryToken, err := c.base.genStartScript(c.base.NumInstances, "/dev/xvdb") if err != nil { return err } c.base.DiscoveryToken = discoveryToken if err := c.base.saveField("DiscoveryToken", discoveryToken); err != nil { return err } var stackTemplateBuffer bytes.Buffer err = stackTemplate.Execute(&stackTemplateBuffer, &stackTemplateData{ Instances: make([]struct{}, c.base.NumInstances), DefaultInstanceType: DefaultInstanceType, }) if err != nil { return err } stackTemplateString := stackTemplateBuffer.String() parameters := []cloudformation.Parameter{ { ParameterKey: aws.String("ImageId"), ParameterValue: aws.String(c.ImageID), }, { ParameterKey: aws.String("ClusterDomain"), ParameterValue: aws.String(c.base.Domain.Name), }, { ParameterKey: aws.String("KeyName"), ParameterValue: aws.String(c.base.SSHKeyName), }, { ParameterKey: aws.String("UserData"), ParameterValue: aws.String(startScript), }, { ParameterKey: aws.String("InstanceType"), ParameterValue: aws.String(c.InstanceType), }, { ParameterKey: aws.String("VpcCidrBlock"), ParameterValue: aws.String(c.VpcCIDR), }, { ParameterKey: aws.String("SubnetCidrBlock"), ParameterValue: aws.String(c.SubnetCIDR), }, } stackEventsSince := time.Now() if c.StackID != "" && c.StackName != "" { if err := c.fetchStack(); err == nil && !strings.HasPrefix(*c.stack.StackStatus, "DELETE") { if c.base.YesNoPrompt(fmt.Sprintf("Stack found from previous installation (%s), would you like to delete it? (a new one will be created either way)", c.StackName)) { c.base.SendLog(fmt.Sprintf("Deleting stack %s", c.StackName)) if err := c.wrapRequest(func() error { return c.cf.DeleteStack(&cloudformation.DeleteStackInput{ StackName: aws.String(c.StackName), }) }); err != nil { c.base.SendLog(fmt.Sprintf("Unable to delete stack %s: %s", c.StackName, err)) } } } } c.base.SendLog("Creating stack") var res *cloudformation.CreateStackResult err = c.wrapRequest(func() error { var err error res, err = c.cf.CreateStack(&cloudformation.CreateStackInput{ OnFailure: aws.String("DELETE"), StackName: aws.String(c.StackName), Tags: []cloudformation.Tag{}, TemplateBody: aws.String(stackTemplateString), TimeoutInMinutes: aws.Integer(10), Parameters: parameters, }) return err }) if err != nil { return err } c.StackID = *res.StackID if err := c.saveField("StackID", c.StackID); err != nil { return err } return c.waitForStackCompletion("CREATE", stackEventsSince) }
func (c *AWSCluster) createKeyPair() error { keypairNames := listSSHKeyNames() if c.base.SSHKeyName != "" { newKeypairNames := make([]string, len(keypairNames)+1) newKeypairNames[0] = c.base.SSHKeyName for i, name := range keypairNames { newKeypairNames[i+1] = name } keypairNames = newKeypairNames } for _, name := range keypairNames { if err := c.loadKeyPair(name); err == nil { c.base.SendLog(fmt.Sprintf("Using saved key pair (%s)", c.base.SSHKeyName)) return nil } } keypairName := "flynn" if c.base.SSHKeyName != "" { keypairName = c.base.SSHKeyName } keypair, err := loadSSHKey(keypairName) if err == nil { c.base.SendLog("Importing key pair") } else { c.base.SendLog("Creating key pair") keypair, err = sshkeygen.Generate() if err != nil { return err } } enc := base64.StdEncoding publicKeyBytes := make([]byte, enc.EncodedLen(len(keypair.PublicKey))) enc.Encode(publicKeyBytes, keypair.PublicKey) var res *ec2.ImportKeyPairResult err = c.wrapRequest(func() error { var err error res, err = c.ec2.ImportKeyPair(&ec2.ImportKeyPairRequest{ KeyName: aws.String(keypairName), PublicKeyMaterial: publicKeyBytes, }) return err }) if apiErr, ok := err.(aws.APIError); ok && apiErr.Code == "InvalidKeyPair.Duplicate" { if c.base.YesNoPrompt(fmt.Sprintf("Key pair %s already exists, would you like to delete it?", keypairName)) { c.base.SendLog("Deleting key pair") if err := c.wrapRequest(func() error { return c.ec2.DeleteKeyPair(&ec2.DeleteKeyPairRequest{ KeyName: aws.String(keypairName), }) }); err != nil { return err } return c.createKeyPair() } for { keypairName = c.base.PromptInput("Please enter a new key pair name") if keypairName != "" { c.base.SSHKeyName = keypairName return c.createKeyPair() } } } if err != nil { return err } c.base.SSHKey = keypair c.base.SSHKeyName = *res.KeyName err = saveSSHKey(keypairName, keypair) if err != nil { return err } return nil }
func TestEC2Request(t *testing.T) { var m sync.Mutex var httpReq *http.Request var form url.Values server := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { m.Lock() defer m.Unlock() httpReq = r if err := r.ParseForm(); err != nil { t.Fatal(err) } form = r.Form fmt.Fprintln(w, `<Thing><IpAddress>woo</IpAddress></Thing>`) }, )) defer server.Close() client := aws.EC2Client{ Context: aws.Context{ Service: "animals", Region: "us-west-2", Credentials: aws.Creds( "accessKeyID", "secretAccessKey", "securityToken", ), }, Client: http.DefaultClient, Endpoint: server.URL, APIVersion: "1.1", } req := fakeEC2Request{ PresentString: aws.String("string"), PresentBoolean: aws.True(), PresentInteger: aws.Integer(1), PresentLong: aws.Long(2), PresentDouble: aws.Double(1.2), PresentFloat: aws.Float(2.3), PresentTime: time.Date(2001, 1, 1, 2, 1, 1, 0, time.FixedZone("UTC+1", 3600)), PresentSlice: []string{"one", "two"}, PresentStruct: &EmbeddedStruct{Value: aws.String("v")}, PresentStructSlice: []EmbeddedStruct{ {Value: aws.String("p")}, {Value: aws.String("q")}, }, } var resp fakeEC2Response if err := client.Do("GetIP", "POST", "/", &req, &resp); err != nil { t.Fatal(err) } m.Lock() defer m.Unlock() if v, want := httpReq.Method, "POST"; v != want { t.Errorf("Method was %v but expected %v", v, want) } if httpReq.Header.Get("Authorization") == "" { t.Error("Authorization header is missing") } if v, want := httpReq.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; v != want { t.Errorf("Content-Type was %v but expected %v", v, want) } if v, want := httpReq.Header.Get("User-Agent"), "aws-go"; v != want { t.Errorf("User-Agent was %v but expected %v", v, want) } if err := httpReq.ParseForm(); err != nil { t.Fatal(err) } expectedForm := url.Values{ "Action": []string{"GetIP"}, "Version": []string{"1.1"}, "PresentString": []string{"string"}, "PresentBoolean": []string{"true"}, "PresentInteger": []string{"1"}, "PresentLong": []string{"2"}, "PresentDouble": []string{"1.2"}, "PresentFloat": []string{"2.3"}, "PresentTime": []string{"2001-01-01T01:01:01Z"}, "PresentSlice.1": []string{"one"}, "PresentSlice.2": []string{"two"}, "PresentStruct.Value": []string{"v"}, "PresentStructSlice.1.Value": []string{"p"}, "PresentStructSlice.2.Value": []string{"q"}, } if !reflect.DeepEqual(form, expectedForm) { t.Errorf("Post body was \n%s\n but expected \n%s", form.Encode(), expectedForm.Encode()) } if want := (fakeEC2Response{IPAddress: "woo"}); want != resp { t.Errorf("Response was %#v, but expected %#v", resp, want) } }