func cmdInit(c *cli.Context) error { ep := stdcli.QOSEventProperties{Start: time.Now()} distinctId, err := currentId() if err != nil { stdcli.QOSEventSend("cli-init", distinctId, stdcli.QOSEventProperties{Error: err}) } wd := "." if len(c.Args()) > 0 { wd = c.Args()[0] } dir, _, err := stdcli.DirApp(c, wd) if err != nil { return stdcli.QOSEventSend("cli-init", distinctId, stdcli.QOSEventProperties{Error: err}) } // TODO parse the Dockerfile and build a docker-compose.yml if exists("docker-compose.yml") { return stdcli.Error(fmt.Errorf("Cannot initialize a project that already contains a docker-compose.yml")) } err = initApplication(dir) if err != nil { return stdcli.QOSEventSend("cli-init", distinctId, stdcli.QOSEventProperties{Error: err}) } return stdcli.QOSEventSend("cli-init", distinctId, ep) }
func waitForAvailability(url string) error { tick := time.Tick(10 * time.Second) timeout := time.After(20 * time.Minute) for { select { case <-tick: fmt.Print(".") client := &http.Client{ Timeout: 2 * time.Second, } _, err := client.Get(url) if err == nil { return nil } case <-timeout: stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: fmt.Errorf("timeout")}) return fmt.Errorf("timeout") } } return fmt.Errorf("unknown error") }
/// validateUserAccess checks for the "AdministratorAccess" policy needed to create a rack. func validateUserAccess(region string, creds *AwsCredentials) error { // this validation need to check for actual permissions somehow and not // just a policy name return nil Iam := iam.New(session.New(), awsConfig(region, creds)) userOutput, err := Iam.GetUser(&iam.GetUserInput{}) if err != nil { if ae, ok := err.(awserr.Error); ok { return fmt.Errorf("%s. See %s", ae.Code(), iamUserURL) } return fmt.Errorf("%s. See %s", err, iamUserURL) } policies, err := Iam.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{ UserName: userOutput.User.UserName, }) if err != nil { if ae, ok := err.(awserr.Error); ok { return fmt.Errorf("%s. See %s", ae.Code(), iamUserURL) } } for _, policy := range policies.AttachedPolicies { if "AdministratorAccess" == *policy.PolicyName { return nil } } msg := fmt.Errorf("Administrator access needed. See %s", iamUserURL) stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: msg}) return stdcli.Error(msg) }
func waitForCompletion(stack string, CloudFormation *cloudformation.CloudFormation, isDeleting bool) (string, error) { for { dres, err := CloudFormation.DescribeStacks(&cloudformation.DescribeStacksInput{ StackName: aws.String(stack), }) if err != nil { return "", err } err = displayProgress(stack, CloudFormation, isDeleting) if err != nil { return "", err } if len(dres.Stacks) != 1 { return "", fmt.Errorf("could not read stack status") } switch *dres.Stacks[0].StackStatus { case "CREATE_COMPLETE": for _, o := range dres.Stacks[0].Outputs { if *o.OutputKey == "Dashboard" { return *o.OutputValue, nil } } stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("could not install stack, contact [email protected] for assistance") case "CREATE_FAILED": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("stack creation failed, contact [email protected] for assistance") case "ROLLBACK_COMPLETE": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("stack creation failed, contact [email protected] for assistance") case "DELETE_COMPLETE": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", nil case "DELETE_FAILED": stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return "", fmt.Errorf("stack deletion failed, contact [email protected] for assistance") } time.Sleep(2 * time.Second) } }
func cmdStart(c *cli.Context) error { // go handleResize() id, err := currentId() if err != nil { stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{Error: err}) } err = dockerTest() if err != nil { return stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ValidationError: err}) } dir, app, err := stdcli.DirApp(c, filepath.Dir(c.String("file"))) if err != nil { return stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ValidationError: err}) } appType := detectApplication(dir) m, err := manifest.LoadFile(c.String("file")) if err != nil { return stdcli.ExitError(err) } if err := m.Shift(c.Int("shift")); err != nil { return stdcli.ExitError(err) } if pcc, err := m.PortConflicts(); err != nil || len(pcc) > 0 { if err == nil { err = fmt.Errorf("ports in use: %v", pcc) } stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ ValidationError: err, AppType: appType, }) return stdcli.ExitError(err) } cache := !c.Bool("no-cache") sync := !c.Bool("no-sync") r := m.Run(dir, app, cache, sync) err = r.Start() if err != nil { return stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ ValidationError: err, AppType: appType, }) } stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ AppType: appType, }) go handleInterrupt(r) return r.Wait() }
func cmdStart(c *cli.Context) error { // go handleResize() var service string var command []string if len(c.Args()) > 0 { service = c.Args()[0] } if len(c.Args()) > 1 { command = c.Args()[1:] } id, err := currentId() if err != nil { stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{Error: err}) } err = dockerTest() if err != nil { stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ValidationError: err}) return stdcli.Error(err) } dir, app, err := stdcli.DirApp(c, filepath.Dir(c.String("file"))) if err != nil { stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ValidationError: err}) return stdcli.Error(err) } appType := detectApplication(dir) m, err := manifest.LoadFile(c.String("file")) if err != nil { return stdcli.Error(err) } errs := m.Validate() if len(errs) > 0 { for _, e := range errs[1:] { stdcli.Error(e) } return stdcli.Error(errs[0]) } if service != "" { _, ok := m.Services[service] if !ok { return stdcli.Error(fmt.Errorf("Service %s not found in manifest", service)) } } if err := m.Shift(c.Int("shift")); err != nil { return stdcli.Error(err) } // one-off commands don't need port validation if len(command) == 0 { if pcc, err := m.PortConflicts(); err != nil || len(pcc) > 0 { if err == nil { err = fmt.Errorf("ports in use: %v", pcc) } stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ ValidationError: err, AppType: appType, }) return stdcli.Error(err) } } cache := !c.Bool("no-cache") sync := !c.Bool("no-sync") r := m.Run(dir, app, manifest.RunOptions{ Cache: cache, Sync: sync, Service: service, Command: command, }) err = r.Start() if err != nil { r.Stop() stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ ValidationError: err, AppType: appType, }) return stdcli.Error(err) } stdcli.QOSEventSend("cli-start", id, stdcli.QOSEventProperties{ AppType: appType, }) // Setup the local "cron jobs" for _, entry := range m.Services { labels := entry.LabelsByPrefix("convox.cron") processName := fmt.Sprintf("%s-%s", app, entry.Name) c := cron.New() for key, value := range labels { p, ok := r.Processes[processName] if !ok { continue } cronjob := models.NewCronJobFromLabel(key, value) rs := strings.NewReplacer("cron(", "0 ", ")", "") // cron pkg first field is in seconds so set to 0 trigger := strings.TrimSuffix(rs.Replace(cronjob.Schedule), " *") // and doesn't recognize year so we trim it c.AddFunc(trigger, func() { cronProccesName := fmt.Sprintf("cron-%s-%04d", cronjob.Name, rand.Intn(9000)) // Replace args with cron specific ones cronArgs := p.GenerateArgs(&manifest.ArgOptions{ Command: cronjob.Command, IgnorePorts: true, Name: cronProccesName, }) done := make(chan error) manifest.RunAsync( r.Output.Stream(cronProccesName), manifest.Docker(append([]string{"run"}, cronArgs...)...), done, ) err := <-done if err != nil { fmt.Printf("error running %s: %s\n", cronProccesName, err.Error()) } }) } c.Start() } go handleInterrupt(r) return r.Wait() }
func cmdUninstall(c *cli.Context) error { ep := stdcli.QOSEventProperties{Start: time.Now()} distinctId, err := currentId() if err != nil { return stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) } if len(c.Args()) != 2 && len(c.Args()) != 3 { stdcli.Usage(c, "uninstall") return nil } rackName := c.Args()[0] region := c.Args()[1] credentialsFile := "" if len(c.Args()) == 3 { credentialsFile = c.Args()[2] } fmt.Println(Banner) creds, err := readCredentials(credentialsFile) if err != nil { return stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) } if creds == nil { return stdcli.Error(fmt.Errorf("error reading credentials")) } CF := cloudformation.New(session.New(), awsConfig(region, creds)) S3 := s3.New(session.New(), awsConfig(region, creds)) stacks, err := describeRackStacks(rackName, distinctId, CF) if err != nil { return stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) } // verify that rack was detected if len(stacks.Rack) == 0 || stacks.Rack[0].StackName != rackName { return stdcli.Error(fmt.Errorf("Can not find rack named %s.", rackName)) } fmt.Println("Resources to delete:\n") // display all the services, apps, then rack t := stdcli.NewTable("STACK", "TYPE", "STATUS") for _, s := range stacks.Services { t.AddRow(s.Name, s.Type, s.Status) } for _, s := range stacks.Apps { t.AddRow(s.Name, s.Type, s.Status) } t.AddRow(stacks.Rack[0].Name, "rack", stacks.Rack[0].Status) t.Print() fmt.Println() // verify that no stack is being updated for _, s := range stacks.all() { if strings.HasSuffix(s.Status, "IN_PROGRESS") { return stdcli.Error(fmt.Errorf("Can not uninstall while %s is updating.", s.StackName)) } } // prompt to confirm rack name reader := bufio.NewReader(os.Stdin) if !c.Bool("force") { if terminal.IsTerminal(int(os.Stdin.Fd())) { fmt.Printf("Delete everything? y/N: ") confirm, err := reader.ReadString('\n') if err != nil { return stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) } if strings.TrimSpace(confirm) != "y" { return stdcli.Error(fmt.Errorf("Aborting uninstall.")) } } else { return stdcli.Error(fmt.Errorf("Aborting uninstall. Use the --force for non-interactive uninstall.")) } } fmt.Println("") fmt.Println("Uninstalling Convox...") // CF Stack Delete and Retry could take 30+ minutes. Periodically generate more progress output. go func() { t := time.Tick(2 * time.Minute) for range t { fmt.Println("Uninstalling Convox...") } }() success := true // Delete all Service, App and Rack stacks err = deleteStacks("service", rackName, distinctId, CF) if err != nil { stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) success = false } err = deleteStacks("app", rackName, distinctId, CF) if err != nil { stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) success = false } err = deleteStacks("rack", rackName, distinctId, CF) if err != nil { stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) success = false } // Delete all S3 buckets wg := new(sync.WaitGroup) for _, s := range stacks.Apps { for _, b := range s.Buckets { wg.Add(1) go deleteBucket(b, wg, S3) } } for _, s := range stacks.Rack { for _, b := range s.Buckets { wg.Add(1) go deleteBucket(b, wg, S3) } } wg.Wait() // Clean up ~/.convox host := stacks.Rack[0].Outputs["Dashboard"] if configuredHost, _ := currentHost(); configuredHost == host { err = removeHost() if err != nil { return stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) } } err = removeLogin(host) if err != nil { return stdcli.QOSEventSend("cli-uninstall", distinctId, stdcli.QOSEventProperties{Error: err}) } if success { fmt.Println("Successfully uninstalled.") } else { return stdcli.Error(fmt.Errorf("Uninstall encountered some errors, contact [email protected] for assistance")) } return stdcli.QOSEventSend("cli-uninstall", distinctId, ep) }
func cmdInstall(c *cli.Context) error { ep := stdcli.QOSEventProperties{Start: time.Now()} region := c.String("region") stackName := c.String("stack-name") awsRegexRules := []string{ //ecr: http://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_CreateRepository.html "(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*", //cloud formation: https://forums.aws.amazon.com/thread.jspa?threadID=118427 "[a-zA-Z][-a-zA-Z0-9]*", } for _, r := range awsRegexRules { rp := regexp.MustCompile(r) matchedStr := rp.FindString(stackName) match := len(matchedStr) == len(stackName) if !match { msg := fmt.Errorf("Stack name '%s' is invalid, must match [a-z0-9-]*", stackName) stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{ValidationError: msg}) return stdcli.Error(msg) } } tenancy := "default" instanceType := c.String("instance-type") if c.Bool("dedicated") { tenancy = "dedicated" if strings.HasPrefix(instanceType, "t2") { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{ValidationError: fmt.Errorf("t2 instance types aren't supported in dedicated tenancy, please set --instance-type.")}) return stdcli.Error(fmt.Errorf("t2 instance types aren't supported in dedicated tenancy, please set --instance-type.")) } } numInstances := c.Int("instance-count") instanceCount := fmt.Sprintf("%d", numInstances) if numInstances <= 2 { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{ValidationError: fmt.Errorf("instance-count must be greater than 2")}) return stdcli.Error(fmt.Errorf("instance-count must be greater than 2")) } var subnet0CIDR, subnet1CIDR, subnet2CIDR string if cidrs := c.String("subnet-cidrs"); cidrs != "" { parts := strings.SplitN(cidrs, ",", 3) if len(parts) < 3 { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{ValidationError: fmt.Errorf("subnet-cidrs must have 3 values")}) return stdcli.Error(fmt.Errorf("subnet-cidrs must have 3 values")) } subnet0CIDR = parts[0] subnet1CIDR = parts[1] subnet2CIDR = parts[2] } var subnetPrivate0CIDR, subnetPrivate1CIDR, subnetPrivate2CIDR string if cidrs := c.String("private-cidrs"); cidrs != "" { parts := strings.SplitN(cidrs, ",", 3) if len(parts) < 3 { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{ValidationError: fmt.Errorf("private-cidrs must have 3 values")}) return stdcli.Error(fmt.Errorf("private-cidrs must have 3 values")) } subnetPrivate0CIDR = parts[0] subnetPrivate1CIDR = parts[1] subnetPrivate2CIDR = parts[2] } var existingVPC string if vpc := c.String("existing-vpc"); vpc != "" { existingVPC = vpc } internetGateway := c.String("internet-gateway") if (existingVPC != "") && (internetGateway == "") { return stdcli.Error(fmt.Errorf("must specify valid Internet Gateway for existing VPC")) } private := "No" if c.Bool("private") || strings.ToLower(os.Getenv("RACK_PRIVATE")) == "yes" || strings.ToLower(os.Getenv("RACK_PRIVATE")) == "true" { private = "Yes" } ami := c.String("ami") key := c.String("key") vpcCIDR := c.String("vpc-cidr") versions, err := version.All() if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: fmt.Errorf("error getting versions: %s", err)}) return stdcli.Error(err) } version, err := versions.Resolve(c.String("version")) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: fmt.Errorf("error resolving version: %s", err)}) return stdcli.Error(err) } versionName := version.Version furl := fmt.Sprintf(formationURL, versionName) fmt.Println(Banner) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } fmt.Printf("Installing Convox (%s)...\n", versionName) if private == "Yes" { fmt.Println("(Private Network Edition)") } reader := bufio.NewReader(os.Stdin) if email := c.String("email"); email != "" { distinctID = email updateId(distinctID) } else if terminal.IsTerminal(int(os.Stdin.Fd())) { fmt.Print("Email Address (optional, to receive project updates): ") email, err := reader.ReadString('\n') if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } if strings.TrimSpace(email) != "" { distinctID = email updateId(email) } } credentialsFile := "" if len(c.Args()) >= 1 { credentialsFile = c.Args()[0] } creds, err := readCredentials(credentialsFile) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: fmt.Errorf("error: %s", err)}) return stdcli.Error(err) } if creds == nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: fmt.Errorf("error reading credentials")}) return stdcli.Error(err) } err = validateUserAccess(region, creds) if err != nil { stdcli.Error(err) } password := c.String("password") if password == "" { password = randomString(30) } CloudFormation := cloudformation.New(session.New(), awsConfig(region, creds)) req := &cloudformation.CreateStackInput{ Capabilities: []*string{aws.String("CAPABILITY_IAM")}, Parameters: []*cloudformation.Parameter{ {ParameterKey: aws.String("Ami"), ParameterValue: aws.String(ami)}, {ParameterKey: aws.String("ClientId"), ParameterValue: aws.String(distinctID)}, {ParameterKey: aws.String("ExistingVpc"), ParameterValue: aws.String(existingVPC)}, {ParameterKey: aws.String("InstanceCount"), ParameterValue: aws.String(instanceCount)}, {ParameterKey: aws.String("InstanceType"), ParameterValue: aws.String(instanceType)}, {ParameterKey: aws.String("InternetGateway"), ParameterValue: aws.String(internetGateway)}, {ParameterKey: aws.String("Key"), ParameterValue: aws.String(key)}, {ParameterKey: aws.String("Password"), ParameterValue: aws.String(password)}, {ParameterKey: aws.String("Private"), ParameterValue: aws.String(private)}, {ParameterKey: aws.String("Tenancy"), ParameterValue: aws.String(tenancy)}, {ParameterKey: aws.String("Version"), ParameterValue: aws.String(versionName)}, {ParameterKey: aws.String("Subnet0CIDR"), ParameterValue: aws.String(subnet0CIDR)}, {ParameterKey: aws.String("Subnet1CIDR"), ParameterValue: aws.String(subnet1CIDR)}, {ParameterKey: aws.String("Subnet2CIDR"), ParameterValue: aws.String(subnet2CIDR)}, {ParameterKey: aws.String("SubnetPrivate0CIDR"), ParameterValue: aws.String(subnetPrivate0CIDR)}, {ParameterKey: aws.String("SubnetPrivate1CIDR"), ParameterValue: aws.String(subnetPrivate1CIDR)}, {ParameterKey: aws.String("SubnetPrivate2CIDR"), ParameterValue: aws.String(subnetPrivate2CIDR)}, {ParameterKey: aws.String("VPCCIDR"), ParameterValue: aws.String(vpcCIDR)}, }, StackName: aws.String(stackName), TemplateURL: aws.String(furl), } if tf := os.Getenv("TEMPLATE_FILE"); tf != "" { dat, err := ioutil.ReadFile(tf) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: fmt.Errorf("error reading template file: %s", tf)}) return stdcli.Error(err) } t := new(bytes.Buffer) if err := json.Compact(t, dat); err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } req.TemplateURL = nil req.TemplateBody = aws.String(t.String()) } res, err := CloudFormation.CreateStack(req) if err != nil { if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() == "AlreadyExistsException" { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(fmt.Errorf("Stack %q already exists. Run `convox uninstall` then try again", stackName)) } } stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } // NOTE: we start making lots of network requests here // so we're just going to return for testability if os.Getenv("AWS_REGION") == "test" { fmt.Println(*res.StackId) return nil } host, err := waitForCompletion(*res.StackId, CloudFormation, false) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } fmt.Println("Waiting for load balancer...") if err := waitForAvailability(fmt.Sprintf("http://%s/", host)); err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } fmt.Println("Logging in...") err = addLogin(host, password) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } err = switchHost(host) if err != nil { stdcli.QOSEventSend("cli-install", distinctID, stdcli.QOSEventProperties{Error: err}) return stdcli.Error(err) } fmt.Println("Success, try `convox apps`") return stdcli.QOSEventSend("cli-install", distinctID, ep) }