// ListVars lists all environment variables. func ListVars(settings *models.Settings) { helpers.SignIn(settings) envVars := helpers.ListEnvVars(settings) for key, value := range envVars { fmt.Printf("%s=%s\n", key, value) } }
// Status prints out an environment healthcheck. The status of the environment // and every service in the environment is printed out. func Status(settings *models.Settings) { helpers.SignIn(settings) env := helpers.RetrieveEnvironment("pod", settings) fmt.Printf("%s (environment ID = %s):\n", env.Data.Name, env.ID) for _, service := range *env.Data.Services { if service.Type != "utility" { if service.Type == "code" { switch service.Size.(type) { case string: printLegacySizing(&service) default: printNewSizing(&service) } } else { switch service.Size.(type) { case string: sizeString := service.Size.(string) defer fmt.Printf("\t%s (size = %s, image = %s, status = %s) ID: %s\n", service.Label, sizeString, service.Name, service.DeployStatus, service.ID) default: serviceSize := service.Size.(map[string]interface{}) defer fmt.Printf("\t%s (ram = %.0f, storage = %.0f, behavior = %s, type = %s, cpu = %.0f, image = %s, status = %s) ID: %s\n", service.Label, serviceSize["ram"], serviceSize["storage"], serviceSize["behavior"], serviceSize["type"], serviceSize["cpu"], service.Name, service.DeployStatus, service.ID) } } } } }
// SupportIds prints out various IDs related to the associated environment to be // used when contacting Catalyze support at [email protected]. func SupportIds(settings *models.Settings) { helpers.SignIn(settings) fmt.Printf(`EnvironmentID: %s UsersID: %s ServiceID: %s `, settings.EnvironmentID, settings.UsersID, settings.ServiceID) }
// Export dumps all data from a database service and downloads the encrypted // data to the local machine. The export is accomplished by first creating a // backup. Once finished, the CLI asks where the file can be downloaded from. // The file is downloaded, decrypted, and saved locally. func Export(databaseLabel string, filePath string, force bool, settings *models.Settings) { helpers.PHIPrompt() helpers.SignIn(settings) if !force { if _, err := os.Stat(filePath); err == nil { fmt.Printf("File already exists at path '%s'. Specify `--force` to overwrite\n", filePath) os.Exit(1) } } else { os.Remove(filePath) } service := helpers.RetrieveServiceByLabel(databaseLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", databaseLabel) os.Exit(1) } task := helpers.CreateBackup(service.ID, settings) fmt.Printf("Export started (task ID = %s)\n", task.ID) fmt.Print("Polling until export finishes.") ch := make(chan string, 1) go helpers.PollTaskStatus(task.ID, ch, settings) status := <-ch task.Status = status if task.Status != "finished" { fmt.Printf("\nExport finished with illegal status \"%s\", aborting.\n", task.Status) helpers.DumpLogs(service, task, "backup", settings) os.Exit(1) } fmt.Printf("\nEnded in status '%s'\n", task.Status) job := helpers.RetrieveJobFromTaskID(task.ID, settings) fmt.Printf("Downloading export %s\n", job.ID) tempURL := helpers.RetrieveTempURL(job.ID, service.ID, settings) dir, dirErr := ioutil.TempDir("", "") if dirErr != nil { fmt.Println(dirErr.Error()) os.Exit(1) } defer os.Remove(dir) tmpFile, tmpFileErr := ioutil.TempFile(dir, "") if tmpFileErr != nil { fmt.Println(tmpFileErr.Error()) os.Exit(1) } resp, respErr := http.Get(tempURL.URL) if respErr != nil { fmt.Println(respErr.Error()) os.Exit(1) } defer resp.Body.Close() io.Copy(tmpFile, resp.Body) fmt.Println("Decrypting...") tmpFile.Close() helpers.DecryptFile(tmpFile.Name(), job.Backup.Key, job.Backup.IV, filePath) fmt.Printf("%s exported successfully to %s\n", databaseLabel, filePath) helpers.DumpLogs(service, task, "backup", settings) }
// ListUsers lists all users who have access to the associated environment. func ListUsers(settings *models.Settings) { helpers.SignIn(settings) envUsers := helpers.ListEnvironmentUsers(settings) for _, userID := range envUsers.Users { if userID == settings.UsersID { fmt.Printf("%s (you)\n", userID) } else { defer fmt.Printf("%s\n", userID) } } }
// Console opens a secure console to a code or database service. For code // services, a command is required. This command is executed as root in the // context of the application root directory. For database services, no command // is needed - instead, the appropriate command for the database type is run. // For example, for a postgres database, psql is run. func Console(serviceLabel string, command string, settings *models.Settings) { helpers.SignIn(settings) service := helpers.RetrieveServiceByLabel(serviceLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", serviceLabel) os.Exit(1) } fmt.Printf("Opening console to %s (%s)\n", serviceLabel, service.ID) task := helpers.RequestConsole(command, service.ID, settings) fmt.Print("Waiting for the console to be ready. This might take a minute.") ch := make(chan string, 1) go helpers.PollConsoleJob(task.ID, service.ID, ch, settings) jobID := <-ch defer helpers.DestroyConsole(jobID, service.ID, settings) creds := helpers.RetrieveConsoleTokens(jobID, service.ID, settings) creds.URL = strings.Replace(creds.URL, "http", "ws", 1) fmt.Println("Connecting...") // BEGIN websocket impl config, _ := websocket.NewConfig(creds.URL, "ws://localhost:9443/") config.TlsConfig = &tls.Config{ MinVersion: tls.VersionTLS12, } config.Header["X-Console-Token"] = []string{creds.Token} ws, err := websocket.DialConfig(config) if err != nil { panic(err) } defer ws.Close() fmt.Println("Connection opened") stdin, stdout, _ := term.StdStreams() fdIn, isTermIn := term.GetFdInfo(stdin) if !isTermIn { panic(errors.New("StdIn is not a terminal")) } oldState, err := term.SetRawTerminal(fdIn) if err != nil { panic(err) } done := make(chan bool) msgCh := make(chan []byte, 2) go webSocketDaemon(ws, &stdout, done, msgCh) signal.Notify(make(chan os.Signal, 1), os.Interrupt) defer term.RestoreTerminal(fdIn, oldState) go termDaemon(&stdin, ws) <-done }
// ListInvites lists all pending invites for a given environment. After an // invite is accepted, you can manage the users access with the `users` // commands. func ListInvites(settings *models.Settings) { helpers.SignIn(settings) invites := helpers.ListEnvironmentInvites(settings) if len(*invites) == 0 { fmt.Printf("There are no pending invites for %s\n", settings.EnvironmentName) return } fmt.Printf("Pending invites for %s:\n", settings.EnvironmentName) for _, invite := range *invites { fmt.Printf("\t%s %s\n", invite.Email, invite.Code) } }
// DownloadBackup an existing backup to the local machine. The backup is encrypted // throughout the entire journey and then decrypted once it is stored locally. func DownloadBackup(serviceLabel string, backupID string, filePath string, force bool, settings *models.Settings) { helpers.PHIPrompt() helpers.SignIn(settings) if !force { if _, err := os.Stat(filePath); err == nil { fmt.Printf("File already exists at path '%s'. Specify `--force` to overwrite\n", filePath) os.Exit(1) } } else { os.Remove(filePath) } service := helpers.RetrieveServiceByLabel(serviceLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", serviceLabel) os.Exit(1) } job := helpers.RetrieveJob(backupID, service.ID, settings) if job.Type != "backup" || job.Status != "finished" { fmt.Println("Only 'finished' 'backup' jobs may be downloaded") } fmt.Printf("Downloading backup %s\n", backupID) tempURL := helpers.RetrieveTempURL(backupID, service.ID, settings) dir, dirErr := ioutil.TempDir("", "") if dirErr != nil { fmt.Println(dirErr.Error()) os.Exit(1) } defer os.Remove(dir) tmpFile, tmpFileErr := ioutil.TempFile(dir, "") if tmpFileErr != nil { fmt.Println(tmpFileErr.Error()) os.Exit(1) } resp, respErr := http.Get(tempURL.URL) if respErr != nil { fmt.Println(respErr.Error()) os.Exit(1) } defer resp.Body.Close() io.Copy(tmpFile, resp.Body) tmpFile.Close() fmt.Println("Decrypting...") helpers.DecryptFile(tmpFile.Name(), job.Backup.Key, job.Backup.IV, filePath) fmt.Printf("%s backup downloaded successfully to %s\n", serviceLabel, filePath) }
// Import imports data into a database service. The import is accomplished // by encrypting the file locally, requesting a location that it can be uploaded // to, then uploads the file. Once uploaded an automated service processes the // file and acts according to the given parameters. // // The type of file that should be imported depends on the database. For // PostgreSQL and MySQL, this should be a single `.sql` file. For Mongo, this // should be a single tar'ed, gzipped archive (`.tar.gz`) of the database dump // that you want to import. func Import(databaseLabel string, filePath string, mongoCollection string, mongoDatabase string, wipeFirst bool, settings *models.Settings) { helpers.SignIn(settings) if _, err := os.Stat(filePath); os.IsNotExist(err) { fmt.Printf("A file does not exist at path '%s'\n", filePath) os.Exit(1) } service := helpers.RetrieveServiceByLabel(databaseLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", databaseLabel) os.Exit(1) } env := helpers.RetrieveEnvironment("spec", settings) pod := helpers.RetrievePodMetadata(env.PodID, settings) fmt.Printf("Importing '%s' into %s (ID = %s)\n", filePath, databaseLabel, service.ID) key := make([]byte, 32) iv := make([]byte, aes.BlockSize) rand.Read(key) rand.Read(iv) fmt.Println("Encrypting...") encrFilePath := helpers.EncryptFile(filePath, key, iv, pod.ImportRequiresLength) defer os.Remove(encrFilePath) options := map[string]string{} if mongoCollection != "" { options["mongoCollection"] = mongoCollection } if mongoDatabase != "" { options["mongoDatabase"] = mongoDatabase } fmt.Println("Uploading...") tempURL := helpers.RetrieveTempUploadURL(service.ID, settings) task := helpers.InitiateImport(tempURL.URL, encrFilePath, string(helpers.Base64Encode(helpers.Hex(key))), string(helpers.Base64Encode(helpers.Hex(iv))), options, wipeFirst, service.ID, settings) fmt.Printf("Processing import... (task ID = %s)\n", task.ID) ch := make(chan string, 1) go helpers.PollTaskStatus(task.ID, ch, settings) status := <-ch task.Status = status fmt.Printf("\nImport complete (end status = '%s')\n", task.Status) helpers.DumpLogs(service, task, "restore", settings) if task.Status != "finished" { os.Exit(1) } }
// ListBackups lists the created backups for the service sorted from oldest to newest func ListBackups(serviceLabel string, page int, pageSize int, settings *models.Settings) { helpers.SignIn(settings) service := helpers.RetrieveServiceByLabel(serviceLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", serviceLabel) os.Exit(1) } jobs := helpers.ListBackups(service.ID, page, pageSize, settings) sort.Sort(SortedJobs(*jobs)) for _, job := range *jobs { fmt.Printf("%s %s (status = %s)\n", job.ID, job.CreatedAt, job.Status) } if len(*jobs) == pageSize && page == 1 { fmt.Println("(for older backups, try with --page 2 or adjust --page-size)") } if len(*jobs) == 0 && page == 1 { fmt.Println("No backups created yet for this service.") } }
// CreateBackup a new backup func CreateBackup(serviceLabel string, skipPoll bool, settings *models.Settings) { helpers.SignIn(settings) service := helpers.RetrieveServiceByLabel(serviceLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", serviceLabel) os.Exit(1) } task := helpers.CreateBackup(service.ID, settings) fmt.Printf("Backup started (task ID = %s)\n", task.ID) if !skipPoll { fmt.Print("Polling until backup finishes.") ch := make(chan string, 1) go helpers.PollTaskStatus(task.ID, ch, settings) status := <-ch task.Status = status fmt.Printf("\nEnded in status '%s'\n", task.Status) helpers.DumpLogs(service, task, "backup", settings) if task.Status != "finished" { os.Exit(1) } } }
// SetVar adds a new environment variables or updates the value of an existing // environment variables. Any changes to environment variables will not take // effect until the service is redeployed by pushing new code or via // `catalyze redeploy`. func SetVar(variables []string, settings *models.Settings) { helpers.SignIn(settings) envVars := helpers.ListEnvVars(settings) envVarsMap := make(map[string]string, len(variables)) for _, envVar := range variables { pieces := strings.Split(envVar, "=") if len(pieces) != 2 { fmt.Printf("Invalid variable format. Expected <key>=<value> but got %s\n", envVar) os.Exit(1) } envVarsMap[pieces[0]] = pieces[1] } for key := range envVarsMap { if _, ok := envVars[key]; ok { helpers.UnsetEnvVar(key, settings) } } helpers.SetEnvVars(envVarsMap, settings) fmt.Println("Set.") }
// Redeploy offers a way of deploying a service without having to do a git push // first. The same version of the currently running service is deployed with // no changes. func Redeploy(settings *models.Settings) { helpers.SignIn(settings) fmt.Printf("Redeploying %s\n", settings.ServiceID) helpers.RedeployService(settings) fmt.Println("Redeploy successful! Check the status and logs for updates") }
// Metrics prints out metrics for a given service or if the service is not // specified, metrics for the entire environment are printed. func Metrics(serviceLabel string, jsonFlag bool, csvFlag bool, sparkFlag bool, streamFlag bool, mins int, settings *models.Settings) { if streamFlag && (jsonFlag || csvFlag || mins != 1) { fmt.Println("--stream cannot be used with a custom format and multiple records") os.Exit(1) } var singleRetriever func(mins int, settings *models.Settings) *models.Metrics if serviceLabel != "" { service := helpers.RetrieveServiceByLabel(serviceLabel, settings) if service == nil { fmt.Printf("Could not find a service with the label \"%s\"\n", serviceLabel) os.Exit(1) } settings.ServiceID = service.ID singleRetriever = helpers.RetrieveServiceMetrics } var transformer Transformer redraw := make(chan bool) if jsonFlag { transformer = Transformer{ SingleRetriever: singleRetriever, DataTransformer: &JSONTransformer{}, } } else if csvFlag { buffer := &bytes.Buffer{} transformer = Transformer{ SingleRetriever: singleRetriever, DataTransformer: &CSVTransformer{ HeadersWritten: false, GroupMode: serviceLabel == "", Buffer: buffer, Writer: csv.NewWriter(buffer), }, } } else if sparkFlag { // the spark lines interface stays up until closed by the user, so // we might as well keep updating it as long as it is there streamFlag = true mins = 60 err := ui.Init() if err != nil { fmt.Println(err.Error()) os.Exit(1) } defer ui.Close() ui.UseTheme("helloworld") p := ui.NewPar("PRESS q TO QUIT") p.HasBorder = false p.TextFgColor = ui.Theme().SparklineTitle ui.Body.AddRows( ui.NewRow(ui.NewCol(12, 0, p)), ) transformer = Transformer{ SingleRetriever: singleRetriever, DataTransformer: &SparkTransformer{ Redraw: redraw, SparkLines: make(map[string]*ui.Sparklines), }, } } else { transformer = Transformer{ SingleRetriever: singleRetriever, DataTransformer: &TextTransformer{}, } } transformer.GroupRetriever = helpers.RetrieveEnvironmentMetrics transformer.Stream = streamFlag transformer.GroupMode = serviceLabel == "" transformer.Mins = mins transformer.settings = settings helpers.SignIn(settings) if sparkFlag { go transformer.process() ui.Body.Align() ui.Render(ui.Body) quit := make(chan bool) go maintainSparkLines(redraw, quit) <-quit } else { transformer.process() } }
// RmInvite deletes an invite sent to a user. This invite must not already be // accepted. func RmInvite(inviteID string, settings *models.Settings) { helpers.SignIn(settings) helpers.DeleteInvite(inviteID, settings) fmt.Printf("Invite %s removed\n", inviteID) }
// WhoAmI prints out your user ID. This can be used for adding users to // environments via `catalyze adduser`, removing users from an environment // via `catalyze rmuser`, when contacting Catalyze Support, etc. func WhoAmI(settings *models.Settings) { helpers.SignIn(settings) fmt.Printf("user ID = %s\n", settings.UsersID) }
// RmUser revokes a user's access to the associated environment. The ID of the // user is required which can be found via `catalyze whoami`. func RmUser(usersID string, settings *models.Settings) { helpers.SignIn(settings) helpers.RemoveUserFromEnvironment(usersID, settings) fmt.Println("Removed.") }
// Associate an environment so that commands can be run against it. This command // no longer adds a git remote. See commands.AddRemote(). func Associate(envLabel string, serviceLabel string, alias string, remote string, defaultEnv bool, settings *models.Settings) { if _, err := os.Stat(".git"); os.IsNotExist(err) { fmt.Println("Not git repo found in the current directory") os.Exit(1) } helpers.SignIn(settings) envs := helpers.ListEnvironments("pod", settings) for _, env := range *envs { if env.Data.Name == envLabel { if env.State == "defined" { fmt.Printf("Your environment is not yet provisioned. Please visit https://dashboard.catalyze.io/environments/update/%s to finish provisioning your environment\n", env.ID) return } // would be nice to have some sort of global filter() function var chosenService models.Service if serviceLabel != "" { labels := []string{} for _, service := range *env.Data.Services { if service.Type == "code" { labels = append(labels, service.Label) if service.Label == serviceLabel { chosenService = service break } } } if chosenService.Type == "" { fmt.Printf("No code service found with name '%s'. Code services found: %s\n", serviceLabel, strings.Join(labels, ", ")) os.Exit(1) } } else { for _, service := range *env.Data.Services { if service.Type == "code" { chosenService = service break } } if chosenService.Type == "" { fmt.Printf("No code service found for \"%s\" environment (ID = %s)\n", envLabel, settings.EnvironmentID) os.Exit(1) } } for _, r := range helpers.ListGitRemote() { if r == remote { helpers.RemoveGitRemote(remote) break } } helpers.AddGitRemote(remote, chosenService.Source) fmt.Printf("\"%s\" remote added.\n", remote) dir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { panic(err) } name := alias if name == "" { name = envLabel } settings.Environments[name] = models.AssociatedEnv{ EnvironmentID: env.ID, ServiceID: chosenService.ID, Directory: dir, Name: envLabel, } if defaultEnv { settings.Default = name } config.DropBreadcrumb(name, settings) config.SaveSettings(settings) if len(settings.Environments) > 1 && settings.Default == "" { fmt.Printf("You now have %d environments associated. Consider running \"catalyze default ENV_NAME\" to set a default\n", len(settings.Environments)) } return } } fmt.Printf("No environment with label \"%s\" found\n", envLabel) os.Exit(1) }
// Logs is a way to stream logs from Kibana to your local terminal. This is // useful because Kibana is hard to look at because it splits every single // log statement into a separate block that spans multiple lines so it's // not very cohesive. This is intended to be similar to the `heroku logs` // command. func Logs(queryString string, tail bool, hours int, minutes int, seconds int, settings *models.Settings) { if settings.Username == "" || settings.Password == "" { // sometimes this will be filled in from env variables // if it is, just use that and don't prompt them settings.Username = "" settings.Password = "" fmt.Println("Please enter your logging dashboard credentials") } // if we remove the session token, the CLI will prompt for the // username/password normally. It will also set the username/password // on the settings object. sessionToken := settings.SessionToken settings.SessionToken = "" helpers.SignIn(settings) env := helpers.RetrieveEnvironment("pod", settings) var domain = env.Data.DNSName if domain == "" { domain = fmt.Sprintf("%s.catalyze.io", env.Data.Namespace) } urlString := fmt.Sprintf("https://%s/__es", domain) offset := time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second timestamp := time.Now().In(time.UTC).Add(-1 * offset) from := 0 query := &models.LogQuery{ Fields: []string{"@timestamp", "message"}, Query: &models.Query{ Wildcard: map[string]string{ "message": queryString, }, }, Filter: &models.FilterRange{ Range: &models.RangeTimestamp{ Timestamp: map[string]string{ "gt": fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ", timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), timestamp.Minute(), timestamp.Second()), }, }, }, Sort: &models.LogSort{ Timestamp: map[string]string{ "order": "asc", }, Message: map[string]string{ "order": "asc", }, }, From: from, Size: 50, } var tr = &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, } client := &http.Client{ Transport: tr, } settings.SessionToken = sessionToken config.SaveSettings(settings) fmt.Println(" @timestamp - message") for { query.From = from b, err := json.Marshal(*query) if err != nil { panic(err) } reader := bytes.NewReader(b) req, _ := http.NewRequest("GET", fmt.Sprintf("%s/_search", urlString), reader) req.SetBasicAuth(settings.Username, settings.Password) resp, err := client.Do(req) if err != nil { fmt.Println(err.Error()) } respBody, _ := ioutil.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { fmt.Println(fmt.Errorf("%d %s", resp.StatusCode, string(respBody)).Error()) os.Exit(1) } var logs models.Logs json.Unmarshal(respBody, &logs) for _, lh := range *logs.Hits.Hits { fmt.Printf("%s - %s\n", lh.Fields["@timestamp"][0], lh.Fields["message"][0]) } if !tail { break } time.Sleep(2 * time.Second) from += len(*logs.Hits.Hits) } }
// Rake executes a rake task. This is only applicable for ruby-based // applications. func Rake(taskName string, settings *models.Settings) { helpers.SignIn(settings) fmt.Printf("Executing Rake task: %s\n", taskName) helpers.InitiateRakeTask(taskName, settings) fmt.Println("Rake task output viewable in your logging server") }
// AddUser grants a user access to the associated environment. The ID of the // user is required which can be found via `catalyze whoami`. func AddUser(usersID string, settings *models.Settings) { fmt.Println("WARNING: This command is deprecated. Please use \"catalyze invites send\" instead.") helpers.SignIn(settings) helpers.AddUserToEnvironment(usersID, settings) fmt.Println("Added.") }
// InviteUser invites a user by email to the associated environment. They do // not need a Dashboard account prior to inviting them, but they must have a // Dashboard account in order to accept the invitation. func InviteUser(email string, settings *models.Settings) { helpers.YesNoPrompt(fmt.Sprintf("Are you sure you want to invite %s to your %s environment? (y/n) ", email, settings.EnvironmentName)) helpers.SignIn(settings) helpers.CreateInvite(email, settings) fmt.Printf("%s has been invited!\n", email) }
// UnsetVar deletes an environment variable. Any changes to environment variables // will not take effect until the service is redeployed by pushing new code // or via `catalyze redeploy`. func UnsetVar(variable string, settings *models.Settings) { helpers.SignIn(settings) helpers.UnsetEnvVar(variable, settings) fmt.Println("Unset.") }