cmd/hub/util/util.go (594 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/. package util import ( "context" "encoding/json" "fmt" "io" "log" "os" "path/filepath" "reflect" "sort" "strconv" "strings" "github.com/logrusorgru/aurora" "github.com/mattn/go-isatty" "golang.org/x/text/cases" "golang.org/x/text/language" "github.com/epam/hubctl/cmd/hub/config" ) var ( warnings = make([]string, 0) warningsEmitted = make(map[string]struct{}) HighlightColor = maybeHighlight(aurora.BrightCyan) WarnColor = maybeHighlight(aurora.BrightMagenta) logTerminal *bool ) func maybeHighlight(color func(interface{}) aurora.Value) func(string) string { return func(str string) string { if config.Tty && (IsLogTerminal() || config.TtyForced) { str = color(str).String() } return str } } func IsLogTerminal() bool { if logTerminal != nil { return *logTerminal } fd := os.Stderr.Fd() if config.LogDestination == "stdout" { fd = os.Stdout.Fd() } tty := isatty.IsTerminal(fd) logTerminal = &tty return tty } func Warn(format string, v ...interface{}) { msg := fmt.Sprintf(format, v...) log.Printf(WarnColor("WARN: %s"), msg) if config.AggWarnings { warnings = append(warnings, msg) } } func Debug(format string, v ...interface{}) { if config.Debug { msg := fmt.Sprintf(format, v...) log.Print(msg) } } func Coalesce(values ...string) string { for _, v := range values { if v != "" { return v } } return "" } func WarnOnce(format string, v ...interface{}) { msg := fmt.Sprintf(format, v...) if _, emitted := warningsEmitted[msg]; emitted { return } warningsEmitted[msg] = struct{}{} log.Printf(WarnColor("WARN: %s"), msg) if config.AggWarnings { warnings = append(warnings, msg) } } func PrintAllWarnings() { if !config.AggWarnings || len(warnings) == 0 { return } if config.Verbose { log.Print(WarnColor("All warnings combined:")) } uniq := make([]string, 0, len(warnings)) seen := make(map[string]struct{}) for _, msg := range warnings { if _, emitted := seen[msg]; emitted { continue } seen[msg] = struct{}{} uniq = append(uniq, msg) } io.WriteString(os.Stderr, strings.Join(uniq, "\n")) io.WriteString(os.Stderr, "\n") } func Errors(sep string, maybeErrors ...error) string { if sep == "" { sep = ", " } errs := make([]string, 0, len(maybeErrors)) for _, err := range maybeErrors { if err != nil { errs = append(errs, err.Error()) } } if len(errs) == 0 { return "(no errors)" } return strings.Join(UniqInOrder(errs), sep) } func Errors2(maybeErrors ...error) string { return Errors("", maybeErrors...) } // TODO honour `secure` func askOnTerminal(prompt string, secure bool) string { if !isatty.IsTerminal(os.Stdin.Fd()) { if config.Verbose { log.Printf("Stdin is not a terminal, not asking for `%s`", prompt) } return "" } var input string fmt.Printf("%s: ", prompt) read, err := fmt.Scanln(&input) if read > 0 { if err != nil { log.Fatalf("Error reading input for `%s`: %v (read %d bytes)", prompt, err, read) } else { return input } } return "" } func maybeAskInput(input string, prompt string, secure bool) string { if input != "" { return input } return askOnTerminal(prompt, secure) } func AskInput(input string, prompt string) string { return maybeAskInput(input, prompt, false) } func AskPassword(input string, prompt string) string { return maybeAskInput(input, prompt, true) } func CopyMap2(m map[string][]string) map[string][]string { res := make(map[string][]string) for k, v := range m { v2 := make([]string, len(v)) copy(v2, v) res[k] = v2 } return res } func AppendMapList(m map[string][]string, key, value string) { if list, exist := m[key]; exist { m[key] = append(list, value) } else { m[key] = []string{value} } } func ConcatMaps(m1, m2 map[string]string) map[string]string { if len(m1) == 0 && len(m2) > 0 { return m2 } if len(m2) == 0 && len(m1) > 0 { return m1 } res := make(map[string]string) for k, v := range m1 { res[k] = v } for k, v := range m2 { res[k] = v } return res } func Reverse(source []string) []string { l := len(source) if l < 2 { return source } reversed := make([]string, l) for i, str := range source { reversed[l-i-1] = str } return reversed } func Uniq(source []string) []string { sorted := make([]string, len(source)) copy(sorted, source) sort.Strings(sorted) dest := make([]string, 0, len(source)) prev := "" for _, str := range sorted { if str != prev { dest = append(dest, str) prev = str } } return dest } func UniqInOrder(source []string) []string { result := make([]string, 0, len(source)) seen := make(map[string]struct{}) for _, str := range source { if _, exist := seen[str]; !exist { seen[str] = struct{}{} result = append(result, str) } } return result } func Contains(list []string, value string) bool { for _, v := range list { if v == value { return true } } return false } func ContainsSubstring(list []string, substr string) bool { for _, v := range list { if strings.Contains(v, substr) { return true } } return false } func ContainsPrefix(prefixes []string, value string) bool { for _, prefix := range prefixes { if prefix == value || (strings.HasSuffix(prefix, "*") && strings.HasPrefix(value, prefix[:len(prefix)-1])) || strings.HasPrefix(value, prefix) { return true } } return false } func ContainsAll(list []string, values []string) bool { for _, v := range values { if !Contains(list, v) { return false } } return true } func ContainsAny(list []string, values []string) bool { for _, v := range values { if Contains(list, v) { return true } } return false } func Union(list, list2 []string) []string { res := make([]string, 0) for _, v := range list2 { if Contains(list, v) { res = append(res, v) } } return res } func ContainsAnySubstring(list []string, substrs []string) bool { for _, substr := range substrs { if ContainsSubstring(list, substr) { return true } } return false } func Equal(list []string, list2 []string) bool { return reflect.DeepEqual(list, list2) } func Omit(list []string, value string) []string { if !Contains(list, value) { return list } filtered := make([]string, 0, len(list)) for _, v := range list { if v != value { filtered = append(filtered, v) } } return filtered } func OmitAll(list []string, values []string) []string { filtered := make([]string, 0, len(list)) for _, v := range list { if !Contains(values, v) { filtered = append(filtered, v) } } return filtered } func Filter(list []string, patterns []string) []string { filtered := make([]string, 0, len(list)) for _, v := range list { if ContainsPrefix(patterns, v) { filtered = append(filtered, v) } } return filtered } func FilterNot(list []string, patterns []string) []string { filtered := make([]string, 0, len(list)) for _, v := range list { if !ContainsPrefix(patterns, v) { filtered = append(filtered, v) } } return filtered } func Index(list []string, search string) int { index := -1 if search != "" { for i, value := range list { if search == value { index = i break } } } return index } func SortedKeys(m map[string]string) []string { if len(m) == 0 { return []string{} } keys := make([]string, 0, len(m)) for name := range m { keys = append(keys, name) } sort.Strings(keys) return keys } func SortedKeys2(m map[string][]string) []string { if len(m) == 0 { return []string{} } keys := make([]string, 0, len(m)) for name := range m { keys = append(keys, name) } sort.Strings(keys) return keys } func MergeUnique(lists ...[]string) []string { ll := 0 for _, list := range lists { ll += len(list) } res := make([]string, 0, ll) for _, list := range lists { for _, value := range list { if !Contains(res, value) { res = append(res, value) } } } return res } func Value(values ...string) string { for _, v := range values { if v != "" { return v } } return "" } func Wrap(str string) string { str = strings.Replace(str, "\n", "\\n", -1) if len(str) > 102 { str = str[:100] + "..." } return str } func Empty(value interface{}) bool { if value == nil { return true } if str, ok := value.(string); ok { return str == "" } return false } func String(value interface{}) string { if value == nil { return "" } if str, ok := value.(string); ok { return str } return fmt.Sprintf("%v", value) } func MaybeJson(value interface{}) string { if value == nil { return "" } if str, ok := value.(string); ok { return str } valueBytes, err := json.Marshal(value) if err != nil { if config.Trace { return fmt.Sprintf("(%v)", err) } return "(error)" } return string(valueBytes) } func TrimColor(str string) string { colors := "\x1B[" for strings.Contains(str, colors) { start := strings.Index(str, colors) end := strings.Index(str[start+1:], "m") if end != -1 { end += start + 1 } else { end = start + len(colors) - 1 } if end < len(str) { str = str[:start] + str[end+1:] } else { str = str[:start] } } return str } func Trim(str string) string { cutset := " \r" return strings.Trim(str, cutset) } func NoSuchFile(err error) bool { if err == nil { return false } str := err.Error() return err == os.ErrNotExist || str == "file does not exist" || strings.Contains(str, "no such file or directory") } func ContextCanceled(err error) bool { if err == nil { return false } str := err.Error() return err == context.Canceled || str == "context canceled" || strings.Contains(str, ": context canceled") || strings.Contains(str, ": operation was canceled") } func Plural(size int, noun ...string) string { l := len(noun) if l == 0 { return "" } if size > 1 { if l > 1 { return noun[1] } return fmt.Sprintf("%ss", noun[0]) } return noun[0] } func SplitPaths(paths string) []string { if paths == "" { return []string{} } return strings.Split(paths, ",") } // strip .hub/ .terraform/ etc. func StripDotDirs(path string) string { for { dir := filepath.Base(path) if strings.HasPrefix(dir, ".") && dir != "." && dir != ".." { path = filepath.Dir(path) continue } return path } } func MustAbs(path string) string { abs, err := filepath.Abs(path) if err != nil { log.Fatalf("Unable to convert `%s` to absolute pathname: %v", path, err) } return abs } func Basedir(paths []string) string { for _, path := range paths { if !strings.Contains(path, "://") { if _, err := os.Stat(path); err == nil { return StripDotDirs(filepath.Dir(path)) } } } cwd, err := os.Getwd() if err != nil { log.Fatalf("Unable to determine current working directory: %v", err) } return cwd } func PlainName(name string) string { if name == "" { return "" } i := strings.Index(name, ":") if i > 0 { return name[0:i] } return name } func SplitQName(qName string) (string, string) { name := qName component := "" parts := strings.SplitN(qName, "|", 2) if len(parts) > 1 { name = parts[0] component = parts[1] } return name, component } func initSecretSuffixes() []string { seed := []string{"password", "secret", "key", "cert", "token", "cookie", "kubeconfig"} suffixes := make([]string, 0, len(seed)*4) for _, suf := range seed { suffixes = append(suffixes, "."+suf) suffixes = append(suffixes, "_"+suf) suffixes = append(suffixes, cases.Title(language.Und).String(suf)) suffixes = append(suffixes, strings.ToUpper(suf)) } return suffixes } var secretSuffixes = initSecretSuffixes() var notASecretWhitelist = []string{"cloud.sshKey"} func LooksLikeSecret(name string) bool { i := strings.Index(name, "|") if i > 0 { // a Qname name = name[0:i] } if Contains(notASecretWhitelist, name) { return false } for _, suf := range secretSuffixes { if strings.HasSuffix(name, suf) { return true } } return false } func MaybeMaskedValue(trace bool, name, value string) string { if !trace && LooksLikeSecret(name) { return "(masked)" } return value } func ParseKvList(list string) (map[string]string, error) { parsed := make(map[string]string) if list == "" { return parsed, nil } vars := strings.Split(list, ",") for _, v := range vars { kv := strings.SplitN(v, "=", 2) if len(kv) != 2 { return nil, fmt.Errorf("`%s` cannot be split into key/value pair", v) } parsed[kv[0]] = kv[1] } if config.Debug { log.Print("Parsed:") for k, v := range parsed { log.Printf("\t%s => %s", k, v) } } return parsed, nil } func IsUint(str string) bool { if str == "" { return false } _, err := strconv.ParseUint(str, 10, 32) return err == nil } func MaybeEnv(vars []string) bool { for _, v := range vars { if _, exist := os.LookupEnv(v); exist { return true } } return false }