mirror of
https://github.com/FlipsideCrypto/convox.git
synced 2026-02-06 10:56:56 +00:00
609 lines
11 KiB
Go
609 lines
11 KiB
Go
package cli
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/convox/convox/pkg/common"
|
|
"github.com/convox/convox/pkg/structs"
|
|
"github.com/convox/convox/sdk"
|
|
"github.com/convox/stdcli"
|
|
"github.com/convox/stdsdk"
|
|
)
|
|
|
|
type rack struct {
|
|
Name string
|
|
Provider string
|
|
Remote bool
|
|
Status string
|
|
Url string
|
|
}
|
|
|
|
func app(c *stdcli.Context) string {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return coalesce(c.String("app"), c.LocalSetting("app"), filepath.Base(wd))
|
|
}
|
|
|
|
func coalesce(ss ...string) string {
|
|
for _, s := range ss {
|
|
if s != "" {
|
|
return s
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func currentHost(c *stdcli.Context) (string, error) {
|
|
if h := os.Getenv("CONVOX_HOST"); h != "" {
|
|
return h, nil
|
|
}
|
|
|
|
if h, _ := c.SettingRead("host"); h != "" {
|
|
return h, nil
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func currentPassword(c *stdcli.Context, host string) (string, error) {
|
|
if pw := os.Getenv("CONVOX_PASSWORD"); pw != "" {
|
|
return pw, nil
|
|
}
|
|
|
|
return c.SettingReadKey("auth", host)
|
|
}
|
|
|
|
func currentEndpoint(c *stdcli.Context) (string, error) {
|
|
if e := os.Getenv("RACK_URL"); e != "" {
|
|
return e, nil
|
|
}
|
|
|
|
host, err := currentHost(c)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if pw := os.Getenv("CONVOX_PASSWORD"); host != "" && pw != "" {
|
|
return fmt.Sprintf("https://convox:%s@%s", url.QueryEscape(pw), host), nil
|
|
}
|
|
|
|
r, err := matchRack(c, currentRack(c, host))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return r.Url, nil
|
|
}
|
|
|
|
func currentRack(c *stdcli.Context, host string) string {
|
|
if r := c.String("rack"); r != "" {
|
|
return r
|
|
}
|
|
|
|
if r := os.Getenv("CONVOX_RACK"); r != "" {
|
|
return r
|
|
}
|
|
|
|
if r := c.LocalSetting("rack"); r != "" {
|
|
return r
|
|
}
|
|
|
|
if r := hostRacks(c)[host]; r != "" {
|
|
return r
|
|
}
|
|
|
|
if r, _ := c.SettingRead("rack"); r != "" {
|
|
return r
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func executableName() string {
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return "convox.exe"
|
|
default:
|
|
return "convox"
|
|
}
|
|
}
|
|
|
|
func generateTempKey() (string, error) {
|
|
data := make([]byte, 1024)
|
|
|
|
if _, err := rand.Read(data); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hash := sha256.Sum256(data)
|
|
|
|
return fmt.Sprintf("tmp/%s", hex.EncodeToString(hash[:])[0:30]), nil
|
|
}
|
|
|
|
func hostRacks(c *stdcli.Context) map[string]string {
|
|
data, err := c.SettingRead("switch")
|
|
if err != nil {
|
|
return map[string]string{}
|
|
}
|
|
|
|
var rs map[string]string
|
|
|
|
if err := json.Unmarshal([]byte(data), &rs); err != nil {
|
|
return map[string]string{}
|
|
}
|
|
|
|
return rs
|
|
}
|
|
|
|
func localRackRunning(c *stdcli.Context) bool {
|
|
rs, err := localRacks(c)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return len(rs) > 0
|
|
}
|
|
|
|
func localRacks(c *stdcli.Context) ([]rack, error) {
|
|
dir, err := c.SettingDirectory("racks")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
return []rack{}, nil
|
|
}
|
|
|
|
subs, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
racks := []rack{}
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.Chdir(wd)
|
|
|
|
for _, sub := range subs {
|
|
if !sub.IsDir() {
|
|
continue
|
|
}
|
|
|
|
tf := filepath.Join(dir, sub.Name(), "main.tf")
|
|
|
|
if _, err := os.Stat(tf); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
os.Chdir(filepath.Dir(tf))
|
|
|
|
data, err := c.Execute("terraform", "output", "-json")
|
|
if err != nil {
|
|
fmt.Printf("err: %+v\n", err)
|
|
continue
|
|
}
|
|
|
|
var output map[string]struct {
|
|
Sensitive bool
|
|
Type string
|
|
Value string
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &output); err != nil {
|
|
fmt.Printf("err: %+v\n", err)
|
|
continue
|
|
}
|
|
|
|
api := ""
|
|
provider := "unknown"
|
|
status := "unknown"
|
|
|
|
if o, ok := output["api"]; ok {
|
|
api = o.Value
|
|
status = "running"
|
|
}
|
|
|
|
if o, ok := output["provider"]; ok {
|
|
provider = o.Value
|
|
}
|
|
|
|
racks = append(racks, rack{
|
|
Name: sub.Name(),
|
|
Provider: strings.TrimSpace(string(provider)),
|
|
Status: status,
|
|
Url: strings.TrimSpace(string(api)),
|
|
})
|
|
}
|
|
|
|
return racks, nil
|
|
}
|
|
|
|
func matchRack(c *stdcli.Context, name string) (*rack, error) {
|
|
rs, err := racks(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
matches := []rack{}
|
|
|
|
for _, r := range rs {
|
|
if r.Name == name {
|
|
return &r, nil
|
|
}
|
|
|
|
if strings.Index(r.Name, name) != -1 {
|
|
matches = append(matches, r)
|
|
}
|
|
}
|
|
|
|
if name == "" {
|
|
switch len(matches) {
|
|
case 0:
|
|
return nil, fmt.Errorf("no racks found")
|
|
case 1:
|
|
return &matches[0], nil
|
|
default:
|
|
return nil, fmt.Errorf("multiple racks detected, use `convox switch` to select one")
|
|
}
|
|
}
|
|
|
|
switch len(matches) {
|
|
case 0:
|
|
return nil, fmt.Errorf("could not find rack: %s", name)
|
|
case 1:
|
|
return &matches[0], nil
|
|
default:
|
|
return nil, fmt.Errorf("ambiguous rack name: %s", name)
|
|
}
|
|
}
|
|
|
|
func racks(c *stdcli.Context) ([]rack, error) {
|
|
rs := []rack{}
|
|
|
|
lrs, err := localRacks(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rs = append(rs, lrs...)
|
|
|
|
rrs, err := remoteRacks(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rs = append(rs, rrs...)
|
|
|
|
sort.Slice(rs, func(i, j int) bool {
|
|
if !rs[i].Remote && rs[j].Remote {
|
|
return true
|
|
} else {
|
|
return rs[i].Name < rs[j].Name
|
|
}
|
|
})
|
|
|
|
return rs, nil
|
|
}
|
|
|
|
func remoteRacks(c *stdcli.Context) ([]rack, error) {
|
|
host, err := currentHost(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if host == "" {
|
|
return []rack{}, nil
|
|
}
|
|
|
|
pw, err := currentPassword(c, host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
remote := fmt.Sprintf("https://convox:%s@%s", url.QueryEscape(pw), host)
|
|
|
|
p, err := sdk.New(remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p.Authenticator = authenticator(c)
|
|
p.Session = currentSession(c)
|
|
|
|
var rs []struct {
|
|
Name string
|
|
Organization struct {
|
|
Name string
|
|
}
|
|
Provider string
|
|
Status string
|
|
}
|
|
|
|
if err := p.Get("/racks", stdsdk.RequestOptions{}, &rs); err != nil {
|
|
if _, ok := err.(AuthenticationError); ok {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
racks := []rack{}
|
|
|
|
for _, r := range rs {
|
|
racks = append(racks, rack{
|
|
Name: fmt.Sprintf("%s/%s", r.Organization.Name, r.Name),
|
|
Provider: r.Provider,
|
|
Remote: true,
|
|
Status: r.Status,
|
|
Url: remote,
|
|
})
|
|
}
|
|
|
|
return racks, nil
|
|
}
|
|
|
|
func requireEnv(vars ...string) (map[string]string, error) {
|
|
env := map[string]string{}
|
|
missing := []string{}
|
|
|
|
for _, k := range vars {
|
|
if v := os.Getenv(k); v != "" {
|
|
env[k] = v
|
|
} else {
|
|
missing = append(missing, k)
|
|
}
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
return nil, fmt.Errorf("required env: %s", strings.Join(missing, ", "))
|
|
}
|
|
|
|
return env, nil
|
|
}
|
|
|
|
func switchRack(c *stdcli.Context, name string) error {
|
|
rs := hostRacks(c)
|
|
|
|
host, err := currentHost(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rs[host] = name
|
|
|
|
data, err := json.MarshalIndent(rs, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.SettingWrite("switch", string(data)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func tag(name, value string) string {
|
|
return fmt.Sprintf("<%s>%s</%s>", name, value, name)
|
|
}
|
|
func terraform(c *stdcli.Context, dir string, env map[string]string, args ...string) error {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Chdir(wd)
|
|
|
|
if err := os.Chdir(dir); err != nil {
|
|
return err
|
|
}
|
|
|
|
signal.Ignore(os.Interrupt)
|
|
defer signal.Reset(os.Interrupt)
|
|
|
|
if err := c.Terminal("terraform", args...); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func terraformEnv(provider string) (map[string]string, error) {
|
|
switch provider {
|
|
case "aws":
|
|
return requireEnv("AWS_DEFAULT_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
|
|
case "azure":
|
|
return requireEnv("ARM_CLIENT_ID", "ARM_CLIENT_SECRET", "ARM_SUBSCRIPTION_ID", "ARM_TENANT_ID")
|
|
case "gcp":
|
|
return requireEnv("GOOGLE_CREDENTIALS", "GOOGLE_PROJECT", "GOOGLE_REGION")
|
|
default:
|
|
return map[string]string{}, nil
|
|
}
|
|
}
|
|
|
|
func terraformOptionVars(dir string, args []string) (map[string]string, error) {
|
|
vars := map[string]string{}
|
|
|
|
vf := filepath.Join(dir, "vars.json")
|
|
|
|
if _, err := os.Stat(vf); !os.IsNotExist(err) {
|
|
data, err := ioutil.ReadFile(vf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &vars); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, arg := range args {
|
|
parts := strings.Split(arg, "=")
|
|
k := strings.TrimSpace(parts[0])
|
|
if v := strings.TrimSpace(parts[1]); v != "" {
|
|
vars[k] = v
|
|
} else {
|
|
delete(vars, k)
|
|
}
|
|
}
|
|
|
|
data, err := json.MarshalIndent(vars, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := ioutil.WriteFile(vf, data, 0600); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return vars, nil
|
|
}
|
|
|
|
func terraformProviderVars(provider string) (map[string]string, error) {
|
|
switch provider {
|
|
case "do":
|
|
env, err := requireEnv("DIGITALOCEAN_ACCESS_ID", "DIGITALOCEAN_SECRET_KEY", "DIGITALOCEAN_TOKEN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vars := map[string]string{
|
|
"access_id": env["DIGITALOCEAN_ACCESS_ID"],
|
|
"secret_key": env["DIGITALOCEAN_SECRET_KEY"],
|
|
"release": "",
|
|
"token": env["DIGITALOCEAN_TOKEN"],
|
|
}
|
|
return vars, nil
|
|
default:
|
|
vars := map[string]string{
|
|
"release": "",
|
|
}
|
|
return vars, nil
|
|
}
|
|
}
|
|
|
|
func terraformTemplateHelpers() template.FuncMap {
|
|
return template.FuncMap{
|
|
"keys": func(h map[string]string) []string {
|
|
ks := []string{}
|
|
for k := range h {
|
|
ks = append(ks, k)
|
|
}
|
|
sort.Strings(ks)
|
|
return ks
|
|
},
|
|
}
|
|
}
|
|
|
|
func terraformWriteTemplate(filename string, params map[string]interface{}) error {
|
|
if source := os.Getenv("CONVOX_TERRAFORM_SOURCE"); source != "" {
|
|
params["Source"] = fmt.Sprintf(source, params["Provider"])
|
|
} else {
|
|
params["Source"] = fmt.Sprintf("github.com/convox/convox//terraform/system/%s", params["Provider"])
|
|
}
|
|
|
|
t, err := template.New("main").Funcs(terraformTemplateHelpers()).Parse(`
|
|
module "system" {
|
|
source = "{{.Source}}"
|
|
|
|
name = "{{.Name}}"
|
|
|
|
{{- range (keys .Vars) }}
|
|
{{.}} = "{{index $.Vars .}}"
|
|
{{- end }}
|
|
}
|
|
|
|
output "api" {
|
|
value = module.system.api
|
|
}
|
|
|
|
output "provider" {
|
|
value = "{{.Provider}}"
|
|
}`,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fd, err := os.Create(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fd.Close()
|
|
|
|
if err := t.Execute(fd, params); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func waitForResourceDeleted(rack sdk.Interface, c *stdcli.Context, resource string) error {
|
|
s, err := rack.SystemGet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
time.Sleep(WaitDuration) // give the stack time to start updating
|
|
|
|
return common.Wait(WaitDuration, 30*time.Minute, 2, func() (bool, error) {
|
|
var err error
|
|
if s.Version <= "20190111211123" {
|
|
_, err = rack.SystemResourceGetClassic(resource)
|
|
} else {
|
|
_, err = rack.SystemResourceGet(resource)
|
|
}
|
|
if err == nil {
|
|
return false, nil
|
|
}
|
|
if strings.Contains(err.Error(), "no such resource") {
|
|
return true, nil
|
|
}
|
|
if strings.Contains(err.Error(), "does not exist") {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
})
|
|
}
|
|
|
|
func waitForResourceRunning(rack sdk.Interface, c *stdcli.Context, resource string) error {
|
|
s, err := rack.SystemGet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
time.Sleep(WaitDuration) // give the stack time to start updating
|
|
|
|
return common.Wait(WaitDuration, 30*time.Minute, 2, func() (bool, error) {
|
|
var r *structs.Resource
|
|
var err error
|
|
|
|
if s.Version <= "20190111211123" {
|
|
r, err = rack.SystemResourceGetClassic(resource)
|
|
} else {
|
|
r, err = rack.SystemResourceGet(resource)
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return r.Status == "running", nil
|
|
})
|
|
}
|