cmd/hub/api/logs.go (268 lines of code) (raw):
// Copyright (c) 2022 EPAM Systems, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build api
package api
import (
"fmt"
"log"
"os"
"strings"
"sync"
"time"
gosocketio "github.com/arkadijs/golang-socketio"
"github.com/logrusorgru/aurora"
"github.com/epam/hubctl/cmd/hub/config"
"github.com/epam/hubctl/cmd/hub/util"
)
type WsMessage struct {
Id string `json:"id"`
Entity string `json:"entity"`
Name string `json:"name"`
Action string `json:"action"`
Success bool `json:"success"`
Logs string `json:"logs"`
}
type Filter struct {
Id string
Entity string
Completed bool
Success bool
}
var opCompletedActions = []string{"onboard", "deploy", "install", "backup", "undeploy", "uninstall", "delete"}
func Logs(selectors []string, exitOnCompletedOperation bool) int {
filters := parseFilters(selectors)
if len(selectors) > 0 && len(filters) == 0 {
msg := fmt.Sprintf("No entities found by %v", selectors)
if config.Force {
config.AggWarnings = false
util.Warn("%s", msg)
} else {
log.Fatalf("%s", msg)
}
}
updates := make(chan WsMessage, 2)
exitCode := make(chan int)
var names sync.Map
key := func(m *WsMessage) string {
return m.Entity + ":" + m.Id
}
reconnects := 0
var connect func()
connect = func() {
onDisconnect := func() {
time.Sleep(1000)
reconnects++
if config.Debug {
log.Printf("Reconnecting (%d)...", reconnects)
}
go connect()
}
ws, err := hubWsSocketIo(
func() {
if config.Verbose && reconnects == 0 {
log.Print("Reading updates from WebSocket...")
}
},
onDisconnect, onDisconnect)
if err != nil {
if reconnects == 0 {
log.Fatalf("Unable to connect Hub WebSocket: %v", err)
} else {
go onDisconnect()
return
}
}
ws.On("change", func(ch *gosocketio.Channel, args []WsMessage) {
m := WsMessage{}
for _, arg := range args {
if arg.Id != "" {
m.Id = arg.Id
}
if arg.Name != "" {
m.Name = arg.Name
}
if arg.Entity != "" {
m.Entity = arg.Entity
m.Action = arg.Action
m.Success = arg.Success
}
if arg.Logs != "" {
m.Logs = arg.Logs
}
if config.Debug {
if !config.Trace && arg.Logs != "" {
arg.Logs = util.TrimColor(util.Wrap(arg.Logs))
}
fmt.Printf("%s\n", aurora.Cyan(fmt.Sprintf("%+v", arg)).Bold().String())
}
}
if m.Id != "" && m.Entity != "" {
k := key(&m)
if m.Name != "" {
names.LoadOrStore(k, m.Name)
} else {
if maybeStr, ok := names.Load(k); ok {
if str, ok := maybeStr.(string); ok {
m.Name = str
}
}
}
}
updates <- m
})
}
connect()
for {
select {
case code := <-exitCode:
return code
case m := <-updates:
if len(filters) > 0 && !filterMatch(filters, &m) {
continue
}
if m.Logs != "" {
os.Stdout.Write([]byte(m.Logs))
if strings.HasSuffix(m.Action, "-update") {
continue
}
}
success := aurora.Green("success").String()
if !m.Success {
success = aurora.Red("fail").String()
}
fmt.Printf("%s %s %s [%s] %s %s %s\n",
aurora.Magenta("===>").Bold().String(),
time.Now().Format("15:04:05"),
aurora.Green(m.Name).String(),
m.Id,
m.Entity,
aurora.Cyan(m.Action).String(),
success)
if exitOnCompletedOperation && (util.Contains(opCompletedActions, m.Action) ||
(m.Entity == "application" && m.Action == "update")) {
exit := true
success := m.Success
if len(filters) > 0 {
markCompletedFilters(filters, &m)
exit, success = allFiltersCompleted(filters)
}
if exit {
if config.Debug {
log.Print("Logs completed, exiting")
}
code := 0
if !success {
code = 2
}
// wait for updates and logs to catch-up
go func() {
time.Sleep(1 * time.Second)
exitCode <- code
}()
}
}
}
}
}
func filterMatch(filters []Filter, msg *WsMessage) bool {
for _, filter := range filters {
if msg.Id == filter.Id && (msg.Entity == "" || msg.Entity == filter.Entity) {
return true
}
}
return false
}
func markCompletedFilters(filters []Filter, msg *WsMessage) {
for i := range filters {
filter := &filters[i]
if msg.Id == filter.Id && (msg.Entity == "" || msg.Entity == filter.Entity) {
filter.Completed = true
filter.Success = msg.Success
}
}
}
func allFiltersCompleted(filters []Filter) (bool, bool) {
success := true
for _, filter := range filters {
success = success && filter.Success
if !filter.Completed {
return false, false
}
}
return true, success
}
func parseFilters(selectors []string) []Filter {
filters := make([]Filter, 0, len(selectors))
if len(selectors) > 0 {
for _, selector := range selectors {
entityKind := "stackInstance"
spec := strings.SplitN(selector, "/", 2)
if len(spec) == 2 {
entityKind = spec[0]
selector = spec[1]
}
ids := []string{}
switch entityKind {
case "cloudAccount":
accounts, err := cloudAccountsBy(selector, false)
if err != nil {
log.Fatalf("Unable to get Cloud Account by `%s`: %v", selector, err)
}
for _, account := range accounts {
ids = append(ids, account.Id)
}
case "environment":
environments, err := environmentsBy(selector)
if err != nil {
log.Fatalf("Unable to get Environment by `%s`: %v", selector, err)
}
for _, environment := range environments {
ids = append(ids, environment.Id)
}
case "stackTemplate":
templates, err := templatesBy(selector)
if err != nil {
log.Fatalf("Unable to get Template by `%s`: %v", selector, err)
}
for _, template := range templates {
ids = append(ids, template.Id)
}
case "stackInstance":
instances, err := stackInstancesBy(selector, "")
if err != nil {
log.Fatalf("Unable to get Stack Instance by `%s`: %v", selector, err)
}
for _, instance := range instances {
ids = append(ids, instance.Id)
}
case "backup":
backups, err := backupsBy(selector)
if err != nil {
log.Fatalf("Unable to get Backup by `%s`: %v", selector, err)
}
for _, backup := range backups {
ids = append(ids, backup.Id)
}
case "application":
applications, err := applicationsBy(selector)
if err != nil {
log.Fatalf("Unable to get Application by `%s`: %v", selector, err)
}
for _, application := range applications {
ids = append(ids, application.Id)
}
default:
log.Fatalf("Unknown entity kind `%s`", entityKind)
}
if len(ids) == 0 {
if config.Force {
if !util.IsUint(selector) {
ids = []string{selector}
}
} else {
log.Fatalf("Entity `%s` by `%s` not found", entityKind, selector)
}
}
for _, id := range ids {
filters = append(filters, Filter{Id: id, Entity: entityKind})
}
}
}
return filters
}