예제 #1
0
파일: rack.go 프로젝트: gmelika/rack
func cmdRackUpdate(c *cli.Context) error {
	versions, err := version.All()
	if err != nil {
		return stdcli.ExitError(err)
	}

	specified := "stable"

	if len(c.Args()) > 0 {
		specified = c.Args()[0]
	}

	version, err := versions.Resolve(specified)
	if err != nil {
		return stdcli.ExitError(err)
	}

	system, err := rackClient(c).UpdateSystem(version.Version)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("Name     %s\n", system.Name)
	fmt.Printf("Status   %s\n", system.Status)
	fmt.Printf("Version  %s\n", system.Version)
	fmt.Printf("Count    %d\n", system.Count)
	fmt.Printf("Type     %s\n", system.Type)

	fmt.Println()
	fmt.Printf("Updating to version: %s\n", version.Version)
	return nil
}
예제 #2
0
파일: apps.go 프로젝트: gmelika/rack
func cmdAppParams(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	params, err := rackClient(c).ListParameters(app)
	if err != nil {
		return stdcli.ExitError(err)
	}

	keys := []string{}

	for key, _ := range params {
		keys = append(keys, key)
	}

	sort.Strings(keys)

	t := stdcli.NewTable("NAME", "VALUE")

	for _, key := range keys {
		t.AddRow(key, params[key])
	}

	t.Print()
	return nil
}
예제 #3
0
파일: run.go 프로젝트: gmelika/rack
func cmdRun(c *cli.Context) error {
	if c.Bool("detach") {
		return cmdRunDetached(c)
	}

	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) < 2 {
		stdcli.Usage(c, "run")
		return nil
	}

	ps := c.Args()[0]
	err = validateProcessId(c, app, ps)
	if err != nil {
		return stdcli.ExitError(err)
	}

	args := strings.Join(c.Args()[1:], " ")

	release := c.String("release")

	code, err := runAttached(c, app, ps, args, release)
	if err != nil {
		return stdcli.ExitError(err)
	}

	return cli.NewExitError("", code)
}
예제 #4
0
파일: apps.go 프로젝트: gmelika/rack
func cmdAppParamsSet(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	params := map[string]string{}

	for _, arg := range c.Args() {
		parts := strings.SplitN(arg, "=", 2)

		if len(parts) != 2 {
			return stdcli.ExitError(fmt.Errorf("invalid argument: %s", arg))
		}

		params[parts[0]] = parts[1]
	}

	fmt.Print("Updating parameters... ")

	err = rackClient(c).SetParameters(app, params)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("OK")
	return nil
}
예제 #5
0
파일: ssl.go 프로젝트: gmelika/rack
func cmdSSLList(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) > 0 {
		return stdcli.ExitError(fmt.Errorf("`convox ssl` does not take arguments. Perhaps you meant `convox ssl update`?"))
	}

	if c.Bool("help") {
		stdcli.Usage(c, "")
		return nil
	}

	ssls, err := rackClient(c).ListSSL(app)
	if err != nil {
		return stdcli.ExitError(err)
	}

	t := stdcli.NewTable("TARGET", "CERTIFICATE", "DOMAIN", "EXPIRES")

	for _, ssl := range *ssls {
		t.AddRow(fmt.Sprintf("%s:%d", ssl.Process, ssl.Port), ssl.Certificate, ssl.Domain, humanizeTime(ssl.Expiration))
	}

	t.Print()
	return nil
}
예제 #6
0
파일: releases.go 프로젝트: gmelika/rack
func cmdReleasePromote(c *cli.Context) error {
	if len(c.Args()) < 1 {
		stdcli.Usage(c, "releases promote")
		return nil
	}

	release := c.Args()[0]

	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("Promoting %s... ", release)

	_, err = rackClient(c).PromoteRelease(app, release)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("UPDATING")

	if c.Bool("wait") {
		fmt.Printf("Waiting for stabilization... ")

		if err := waitForReleasePromotion(c, app, release); err != nil {
			return stdcli.ExitError(err)
		}

		fmt.Println("OK")
	}

	return nil
}
예제 #7
0
파일: services.go 프로젝트: gmelika/rack
func cmdServiceURL(c *cli.Context) error {
	if len(c.Args()) != 1 {
		stdcli.Usage(c, "url")
		return nil
	}

	name := c.Args()[0]

	service, err := rackClient(c).GetService(name)
	if err != nil {
		return stdcli.ExitError(err)
	}

	if service.Status == "failed" {
		return stdcli.ExitError(fmt.Errorf("Service failure for %s", service.StatusReason))
	}

	if service.URL == "" {
		return stdcli.ExitError(fmt.Errorf("URL does not exist for %s", service.Name))
	}

	fmt.Printf("%s\n", service.URL)

	return nil
}
예제 #8
0
파일: apps.go 프로젝트: gmelika/rack
func cmdAppCreate(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) > 0 {
		app = c.Args()[0]
	}

	if app == "" {
		return stdcli.ExitError(fmt.Errorf("must specify an app name"))
	}

	fmt.Printf("Creating app %s... ", app)

	_, err = rackClient(c).CreateApp(app)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("CREATING")

	if c.Bool("wait") {
		fmt.Printf("Waiting for %s... ", app)

		if err := waitForAppRunning(c, app); err != nil {
			stdcli.ExitError(err)
		}

		fmt.Println("OK")
	}

	return nil
}
예제 #9
0
파일: login.go 프로젝트: gmelika/rack
func cmdLogin(c *cli.Context) error {
	var host string

	if len(c.Args()) < 1 {
		host = "console.convox.com"
	} else {
		host = c.Args()[0]
	}

	u, err := url.Parse(host)
	if err != nil {
		return stdcli.ExitError(err)
	}

	if u.Host != "" {
		host = u.Host
	}

	password := os.Getenv("CONVOX_PASSWORD")

	if password == "" {
		password = c.String("password")
	}

	if password != "" {
		// password flag
		err = testLogin(host, password, c.App.Version)
	} else {
		// first try current login
		password, err = getLogin(host)
		err = testLogin(host, password, c.App.Version)

		// then prompt for password
		if err != nil {
			password = promptForPassword()
			err = testLogin(host, password, c.App.Version)
		}
	}

	if err != nil {
		if strings.Contains(err.Error(), "401") {
			return stdcli.ExitError(fmt.Errorf("invalid login"))
		} else {
			return stdcli.ExitError(err)
		}
	}

	err = addLogin(host, password)
	if err != nil {
		return stdcli.ExitError(err)
	}

	err = switchHost(host)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("Logged in successfully.")
	return nil
}
예제 #10
0
파일: env.go 프로젝트: gmelika/rack
func cmdEnvList(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) > 0 {
		return stdcli.ExitError(fmt.Errorf("`convox env` does not take arguments. Perhaps you meant `convox env set`?"))
	}

	if c.Bool("help") {
		stdcli.Usage(c, "")
		return nil
	}

	env, err := rackClient(c).GetEnvironment(app)
	if err != nil {
		return stdcli.ExitError(err)
	}

	keys := []string{}

	for key, _ := range env {
		keys = append(keys, key)
	}

	sort.Strings(keys)

	for _, key := range keys {
		fmt.Printf("%s=%s\n", key, env[key])
	}

	return nil
}
예제 #11
0
파일: releases.go 프로젝트: gmelika/rack
func cmdReleaseInfo(c *cli.Context) error {
	if len(c.Args()) < 1 {
		stdcli.Usage(c, "release info")
		return nil
	}

	release := c.Args()[0]

	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	r, err := rackClient(c).GetRelease(app, release)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("Id       %s\n", r.Id)
	fmt.Printf("Build    %s\n", r.Build)
	fmt.Printf("Created  %s\n", r.Created)
	fmt.Printf("Env      ")

	fmt.Println(strings.Replace(r.Env, "\n", "\n         ", -1))
	return nil
}
예제 #12
0
파일: rack.go 프로젝트: gmelika/rack
func cmdRackPs(c *cli.Context) error {
	system, err := rackClient(c).GetSystem()
	if err != nil {
		return stdcli.ExitError(err)
	}

	ps, err := rackClient(c).GetProcesses(system.Name, c.Bool("stats"))
	if err != nil {
		return stdcli.ExitError(err)
	}

	if c.Bool("stats") {
		fm, err := rackClient(c).ListFormation(system.Name)
		if err != nil {
			return stdcli.ExitError(err)
		}

		displayProcessesStats(ps, fm)
		return nil
	}

	displayProcesses(ps)

	return nil
}
예제 #13
0
파일: ssl.go 프로젝트: gmelika/rack
func cmdSSLUpdate(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) < 2 {
		stdcli.Usage(c, "update")
		return nil
	}

	target := c.Args()[0]

	parts := strings.Split(target, ":")

	if len(parts) != 2 {
		return stdcli.ExitError(fmt.Errorf("target must be process:port"))
	}

	fmt.Printf("Updating certificate... ")

	_, err = rackClient(c).UpdateSSL(app, parts[0], parts[1], c.Args()[1])
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("OK")
	return nil
}
예제 #14
0
파일: rack.go 프로젝트: gmelika/rack
func cmdRackParams(c *cli.Context) error {
	system, err := rackClient(c).GetSystem()
	if err != nil {
		return stdcli.ExitError(err)
	}

	params, err := rackClient(c).ListParameters(system.Name)
	if err != nil {
		return stdcli.ExitError(err)
	}

	keys := []string{}

	for key, _ := range params {
		keys = append(keys, key)
	}

	sort.Strings(keys)

	t := stdcli.NewTable("NAME", "VALUE")

	for _, key := range keys {
		t.AddRow(key, params[key])
	}

	t.Print()
	return nil
}
예제 #15
0
파일: instances.go 프로젝트: gmelika/rack
func cmdInstancesList(c *cli.Context) error {
	if len(c.Args()) > 0 {
		return stdcli.ExitError(fmt.Errorf("`convox instances` does not take arguments. Perhaps you meant `convox instances ssh`?"))
	}

	if c.Bool("help") {
		stdcli.Usage(c, "")
		return nil
	}

	instances, err := rackClient(c).GetInstances()
	if err != nil {
		return stdcli.ExitError(err)
	}

	t := stdcli.NewTable("ID", "AGENT", "STATUS", "STARTED", "PS", "CPU", "MEM")

	for _, i := range instances {
		agent := "off"
		if i.Agent {
			agent = "on"
		}

		t.AddRow(i.Id, agent, i.Status,
			humanizeTime(i.Started),
			strconv.Itoa(i.Processes),
			fmt.Sprintf("%0.2f%%", i.Cpu*100),
			fmt.Sprintf("%0.2f%%", i.Memory*100))
	}

	t.Print()
	return nil
}
예제 #16
0
파일: ps.go 프로젝트: gmelika/rack
func cmdPsInfo(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) != 1 {
		stdcli.Usage(c, "info")
		return nil
	}

	id := c.Args()[0]

	p, err := rackClient(c).GetProcess(app, id)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("Id       %s\n", p.Id)
	fmt.Printf("Name     %s\n", p.Name)
	fmt.Printf("Release  %s\n", p.Release)
	fmt.Printf("CPU      %0.2f%%\n", p.Cpu)
	fmt.Printf("Memory   %0.2f%%\n", p.Memory*100)
	fmt.Printf("Started  %s\n", humanizeTime(p.Started))
	fmt.Printf("Command  %s\n", p.Command)

	return nil
}
예제 #17
0
파일: ps.go 프로젝트: gmelika/rack
func cmdPs(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	ps, err := rackClient(c).GetProcesses(app, c.Bool("stats"))
	if err != nil {
		return stdcli.ExitError(err)
	}

	if c.Bool("stats") {
		fm, err := rackClient(c).ListFormation(app)
		if err != nil {
			return stdcli.ExitError(err)
		}

		displayProcessesStats(ps, fm)
		return nil
	}

	displayProcesses(ps)

	return nil
}
예제 #18
0
파일: update.go 프로젝트: gmelika/rack
func cmdUpdate(c *cli.Context) error {
	client, err := updateClient()
	if err != nil {
		return stdcli.ExitError(err)
	}

	stdcli.Spinner.Prefix = "Updating convox/proxy: "
	stdcli.Spinner.Start()

	if err := updateProxy(); err != nil {
		fmt.Printf("\x08\x08FAILED\n")
	} else {
		fmt.Printf("\x08\x08OK\n")
	}

	stdcli.Spinner.Stop()

	stdcli.Spinner.Prefix = "Updating convox: "
	stdcli.Spinner.Start()

	opts := equinox.Options{
		CurrentVersion: Version,
		Channel:        "stable",
		HTTPClient:     client,
	}
	if err := opts.SetPublicKeyPEM(publicKey); err != nil {
		return stdcli.ExitError(err)
	}

	// check for update
	r, err := equinox.Check("app_i8m2L26DxKL", opts)
	switch {
	case err == equinox.NotAvailableErr:
		fmt.Println("\x08\x08Already up to date")
		return nil
	case err != nil:
		return stdcli.ExitError(err)
	}

	// apply update
	err = r.Apply()
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("\x08\x08OK, %s\n", r.ReleaseVersion)
	stdcli.Spinner.Stop()

	return nil
}
예제 #19
0
파일: proxy.go 프로젝트: gmelika/rack
func cmdProxy(c *cli.Context) error {
	if len(c.Args()) == 0 {
		stdcli.Usage(c, "proxy")
		return nil
	}

	for _, arg := range c.Args() {
		parts := strings.SplitN(arg, ":", 3)

		var host string
		var port, hostport int

		switch len(parts) {
		case 2:
			host = parts[0]

			p, err := strconv.Atoi(parts[1])
			if err != nil {
				return stdcli.ExitError(err)
			}

			port = p
			hostport = p
		case 3:
			host = parts[1]

			p, err := strconv.Atoi(parts[0])
			if err != nil {
				return stdcli.ExitError(err)
			}

			port = p

			p, err = strconv.Atoi(parts[2])

			if err != nil {
				return stdcli.ExitError(err)
			}

			hostport = p
		default:
			return stdcli.ExitError(fmt.Errorf("invalid argument: %s", arg))
		}

		go proxy("127.0.0.1", port, host, hostport, rackClient(c))
	}

	// block forever
	select {}
}
예제 #20
0
파일: services.go 프로젝트: gmelika/rack
func cmdServiceInfo(c *cli.Context) error {
	if len(c.Args()) != 1 {
		stdcli.Usage(c, "info")
		return nil
	}

	name := c.Args()[0]

	service, err := rackClient(c).GetService(name)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("Name    %s\n", service.Name)
	fmt.Printf("Status  %s\n", service.Status)

	if service.Status == "failed" {
		fmt.Printf("Reason  %s\n", service.StatusReason)
	}

	if len(service.Exports) > 0 {
		fmt.Printf("Exports\n")

		for key, value := range service.Exports {
			fmt.Printf("  %s: %s\n", key, value)
		}
	} else if service.URL != "" {
		// NOTE: this branch is deprecated
		fmt.Printf("URL     %s\n", service.URL)
	}

	return nil
}
예제 #21
0
파일: init.go 프로젝트: gmelika/rack
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.ExitError(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)
}
예제 #22
0
파일: rack.go 프로젝트: gmelika/rack
func cmdRackScale(c *cli.Context) error {
	// initialize to invalid values that indicate no change
	count := -1
	typ := ""

	if c.IsSet("count") {
		count = c.Int("count")
	}

	if c.IsSet("type") {
		typ = c.String("type")
	}

	// validate no argument
	switch len(c.Args()) {
	case 0:
		if count == -1 && typ == "" {
			displaySystem(c)
			return nil
		}
		// fall through to scale API call
	default:
		stdcli.Usage(c, "scale")
		return nil
	}

	_, err := rackClient(c).ScaleSystem(count, typ)
	if err != nil {
		return stdcli.ExitError(err)
	}

	displaySystem(c)
	return nil
}
예제 #23
0
파일: registries.go 프로젝트: gmelika/rack
func cmdRegistryAdd(c *cli.Context) error {
	if len(c.Args()) < 1 {
		stdcli.Usage(c, "add")
		return nil
	}

	server := c.Args()[0]
	username := c.String("username")
	password := c.String("password")
	email := c.String("email")

	if username == "" {
		username = promptForUsername()
	}

	if password == "" {
		password = promptForPassword()
	}

	_, err := rackClient(c).AddRegistry(server, username, password, email)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("Done.")
	return nil
}
예제 #24
0
파일: logs.go 프로젝트: gmelika/rack
func cmdLogsStream(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) > 0 {
		return stdcli.ExitError(fmt.Errorf("`convox logs` does not take arguments. Perhaps you meant `convox logs`?"))
	}

	err = rackClient(c).StreamAppLogs(app, c.String("filter"), c.BoolT("follow"), c.Duration("since"), os.Stdout)
	if err != nil {
		return stdcli.ExitError(err)
	}
	return nil
}
예제 #25
0
파일: rack.go 프로젝트: gmelika/rack
func cmdRackLogs(c *cli.Context) error {
	err := rackClient(c).StreamRackLogs(c.String("filter"), c.BoolT("follow"), c.Duration("since"), os.Stdout)
	if err != nil {
		return stdcli.ExitError(err)
	}

	return nil
}
예제 #26
0
파일: rack.go 프로젝트: gmelika/rack
func cmdRackReleases(c *cli.Context) error {
	system, err := rackClient(c).GetSystem()
	if err != nil {
		return stdcli.ExitError(err)
	}

	pendingVersion := system.Version

	releases, err := rackClient(c).GetSystemReleases()
	if err != nil {
		return stdcli.ExitError(err)
	}

	t := stdcli.NewTable("VERSION", "UPDATED", "STATUS")

	for i, r := range releases {
		status := ""

		if system.Status == "updating" && i == 0 {
			pendingVersion = r.Id
			status = "updating"
		}

		if system.Version == r.Id {
			status = "active"
		}

		t.AddRow(r.Id, humanizeTime(r.Created), status)
	}

	t.Print()

	next, err := version.Next(system.Version)
	if err != nil {
		return stdcli.ExitError(err)
	}

	if next > pendingVersion {
		// if strings.Compare(next, pendingVersion) == 1 {
		fmt.Println()
		fmt.Printf("New version available: %s\n", next)
	}

	return nil
}
예제 #27
0
파일: builds.go 프로젝트: gmelika/rack
func cmdBuildsCopy(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) != 2 {
		stdcli.Usage(c, "copy")
		return nil
	}

	build := c.Args()[0]
	destApp := c.Args()[1]

	fmt.Print("Copying build... ")

	b, err := rackClient(c).CopyBuild(app, build, destApp)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("OK")

	releaseID, err := finishBuild(c, destApp, b)
	if err != nil {
		return stdcli.ExitError(err)
	}

	if releaseID != "" {
		if c.Bool("promote") {
			fmt.Printf("Promoting %s %s... ", destApp, releaseID)

			_, err = rackClient(c).PromoteRelease(destApp, releaseID)
			if err != nil {
				return stdcli.ExitError(err)
			}

			fmt.Println("OK")
		} else {
			fmt.Printf("To deploy this copy run `convox releases promote %s --app %s`\n", releaseID, destApp)
		}
	}

	return nil
}
예제 #28
0
파일: instances.go 프로젝트: gmelika/rack
func cmdInstancesKeyroll(c *cli.Context) error {
	err := rackClient(c).InstanceKeyroll()
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("Rebooting instances")
	return nil
}
예제 #29
0
파일: deploy.go 프로젝트: gmelika/rack
func cmdDeploy(c *cli.Context) error {
	wd := "."

	if len(c.Args()) > 0 {
		wd = c.Args()[0]
	}

	dir, app, err := stdcli.DirApp(c, wd)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Printf("Deploying %s\n", app)

	a, err := rackClient(c).GetApp(app)
	if err != nil {
		return stdcli.ExitError(err)
	}

	switch a.Status {
	case "creating":
		return stdcli.ExitError(fmt.Errorf("app is still creating: %s", app))
	case "running", "updating":
	default:
		return stdcli.ExitError(fmt.Errorf("unable to build app: %s", app))
	}

	// build
	release, err := executeBuild(c, dir, app, c.String("file"), c.String("description"))
	if err != nil {
		return stdcli.ExitError(err)
	}

	if release == "" {
		return nil
	}

	fmt.Printf("Promoting %s... ", release)

	_, err = rackClient(c).PromoteRelease(app, release)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("UPDATING")

	if c.Bool("wait") {
		fmt.Printf("Waiting for %s... ", release)

		if err := waitForReleasePromotion(c, app, release); err != nil {
			return stdcli.ExitError(err)
		}

		fmt.Println("OK")
	}

	return nil
}
예제 #30
0
파일: env.go 프로젝트: gmelika/rack
func cmdEnvUnset(c *cli.Context) error {
	_, app, err := stdcli.DirApp(c, ".")
	if err != nil {
		return stdcli.ExitError(err)
	}

	if len(c.Args()) == 0 {
		return stdcli.ExitError(errors.New("No variable specified"))
	}

	if len(c.Args()) > 1 {
		return stdcli.ExitError(errors.New("Only 1 variable can be unset at a time"))
	}

	key := c.Args()[0]

	fmt.Print("Updating environment... ")

	_, releaseID, err := rackClient(c).DeleteEnvironment(app, key)
	if err != nil {
		return stdcli.ExitError(err)
	}

	fmt.Println("OK")

	if releaseID != "" {
		if c.Bool("promote") {
			fmt.Printf("Promoting %s... ", releaseID)

			_, err = rackClient(c).PromoteRelease(app, releaseID)
			if err != nil {
				return stdcli.ExitError(err)
			}

			fmt.Println("OK")
		} else {
			fmt.Printf("To deploy these changes run `convox releases promote %s`\n", releaseID)
		}
	}

	return nil
}