tools/cli/utils.go (855 lines of code) (raw):
// Copyright (c) 2017-2020 Uber Technologies Inc.
// Portions of the Software are attributed to Copyright (c) 2020 Temporal Technologies Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package cli
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"regexp"
"runtime/debug"
"sort"
"strconv"
"strings"
"time"
"github.com/fatih/color"
"github.com/golang-jwt/jwt/v5"
"github.com/urfave/cli"
"github.com/valyala/fastjson"
"github.com/uber/cadence/client/frontend"
"github.com/uber/cadence/common"
"github.com/uber/cadence/common/authorization"
"github.com/uber/cadence/common/pagination"
"github.com/uber/cadence/common/types"
)
// JSONHistorySerializer is used to encode history event in JSON
type JSONHistorySerializer struct{}
// Serialize serializes history.
func (j *JSONHistorySerializer) Serialize(h *types.History) ([]byte, error) {
return json.Marshal(h.Events)
}
// Deserialize deserializes history
func (j *JSONHistorySerializer) Deserialize(data []byte) (*types.History, error) {
var events []*types.HistoryEvent
err := json.Unmarshal(data, &events)
if err != nil {
return nil, err
}
return &types.History{Events: events}, nil
}
// GetHistory helper method to iterate over all pages and return complete list of history events
func GetHistory(ctx context.Context, workflowClient frontend.Client, domain, workflowID, runID string) (*types.History, error) {
events := []*types.HistoryEvent{}
iterator, err := GetWorkflowHistoryIterator(ctx, workflowClient, domain, workflowID, runID, false, types.HistoryEventFilterTypeAllEvent.Ptr())
for iterator.HasNext() {
entity, err := iterator.Next()
if err != nil {
return nil, err
}
events = append(events, entity.(*types.HistoryEvent))
}
history := &types.History{}
history.Events = events
return history, err
}
// GetWorkflowHistoryIterator returns a HistoryEvent iterator
func GetWorkflowHistoryIterator(
ctx context.Context,
workflowClient frontend.Client,
domain,
workflowID,
runID string,
isLongPoll bool,
filterType *types.HistoryEventFilterType,
) (pagination.Iterator, error) {
paginate := func(ctx context.Context, pageToken pagination.PageToken) (pagination.Page, error) {
tcCtx, cancel := context.WithTimeout(ctx, 25*time.Second)
defer cancel()
var nextPageToken []byte
if pageToken != nil {
nextPageToken, _ = pageToken.([]byte)
}
request := &types.GetWorkflowExecutionHistoryRequest{
Domain: domain,
Execution: &types.WorkflowExecution{
WorkflowID: workflowID,
RunID: runID,
},
WaitForNewEvent: isLongPoll,
HistoryEventFilterType: filterType,
NextPageToken: nextPageToken,
SkipArchival: isLongPoll,
}
var resp *types.GetWorkflowExecutionHistoryResponse
var err error
Loop:
for {
resp, err = workflowClient.GetWorkflowExecutionHistory(tcCtx, request)
if err != nil {
return pagination.Page{}, err
}
if isLongPoll && len(resp.History.Events) == 0 && len(resp.NextPageToken) != 0 {
request.NextPageToken = resp.NextPageToken
continue Loop
}
break Loop
}
entities := make([]pagination.Entity, len(resp.History.Events))
for i, e := range resp.History.Events {
entities[i] = e
}
var nextToken interface{} = resp.NextPageToken
if len(resp.NextPageToken) == 0 {
nextToken = nil
}
page := pagination.Page{
CurrentToken: pageToken,
NextToken: nextToken,
Entities: entities,
}
return page, err
}
return pagination.NewIterator(ctx, nil, paginate), nil
}
// HistoryEventToString convert HistoryEvent to string
func HistoryEventToString(e *types.HistoryEvent, printFully bool, maxFieldLength int) string {
data := getEventAttributes(e)
return anyToString(data, printFully, maxFieldLength)
}
func anyToString(d interface{}, printFully bool, maxFieldLength int) string {
// fields related to schedule are of time.Time type, and we shouldn't dive
// into it with reflection - it's fields are private.
tm, ok := d.(time.Time)
if ok {
return trimText(tm.String(), maxFieldLength)
}
v := reflect.ValueOf(d)
switch v.Kind() {
case reflect.Ptr:
return anyToString(v.Elem().Interface(), printFully, maxFieldLength)
case reflect.Struct:
var buf bytes.Buffer
t := reflect.TypeOf(d)
buf.WriteString("{")
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if f.Kind() == reflect.Invalid {
continue
}
fieldValue := valueToString(f, printFully, maxFieldLength)
if len(fieldValue) == 0 {
continue
}
if buf.Len() > 1 {
buf.WriteString(", ")
}
fieldName := t.Field(i).Name
if !isAttributeName(fieldName) {
if !printFully {
fieldValue = trimTextAndBreakWords(fieldValue, maxFieldLength)
} else if maxFieldLength != 0 { // for command run workflow and observe history
fieldValue = trimText(fieldValue, maxFieldLength)
}
}
if fieldName == "Reason" || fieldName == "Details" || fieldName == "Cause" {
buf.WriteString(fmt.Sprintf("%s:%s", color.RedString(fieldName), color.MagentaString(fieldValue)))
} else {
buf.WriteString(fmt.Sprintf("%s:%s", fieldName, fieldValue))
}
}
buf.WriteString("}")
return buf.String()
default:
return fmt.Sprint(d)
}
}
func valueToString(v reflect.Value, printFully bool, maxFieldLength int) string {
switch v.Kind() {
case reflect.Ptr:
return valueToString(v.Elem(), printFully, maxFieldLength)
case reflect.Struct:
return anyToString(v.Interface(), printFully, maxFieldLength)
case reflect.Invalid:
return ""
case reflect.Slice:
if v.Type().Elem().Kind() == reflect.Uint8 {
n := string(v.Bytes())
if n != "" && n[len(n)-1] == '\n' {
return fmt.Sprintf("[%v]", n[:len(n)-1])
}
return fmt.Sprintf("[%v]", n)
}
return fmt.Sprintf("[len=%d]", v.Len())
case reflect.Map:
str := "map{"
for i, key := range v.MapKeys() {
str += key.String() + ":"
val := v.MapIndex(key)
switch val.Interface().(type) {
case []byte:
str += string(val.Interface().([]byte))
default:
str += val.String()
}
if i != len(v.MapKeys())-1 {
str += ", "
}
}
str += "}"
return str
default:
return fmt.Sprint(v.Interface())
}
}
// limit the maximum length for each field
func trimText(input string, maxFieldLength int) string {
if len(input) > maxFieldLength {
input = fmt.Sprintf("%s ... %s", input[:maxFieldLength/2], input[(len(input)-maxFieldLength/2):])
}
return input
}
// limit the maximum length for each field, and break long words for table item correctly wrap words
func trimTextAndBreakWords(input string, maxFieldLength int) string {
input = trimText(input, maxFieldLength)
return breakLongWords(input, maxWordLength)
}
// long words will make output in table cell looks bad,
// break long text "ltltltltllt..." to "ltlt ltlt lt..." will make use of table autowrap so that output is pretty.
func breakLongWords(input string, maxWordLength int) string {
if len(input) <= maxWordLength {
return input
}
cnt := 0
for i := 0; i < len(input); i++ {
if cnt == maxWordLength {
cnt = 0
input = input[:i] + " " + input[i:]
continue
}
cnt++
if input[i] == ' ' {
cnt = 0
}
}
return input
}
// ColorEvent takes an event and return string with color
// Event with color mapping rules:
//
// Failed - red
// Timeout - yellow
// Canceled - magenta
// Completed - green
// Started - blue
// Others - default (white/black)
func ColorEvent(e *types.HistoryEvent) string {
f := EventColorFunction(*e.EventType)
return f(e.EventType.String())
}
func EventColorFunction(eventType types.EventType) func(format string, a ...interface{}) string {
var colorFunc func(format string, a ...interface{}) string
noColorFunc := func(format string, a ...interface{}) string {
return format
}
switch eventType {
case types.EventTypeWorkflowExecutionStarted,
types.EventTypeChildWorkflowExecutionStarted:
colorFunc = color.BlueString
case types.EventTypeWorkflowExecutionCompleted,
types.EventTypeChildWorkflowExecutionCompleted:
colorFunc = color.GreenString
case types.EventTypeWorkflowExecutionFailed,
types.EventTypeRequestCancelActivityTaskFailed,
types.EventTypeCancelTimerFailed,
types.EventTypeStartChildWorkflowExecutionFailed,
types.EventTypeChildWorkflowExecutionFailed,
types.EventTypeRequestCancelExternalWorkflowExecutionFailed,
types.EventTypeSignalExternalWorkflowExecutionFailed,
types.EventTypeActivityTaskFailed:
colorFunc = color.RedString
case types.EventTypeWorkflowExecutionTimedOut,
types.EventTypeActivityTaskTimedOut,
types.EventTypeWorkflowExecutionCanceled,
types.EventTypeChildWorkflowExecutionTimedOut,
types.EventTypeDecisionTaskTimedOut:
colorFunc = color.YellowString
case types.EventTypeChildWorkflowExecutionCanceled:
colorFunc = color.MagentaString
default:
colorFunc = noColorFunc
}
return colorFunc
}
func getEventAttributes(e *types.HistoryEvent) interface{} {
var data interface{}
switch e.GetEventType() {
case types.EventTypeWorkflowExecutionStarted:
data = e.WorkflowExecutionStartedEventAttributes
case types.EventTypeWorkflowExecutionCompleted:
data = e.WorkflowExecutionCompletedEventAttributes
case types.EventTypeWorkflowExecutionFailed:
data = e.WorkflowExecutionFailedEventAttributes
case types.EventTypeWorkflowExecutionTimedOut:
data = e.WorkflowExecutionTimedOutEventAttributes
case types.EventTypeDecisionTaskScheduled:
data = e.DecisionTaskScheduledEventAttributes
case types.EventTypeDecisionTaskStarted:
data = e.DecisionTaskStartedEventAttributes
case types.EventTypeDecisionTaskCompleted:
data = e.DecisionTaskCompletedEventAttributes
case types.EventTypeDecisionTaskTimedOut:
data = e.DecisionTaskTimedOutEventAttributes
case types.EventTypeActivityTaskScheduled:
data = e.ActivityTaskScheduledEventAttributes
case types.EventTypeActivityTaskStarted:
data = e.ActivityTaskStartedEventAttributes
case types.EventTypeActivityTaskCompleted:
data = e.ActivityTaskCompletedEventAttributes
case types.EventTypeActivityTaskFailed:
data = e.ActivityTaskFailedEventAttributes
case types.EventTypeActivityTaskTimedOut:
data = e.ActivityTaskTimedOutEventAttributes
case types.EventTypeActivityTaskCancelRequested:
data = e.ActivityTaskCancelRequestedEventAttributes
case types.EventTypeRequestCancelActivityTaskFailed:
data = e.RequestCancelActivityTaskFailedEventAttributes
case types.EventTypeActivityTaskCanceled:
data = e.ActivityTaskCanceledEventAttributes
case types.EventTypeTimerStarted:
data = e.TimerStartedEventAttributes
case types.EventTypeTimerFired:
data = e.TimerFiredEventAttributes
case types.EventTypeCancelTimerFailed:
data = e.CancelTimerFailedEventAttributes
case types.EventTypeTimerCanceled:
data = e.TimerCanceledEventAttributes
case types.EventTypeWorkflowExecutionCancelRequested:
data = e.WorkflowExecutionCancelRequestedEventAttributes
case types.EventTypeWorkflowExecutionCanceled:
data = e.WorkflowExecutionCanceledEventAttributes
case types.EventTypeRequestCancelExternalWorkflowExecutionInitiated:
data = e.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes
case types.EventTypeRequestCancelExternalWorkflowExecutionFailed:
data = e.RequestCancelExternalWorkflowExecutionFailedEventAttributes
case types.EventTypeExternalWorkflowExecutionCancelRequested:
data = e.ExternalWorkflowExecutionCancelRequestedEventAttributes
case types.EventTypeMarkerRecorded:
data = e.MarkerRecordedEventAttributes
case types.EventTypeWorkflowExecutionSignaled:
data = e.WorkflowExecutionSignaledEventAttributes
case types.EventTypeWorkflowExecutionTerminated:
data = e.WorkflowExecutionTerminatedEventAttributes
case types.EventTypeWorkflowExecutionContinuedAsNew:
data = e.WorkflowExecutionContinuedAsNewEventAttributes
case types.EventTypeStartChildWorkflowExecutionInitiated:
data = e.StartChildWorkflowExecutionInitiatedEventAttributes
case types.EventTypeStartChildWorkflowExecutionFailed:
data = e.StartChildWorkflowExecutionFailedEventAttributes
case types.EventTypeChildWorkflowExecutionStarted:
data = e.ChildWorkflowExecutionStartedEventAttributes
case types.EventTypeChildWorkflowExecutionCompleted:
data = e.ChildWorkflowExecutionCompletedEventAttributes
case types.EventTypeChildWorkflowExecutionFailed:
data = e.ChildWorkflowExecutionFailedEventAttributes
case types.EventTypeChildWorkflowExecutionCanceled:
data = e.ChildWorkflowExecutionCanceledEventAttributes
case types.EventTypeChildWorkflowExecutionTimedOut:
data = e.ChildWorkflowExecutionTimedOutEventAttributes
case types.EventTypeChildWorkflowExecutionTerminated:
data = e.ChildWorkflowExecutionTerminatedEventAttributes
case types.EventTypeSignalExternalWorkflowExecutionInitiated:
data = e.SignalExternalWorkflowExecutionInitiatedEventAttributes
case types.EventTypeSignalExternalWorkflowExecutionFailed:
data = e.SignalExternalWorkflowExecutionFailedEventAttributes
case types.EventTypeExternalWorkflowExecutionSignaled:
data = e.ExternalWorkflowExecutionSignaledEventAttributes
default:
data = e
}
return data
}
func isAttributeName(name string) bool {
for i := types.EventType(0); i < types.EventType(40); i++ {
if name == i.String()+"EventAttributes" {
return true
}
}
return false
}
func getCurrentUserFromEnv() string {
for _, n := range envKeysForUserName {
if len(os.Getenv(n)) > 0 {
return os.Getenv(n)
}
}
return "unknown"
}
func prettyPrintJSONObject(o interface{}) {
b, err := json.MarshalIndent(o, "", " ")
if err != nil {
fmt.Printf("Error when try to print pretty: %v\n", err)
fmt.Println(o)
}
os.Stdout.Write(b)
fmt.Println()
}
func mapKeysToArray(m map[string]string) []string {
var out []string
for k := range m {
out = append(out, k)
}
return out
}
func intSliceToSet(s []int) map[int]struct{} {
var ret = make(map[int]struct{}, len(s))
for _, v := range s {
ret[v] = struct{}{}
}
return ret
}
func printMessage(msg string) {
fmt.Printf("%s %s\n", "cadence:", msg)
}
func printError(msg string, err error) {
if err != nil {
fmt.Printf("%s %s\n%s %+v\n", colorRed("Error:"), msg, colorMagenta("Error Details:"), err)
if os.Getenv(showErrorStackEnv) != `` {
fmt.Printf("Stack trace:\n")
debug.PrintStack()
} else {
fmt.Printf("('export %s=1' to see stack traces)\n", showErrorStackEnv)
}
} else {
fmt.Printf("%s %s\n", colorRed("Error:"), msg)
}
}
// ErrorAndExit print easy to understand error msg first then error detail in a new line
func ErrorAndExit(msg string, err error) {
printError(msg, err)
osExit(1)
}
func getWorkflowClient(c *cli.Context) frontend.Client {
return cFactory.ServerFrontendClient(c)
}
func getRequiredOption(c *cli.Context, optionName string) string {
value := c.String(optionName)
if len(value) == 0 {
ErrorAndExit(fmt.Sprintf("Option %s is required", optionName), nil)
}
return value
}
func getRequiredInt64Option(c *cli.Context, optionName string) int64 {
if !c.IsSet(optionName) {
ErrorAndExit(fmt.Sprintf("Option %s is required", optionName), nil)
}
return c.Int64(optionName)
}
func getRequiredIntOption(c *cli.Context, optionName string) int {
if !c.IsSet(optionName) {
ErrorAndExit(fmt.Sprintf("Option %s is required", optionName), nil)
}
return c.Int(optionName)
}
func getRequiredGlobalOption(c *cli.Context, optionName string) string {
value := c.GlobalString(optionName)
if len(value) == 0 {
ErrorAndExit(fmt.Sprintf("Global option %s is required", optionName), nil)
}
return value
}
func timestampPtrToStringPtr(unixNanoPtr *int64, onlyTime bool) *string {
if unixNanoPtr == nil {
return nil
}
return common.StringPtr(convertTime(*unixNanoPtr, onlyTime))
}
func convertTime(unixNano int64, onlyTime bool) string {
t := time.Unix(0, unixNano)
var result string
if onlyTime {
result = t.Format(defaultTimeFormat)
} else {
result = t.Format(defaultDateTimeFormat)
}
return result
}
func parseTime(timeStr string, defaultValue int64) int64 {
if len(timeStr) == 0 {
return defaultValue
}
// try to parse
parsedTime, err := time.Parse(defaultDateTimeFormat, timeStr)
if err == nil {
return parsedTime.UnixNano()
}
// treat as raw time
resultValue, err := strconv.ParseInt(timeStr, 10, 64)
if err == nil {
return resultValue
}
// treat as time range format
parsedTime, err = parseTimeRange(timeStr)
if err != nil {
ErrorAndExit(fmt.Sprintf("Cannot parse time '%s', use UTC format '2006-01-02T15:04:05Z', "+
"time range or raw UnixNano directly. See help for more details.", timeStr), err)
}
return parsedTime.UnixNano()
}
// parseTimeRange parses a given time duration string (in format X<time-duration>) and
// returns parsed timestamp given that duration in the past from current time.
// All valid values must contain a number followed by a time-duration, from the following list (long form/short form):
// - second/s
// - minute/m
// - hour/h
// - day/d
// - week/w
// - month/M
// - year/y
// For example, possible input values, and their result:
// - "3d" or "3day" --> three days --> time.Now().Add(-3 * 24 * time.Hour)
// - "2m" or "2minute" --> two minutes --> time.Now().Add(-2 * time.Minute)
// - "1w" or "1week" --> one week --> time.Now().Add(-7 * 24 * time.Hour)
// - "30s" or "30second" --> thirty seconds --> time.Now().Add(-30 * time.Second)
// Note: Duration strings are case-sensitive, and should be used as mentioned above only.
// Limitation: Value of numerical multiplier, X should be in b/w 0 - 1e6 (1 million), boundary values excluded i.e.
// 0 < X < 1e6. Also, the maximum time in the past can be 1 January 1970 00:00:00 UTC (epoch time),
// so giving "1000y" will result in epoch time.
func parseTimeRange(timeRange string) (time.Time, error) {
match, err := regexp.MatchString(defaultDateTimeRangeShortRE, timeRange)
if !match { // fallback on to check if it's of longer notation
_, err = regexp.MatchString(defaultDateTimeRangeLongRE, timeRange)
}
if err != nil {
return time.Time{}, err
}
re, _ := regexp.Compile(defaultDateTimeRangeNum)
idx := re.FindStringSubmatchIndex(timeRange)
if idx == nil {
return time.Time{}, fmt.Errorf("cannot parse timeRange %s", timeRange)
}
num, err := strconv.Atoi(timeRange[idx[0]:idx[1]])
if err != nil {
return time.Time{}, fmt.Errorf("cannot parse timeRange %s", timeRange)
}
if num >= 1e6 {
return time.Time{}, fmt.Errorf("invalid time-duation multiplier %d, allowed range is 0 < multiplier < 1000000", num)
}
dur, err := parseTimeDuration(timeRange[idx[1]:])
if err != nil {
return time.Time{}, fmt.Errorf("cannot parse timeRange %s", timeRange)
}
res := time.Now().Add(time.Duration(-num) * dur) // using server's local timezone
epochTime := time.Unix(0, 0)
if res.Before(epochTime) {
res = epochTime
}
return res, nil
}
func parseSingleTs(ts string) (time.Time, error) {
var tsOut time.Time
var err error
formats := []string{"2006-01-02T15:04:05", "2006-01-02T15:04", "2006-01-02", "2006-01-02T15:04:05+0700", time.RFC3339}
for _, format := range formats {
if tsOut, err = time.Parse(format, ts); err == nil {
return tsOut, err
}
}
return tsOut, err
}
// parseTimeDuration parses the given time duration in either short or long convention
// and returns the time.Duration
// Valid values (long notation/short notation):
// - second/s
// - minute/m
// - hour/h
// - day/d
// - week/w
// - month/M
// - year/y
// NOTE: the input "duration" is case-sensitive
func parseTimeDuration(duration string) (dur time.Duration, err error) {
switch duration {
case "s", "second":
dur = time.Second
case "m", "minute":
dur = time.Minute
case "h", "hour":
dur = time.Hour
case "d", "day":
dur = day
case "w", "week":
dur = week
case "M", "month":
dur = month
case "y", "year":
dur = year
default:
err = fmt.Errorf("unknown time duration %s", duration)
}
return
}
func strToTaskListType(str string) types.TaskListType {
if strings.ToLower(str) == "activity" {
return types.TaskListTypeActivity
}
return types.TaskListTypeDecision
}
func getCliIdentity() string {
return fmt.Sprintf("cadence-cli@%s", getHostName())
}
func getHostName() string {
hostName, err := os.Hostname()
if err != nil {
hostName = "UnKnown"
}
return hostName
}
func processJWTFlags(ctx context.Context, cliCtx *cli.Context) context.Context {
path := getJWTPrivateKey(cliCtx)
t := getJWT(cliCtx)
var token string
var err error
if t != "" {
token = t
} else if path != "" {
token, err = createJWT(path)
if err != nil {
ErrorAndExit("Error creating JWT token", err)
}
}
return context.WithValue(ctx, CtxKeyJWT, token)
}
func populateContextFromCLIContext(ctx context.Context, cliCtx *cli.Context) context.Context {
ctx = processJWTFlags(ctx, cliCtx)
return ctx
}
func newContext(c *cli.Context) (context.Context, context.CancelFunc) {
return newTimedContext(c, defaultContextTimeout)
}
func newContextForLongPoll(c *cli.Context) (context.Context, context.CancelFunc) {
return newTimedContext(c, defaultContextTimeoutForLongPoll)
}
func newIndefiniteContext(c *cli.Context) (context.Context, context.CancelFunc) {
if c.GlobalIsSet(FlagContextTimeout) {
return newTimedContext(c, time.Duration(c.GlobalInt(FlagContextTimeout))*time.Second)
}
return context.WithCancel(populateContextFromCLIContext(context.Background(), c))
}
func newTimedContext(c *cli.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if overrideTimeout := c.GlobalInt(FlagContextTimeout); overrideTimeout > 0 {
timeout = time.Duration(overrideTimeout) * time.Second
}
ctx := populateContextFromCLIContext(context.Background(), c)
return context.WithTimeout(ctx, timeout)
}
// process and validate input provided through cmd or file
func processJSONInput(c *cli.Context) string {
return processJSONInputHelper(c, jsonTypeInput)
}
// process and validate json
func processJSONInputHelper(c *cli.Context, jType jsonType) string {
var flagNameOfRawInput string
var flagNameOfInputFileName string
switch jType {
case jsonTypeInput:
flagNameOfRawInput = FlagInput
flagNameOfInputFileName = FlagInputFile
case jsonTypeMemo:
flagNameOfRawInput = FlagMemo
flagNameOfInputFileName = FlagMemoFile
case jsonTypeHeader:
flagNameOfRawInput = FlagHeaderValue
flagNameOfInputFileName = FlagHeaderFile
case jsonTypeSignal:
flagNameOfRawInput = FlagSignalInput
flagNameOfInputFileName = FlagSignalInputFile
default:
return ""
}
var input string
if c.IsSet(flagNameOfRawInput) {
input = c.String(flagNameOfRawInput)
} else if c.IsSet(flagNameOfInputFileName) {
inputFile := c.String(flagNameOfInputFileName)
// This method is purely used to parse input from the CLI. The input comes from a trusted user
// #nosec
data, err := ioutil.ReadFile(inputFile)
if err != nil {
ErrorAndExit("Error reading input file", err)
}
input = string(data)
}
if input != "" {
if err := validateJSONs(input); err != nil {
ErrorAndExit("Input is not valid JSON, or JSONs concatenated with spaces/newlines.", err)
}
}
return input
}
func processMultipleKeys(rawKey, separator string) []string {
var keys []string
if strings.TrimSpace(rawKey) != "" {
keys = strings.Split(rawKey, separator)
}
return keys
}
func processMultipleJSONValues(rawValue string) []string {
var values []string
var sc fastjson.Scanner
sc.Init(rawValue)
for sc.Next() {
values = append(values, sc.Value().String())
}
if err := sc.Error(); err != nil {
ErrorAndExit("Parse json error.", err)
}
return values
}
func mapFromKeysValues(keys, values []string) map[string][]byte {
fields := map[string][]byte{}
for i, key := range keys {
fields[key] = []byte(values[i])
}
return fields
}
// validate whether str is a valid json or multi valid json concatenated with spaces/newlines
func validateJSONs(str string) error {
input := []byte(str)
dec := json.NewDecoder(bytes.NewReader(input))
for {
_, err := dec.Token()
if err == io.EOF {
return nil // End of input, valid JSON
}
if err != nil {
return err // Invalid input
}
}
}
// use parseBool to ensure all BOOL search attributes only be "true" or "false"
func parseBool(str string) (bool, error) {
switch str {
case "true":
return true, nil
case "false":
return false, nil
}
return false, fmt.Errorf("not parseable bool value: %s", str)
}
func trimSpace(strs []string) []string {
result := make([]string, len(strs))
for i, v := range strs {
result[i] = strings.TrimSpace(v)
}
return result
}
func parseArray(v string) (interface{}, error) {
if len(v) > 0 && v[0] == '[' && v[len(v)-1] == ']' {
parsedValues, err := fastjson.Parse(v)
if err != nil {
return nil, err
}
arr, err := parsedValues.Array()
if err != nil {
return nil, err
}
result := make([]interface{}, len(arr))
for i, item := range arr {
s := item.String()
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { // remove addition quote from json
s = s[1 : len(s)-1]
if sTime, err := time.Parse(defaultDateTimeFormat, s); err == nil {
result[i] = sTime
continue
}
}
result[i] = s
}
return result, nil
}
return nil, errors.New("not array")
}
func convertStringToRealType(v string) interface{} {
var genVal interface{}
var err error
if genVal, err = strconv.ParseInt(v, 10, 64); err == nil {
} else if genVal, err = parseBool(v); err == nil {
} else if genVal, err = strconv.ParseFloat(v, 64); err == nil {
} else if genVal, err = time.Parse(defaultDateTimeFormat, v); err == nil {
} else if genVal, err = parseArray(v); err == nil {
} else {
genVal = v
}
return genVal
}
func truncate(str string) string {
if len(str) > maxOutputStringLength {
return str[:maxOutputStringLength]
}
return str
}
// this only works for ANSI terminal, which means remove existing lines won't work if users redirect to file
// ref: https://en.wikipedia.org/wiki/ANSI_escape_code
func removePrevious2LinesFromTerminal() {
fmt.Printf("\033[1A")
fmt.Printf("\033[2K")
fmt.Printf("\033[1A")
fmt.Printf("\033[2K")
}
func showNextPage() bool {
fmt.Printf("Press %s to show next page, press %s to quit: ",
color.GreenString("Enter"), color.RedString("any other key then Enter"))
var input string
fmt.Scanln(&input)
return strings.Trim(input, " ") == ""
}
// prompt will show input msg, then waiting user input y/yes to continue
func prompt(msg string) {
reader := bufio.NewReader(os.Stdin)
fmt.Println(msg)
text, _ := reader.ReadString('\n')
textLower := strings.ToLower(strings.TrimRight(text, "\n"))
if textLower != "y" && textLower != "yes" {
os.Exit(0)
}
}
func getInputFile(inputFile string) *os.File {
if len(inputFile) == 0 {
info, err := os.Stdin.Stat()
if err != nil {
ErrorAndExit("Failed to stat stdin file handle", err)
}
if info.Mode()&os.ModeCharDevice != 0 || info.Size() <= 0 {
fmt.Fprintln(os.Stderr, "Provide a filename or pass data to STDIN")
os.Exit(1)
}
return os.Stdin
}
// This code is executed from the CLI. All user input is from a CLI user.
// #nosec
f, err := os.Open(inputFile)
if err != nil {
ErrorAndExit(fmt.Sprintf("Failed to open input file for reading: %v", inputFile), err)
}
return f
}
// createJWT defines the logic to create a JWT
func createJWT(keyPath string) (string, error) {
privateKey, err := common.LoadRSAPrivateKey(keyPath)
if err != nil {
return "", err
}
ttl := int64(60 * 10)
claims := authorization.JWTClaims{
Admin: true,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * time.Duration(ttl))),
},
}
return jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(privateKey)
}
func getWorkflowMemo(input map[string]interface{}) (*types.Memo, error) {
if input == nil {
return nil, nil
}
memo := make(map[string][]byte)
for k, v := range input {
memoBytes, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("encode workflow memo error: %v", err.Error())
}
memo[k] = memoBytes
}
return &types.Memo{Fields: memo}, nil
}
func serializeSearchAttributes(input map[string]interface{}) (*types.SearchAttributes, error) {
if input == nil {
return nil, nil
}
attr := make(map[string][]byte)
for k, v := range input {
attrBytes, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("encode search attribute [%s] error: %v", k, err)
}
attr[k] = attrBytes
}
return &types.SearchAttributes{IndexedFields: attr}, nil
}
// parseIntMultiRange will parse string of multiple integer ranges separates by commas.
// Single range can be an integer or inclusive range separated by dash.
// The result is a sorted set union of integers.
// Example: "3,8-8,5-6" -> [3,4,5,8]
func parseIntMultiRange(s string) ([]int, error) {
set := map[int]struct{}{}
ranges := strings.Split(strings.TrimSpace(s), ",")
for _, r := range ranges {
r = strings.TrimSpace(r)
if len(r) == 0 {
continue
}
parts := strings.Split(r, "-")
switch len(parts) {
case 1:
i, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return nil, fmt.Errorf("single number %q: %v", r, err)
}
set[i] = struct{}{}
case 2:
lower, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return nil, fmt.Errorf("lower range of %q: %v", r, err)
}
upper, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return nil, fmt.Errorf("upper range of %q: %v", r, err)
}
for i := lower; i <= upper; i++ {
set[i] = struct{}{}
}
default:
return nil, fmt.Errorf("invalid range %q", r)
}
}
result := []int{}
for i := range set {
result = append(result, i)
}
sort.Ints(result)
return result, nil
}