| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package commands |
| |
| import ( |
| "bufio" |
| "errors" |
| "fmt" |
| "strings" |
| |
| "github.com/apache/openwhisk-cli/wski18n" |
| "github.com/apache/openwhisk-client-go/whisk" |
| |
| "github.com/fatih/color" |
| "github.com/mattn/go-colorable" |
| |
| //prettyjson "github.com/hokaccha/go-prettyjson" // See prettyjson comment below |
| "archive/tar" |
| "archive/zip" |
| "bytes" |
| "compress/gzip" |
| "encoding/json" |
| "io" |
| "io/ioutil" |
| "os" |
| "reflect" |
| "regexp" |
| "sort" |
| ) |
| |
| func csvToQualifiedActions(artifacts string) []string { |
| var res []string |
| actions := strings.Split(artifacts, ",") |
| for i := 0; i < len(actions); i++ { |
| res = append(res, getQualifiedName(actions[i], Properties.Namespace)) |
| } |
| |
| return res |
| } |
| |
| /** |
| * Processes command line to retrieve pairs of key-value pairs, where the value must be valid JSON. |
| * |
| * Parameters and annotations are handled the same way. The flag here is only for generating an error messages |
| * specific to one or the other. |
| * |
| * NOTE: this function will exit in case of a processing error since it indicates a problem parsing parameters. |
| * |
| * @return either an array or a JSON object (map) formatted representation of the key-value pairs. |
| */ |
| func getParameters(params []string, keyValueFormat bool, annotation bool) interface{} { |
| var parameters interface{} |
| var err error |
| |
| if !annotation { |
| whisk.Debug(whisk.DbgInfo, "Parsing parameters: %#v\n", params) |
| } else { |
| whisk.Debug(whisk.DbgInfo, "Parsing annotations: %#v\n", params) |
| } |
| |
| parameters, err = getJSONFromStrings(params, keyValueFormat) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "getJSONFromStrings(%#v, %t) failed: %s\n", params, keyValueFormat, err) |
| var errStr string |
| |
| if !annotation { |
| errStr = wski18n.T("Invalid parameter argument '{{.param}}': {{.err}}", |
| map[string]interface{}{"param": fmt.Sprintf("%#v", params), "err": err}) |
| } else { |
| errStr = wski18n.T("Invalid annotation argument '{{.annotation}}': {{.err}}", |
| map[string]interface{}{"annotation": fmt.Sprintf("%#v", params), "err": err}) |
| } |
| werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| ExitOnError(werr) |
| } |
| |
| return parameters |
| } |
| |
| func getJSONFromStrings(content []string, keyValueFormat bool) (interface{}, error) { |
| var data map[string]interface{} |
| var res interface{} |
| |
| whisk.Debug(whisk.DbgInfo, "Convert content to JSON: %#v\n", content) |
| |
| for i := 0; i < len(content); i++ { |
| dc := json.NewDecoder(strings.NewReader(content[i])) |
| dc.UseNumber() |
| if err := dc.Decode(&data); err != nil { |
| whisk.Debug(whisk.DbgError, "Invalid JSON detected for '%s' \n", content[i]) |
| return whisk.KeyValueArr{}, err |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "Created map '%v' from '%v'\n", data, content[i]) |
| } |
| |
| if data == nil { |
| data = make(map[string]interface{}) |
| } |
| |
| if keyValueFormat { |
| res = getKeyValueFormattedJSON(data) |
| } else { |
| res = data |
| } |
| |
| return res, nil |
| } |
| |
| func getKeyValueFormattedJSON(data map[string]interface{}) whisk.KeyValueArr { |
| var keyValueArr whisk.KeyValueArr |
| |
| for key, value := range data { |
| keyValue := whisk.KeyValue{ |
| Key: key, |
| Value: value, |
| } |
| keyValueArr = append(keyValueArr, keyValue) |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "Created key/value format '%v' from '%v'\n", keyValueArr, data) |
| |
| return keyValueArr |
| } |
| |
| func getFormattedJSON(key string, value string) string { |
| var res string |
| |
| key = getEscapedJSON(key) |
| |
| if isValidJSON(value) { |
| whisk.Debug(whisk.DbgInfo, "Value '%s' is valid JSON.\n", value) |
| res = fmt.Sprintf("{\"%s\": %s}", key, value) |
| } else { |
| whisk.Debug(whisk.DbgInfo, "Converting value '%s' to a string as it is not valid JSON.\n", value) |
| res = fmt.Sprintf("{\"%s\": \"%s\"}", key, value) |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "Formatted JSON '%s'\n", res) |
| |
| return res |
| } |
| |
| func getEscapedJSON(value string) string { |
| value = strings.Replace(value, "\\", "\\\\", -1) |
| value = strings.Replace(value, "\"", "\\\"", -1) |
| |
| return value |
| } |
| |
| func isValidJSON(value string) bool { |
| var jsonInterface interface{} |
| err := json.Unmarshal([]byte(value), &jsonInterface) |
| |
| return err == nil |
| } |
| |
| var boldString = color.New(color.Bold).SprintFunc() |
| |
| type Sortables []whisk.Sortable |
| |
| // Uses quickSort to sort commands based on their compare methods |
| // Param: Takes in a array of Sortable interfaces which contains a specific command |
| func Swap(sortables Sortables, i, j int) { sortables[i], sortables[j] = sortables[j], sortables[i] } |
| |
| func toPrintable(sortable []whisk.Sortable) []whisk.Printable { |
| sortedPrintable := make([]whisk.Printable, len(sortable), len(sortable)) |
| for i := range sortable { |
| sortedPrintable[i] = sortable[i].(whisk.Printable) |
| } |
| return sortedPrintable |
| } |
| |
| // Prints the parameters/list for wsk xxxx list |
| // Identifies type and then copies array into an array of interfaces(Sortable) to be sorted and printed |
| // Param: Takes in an interface which contains an array of a command(Ex: []Action) |
| func printList(collection interface{}, sortByName bool) { |
| var commandToSort []whisk.Sortable |
| switch collection := collection.(type) { |
| case []whisk.Action: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.Trigger: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.Package: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.Rule: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.Namespace: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.ActivationFilteredRow: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.ApiFilteredList: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| case []whisk.ApiFilteredRow: |
| for i := range collection { |
| commandToSort = append(commandToSort, collection[i]) |
| } |
| } |
| if sortByName && len(commandToSort) > 0 { |
| quickSort(commandToSort, 0, len(commandToSort)-1) |
| } |
| printCommandsList(toPrintable(commandToSort), makeDefaultHeader(collection)) |
| } |
| |
| func quickSort(toSort Sortables, left int, right int) { |
| low := left |
| high := right |
| pivot := toSort[(left+right)/2] |
| |
| for low <= high { |
| for toSort[low].Compare(pivot) { |
| low++ |
| } |
| for pivot.Compare(toSort[high]) { |
| high-- |
| } |
| if low <= high { |
| Swap(toSort, low, high) |
| low++ |
| high-- |
| } |
| } |
| if left < high { |
| quickSort(toSort, left, high) |
| } |
| if low < right { |
| quickSort(toSort, low, right) |
| } |
| } |
| |
| // makeDefaultHeader(collection) returns the default header to be used in case |
| // the list to be printed is empty. |
| func makeDefaultHeader(collection interface{}) string { |
| defaultHeader := reflect.TypeOf(collection).String() |
| defaultHeader = strings.ToLower(defaultHeader[8:] + "s") // Removes '[]whisk.' from `[]whisk.ENTITY_TYPE` |
| if defaultHeader == "apifilteredrows" { |
| defaultHeader = fmt.Sprintf("%-30s %7s %20s %s", "Action", "Verb", "API Name", "URL") |
| } else if defaultHeader == "apifilteredlists" { |
| defaultHeader = "" |
| } else if defaultHeader == "activationfilteredrows" { |
| defaultHeader = "" |
| } |
| return defaultHeader |
| } |
| |
| func stripTimestamp(log string) (strippedLog string) { |
| // parses out the timestamp if it exists first |
| // the timestamp expected format is YYYY-MM-DDTHH:MM:SS.[0-9]+Z |
| // an optional " stdout" or " stderr" stream identifier |
| // and the rest as the log line |
| regex := regexp.MustCompile("\\d{4}-[01]{1}\\d{1}-[0-3]{1}\\d{1}T[0-2]{1}\\d{1}:[0-6]{1}\\d{1}:[0-6]{1}\\d{1}.\\d+Z( *(stdout|stderr):)?\\s(.*)") |
| match := regex.FindStringSubmatch(log) |
| |
| if len(match) > 3 && len(match[3]) > 0 { |
| strippedLog = match[3] |
| } else { |
| strippedLog = log |
| } |
| |
| return strippedLog |
| } |
| |
| func printFullList(collection interface{}) { |
| switch collection := collection.(type) { |
| case []whisk.Action: |
| |
| case []whisk.Trigger: |
| |
| case []whisk.Package: |
| |
| case []whisk.Rule: |
| |
| case []whisk.Namespace: |
| |
| case []whisk.Activation: |
| printFullActivationList(collection) |
| } |
| } |
| |
| func printSummary(collection interface{}) { |
| switch collection := collection.(type) { |
| case *whisk.Action: |
| printActionSummary(collection) |
| case *whisk.Trigger: |
| printTriggerSummary(collection) |
| case *whisk.Package: |
| printPackageSummary(collection) |
| case *whisk.Rule: |
| |
| case *whisk.Namespace: |
| |
| case *whisk.Activation: |
| } |
| } |
| |
| // Used to print Action, Tigger, Package, and Rule lists |
| // Param: Takes in a array of Printable interface, and the name of the command |
| // being sent to it |
| // **Note**: The name should be an empty string for APIs. |
| func printCommandsList(commands []whisk.Printable, defaultHeader string) { |
| if len(commands) != 0 { |
| fmt.Fprint(color.Output, boldString(commands[0].ToHeaderString())) |
| for i := range commands { |
| fmt.Print(commands[i].ToSummaryRowString()) |
| } |
| } else { |
| fmt.Fprintf(color.Output, "%s\n", boldString(defaultHeader)) |
| } |
| } |
| |
| func printFullActivationList(activations []whisk.Activation) { |
| fmt.Fprintf(color.Output, "%s\n", boldString("activations")) |
| for _, activation := range activations { |
| printJSON(activation) |
| } |
| } |
| |
| func printStrippedActivationLogs(logs []string) { |
| for _, log := range logs { |
| fmt.Printf("%s\n", stripTimestamp(log)) |
| } |
| } |
| |
| func printActivationLogs(logs []string) { |
| for _, log := range logs { |
| fmt.Printf("%s\n", log) |
| } |
| } |
| |
| func printArrayContents(arrStr []string) { |
| for _, str := range arrStr { |
| fmt.Printf("%s\n", str) |
| } |
| } |
| |
| func printPackageSummary(pkg *whisk.Package) { |
| printEntitySummary(fmt.Sprintf("%7s", "package"), getFullName(pkg.Namespace, pkg.Name, ""), |
| getValueString(pkg.Annotations, "description"), |
| strings.Join(getParamUnion(pkg.Annotations, pkg.Parameters, "name"), ", ")) |
| |
| if pkg.Actions != nil { |
| for _, action := range pkg.Actions { |
| paramUnion := getParamUnion(action.Annotations, action.Parameters, "name") |
| printEntitySummary(fmt.Sprintf("%7s", "action"), getFullName(pkg.Namespace, pkg.Name, action.Name), |
| getValueString(action.Annotations, "description"), |
| strings.Join(paramUnion, ", ")) |
| } |
| } |
| |
| if pkg.Feeds != nil { |
| for _, feed := range pkg.Feeds { |
| printEntitySummary(fmt.Sprintf("%7s", "feed "), getFullName(pkg.Namespace, pkg.Name, feed.Name), |
| getValueString(feed.Annotations, "description"), |
| strings.Join(getParamUnion(feed.Annotations, feed.Parameters, "name"), ", ")) |
| } |
| } |
| } |
| |
| func printActionSummary(action *whisk.Action) { |
| paramUnion := getParamUnion(action.Annotations, action.Parameters, "name") |
| printEntitySummary(fmt.Sprintf("%6s", "action"), |
| getFullName(action.Namespace, "", action.Name), |
| getValueString(action.Annotations, "description"), |
| strings.Join(paramUnion, ", ")) |
| } |
| |
| func printTriggerSummary(trigger *whisk.Trigger) { |
| printEntitySummary(fmt.Sprintf("%7s", "trigger"), |
| getFullName(trigger.Namespace, "", trigger.Name), |
| getValueString(trigger.Annotations, "description"), |
| strings.Join(getParamUnion(trigger.Annotations, trigger.Parameters, "name"), ", ")) |
| } |
| |
| func printRuleSummary(rule *whisk.Rule) { |
| fmt.Fprintf(color.Output, "%s %s\n", boldString(fmt.Sprintf("%4s", "rule")), |
| getFullName(rule.Namespace, "", rule.Name)) |
| fmt.Fprintf(color.Output, " (%s: %s)\n", boldString(wski18n.T("status")), rule.Status) |
| } |
| |
| func printEntitySummary(entityType string, fullName string, description string, params string) { |
| emptyParams := "none defined" |
| if len(params) <= 0 { |
| params = emptyParams |
| } |
| if len(description) > 0 { |
| fmt.Fprintf(color.Output, "%s %s: %s\n", boldString(entityType), fullName, description) |
| } else if params != emptyParams { |
| descriptionFromParams := buildParamDescription(params) |
| fmt.Fprintf(color.Output, "%s %s: %s\n", boldString(entityType), fullName, descriptionFromParams) |
| } else { |
| fmt.Fprintf(color.Output, "%s %s\n", boldString(entityType), fullName) |
| } |
| fmt.Fprintf(color.Output, " (%s: %s)\n", boldString(wski18n.T("parameters")), params) |
| } |
| |
| // getParamUnion(keyValArrAnnots, keyValArrParams, key) returns the union |
| // of parameters listed under annotations (keyValArrAnnots, using key) and |
| // bound parameters (keyValArrParams). Bound parameters will be denoted with |
| // a prefixed "*", and finalized bound parameters (can't be changed by |
| // user) will be denoted by a prefixed "**". |
| func getParamUnion(keyValArrAnnots whisk.KeyValueArr, keyValArrParams whisk.KeyValueArr, key string) []string { |
| var res []string |
| tag := "*" |
| if getValueBool(keyValArrAnnots, "final") { |
| tag = "**" |
| } |
| boundParams := getKeys(keyValArrParams) |
| annotatedParams := getChildValueStrings(keyValArrAnnots, "parameters", key) |
| res = append(boundParams, annotatedParams...) // Create union of boundParams and annotatedParams with duplication |
| for i := 0; i < len(res); i++ { |
| for j := i + 1; j < len(res); j++ { |
| if res[i] == res[j] { |
| res = append(res[:j], res[j+1:]...) // Remove duplicate entry |
| } |
| } |
| } |
| sort.Strings(res) |
| res = tagBoundParams(boundParams, res, tag) |
| return res |
| } |
| |
| // tagBoundParams(boundParams, paramUnion, tag) returns the list paramUnion with |
| // all strings listed under boundParams set with a prefix tag. |
| func tagBoundParams(boundParams []string, paramUnion []string, tag string) []string { |
| res := paramUnion |
| for i := 0; i < len(boundParams); i++ { |
| for j := 0; j < len(res); j++ { |
| if boundParams[i] == res[j] { |
| res[j] = fmt.Sprintf("%s%s", tag, res[j]) |
| } |
| } |
| } |
| return res |
| } |
| |
| // buildParamDescription(params) returns a default entity description for |
| // `$ wsk [ENTITY] get [ENTITY_NAME] --summary` when parameters are defined, |
| // but the entity description under annotations is not. |
| func buildParamDescription(params string) string { |
| preamble := "Returns a result based on parameter" |
| params = strings.Replace(params, "*", "", -1) |
| temp := strings.Split(params, ",") |
| if len(temp) > 1 { |
| lastParam := temp[len(temp)-1] |
| newParams := strings.Replace(params, fmt.Sprintf(",%s", lastParam), fmt.Sprintf(" and%s", lastParam), 1) |
| return fmt.Sprintf("%ss %s", preamble, newParams) |
| } |
| return fmt.Sprintf("%s %s", preamble, params) |
| } |
| |
| func getFullName(namespace string, packageName string, entityName string) string { |
| var fullName string |
| |
| if len(namespace) > 0 && len(packageName) > 0 && len(entityName) > 0 { |
| fullName = fmt.Sprintf("/%s/%s/%s", namespace, packageName, entityName) |
| } else if len(namespace) > 0 && len(packageName) > 0 { |
| fullName = fmt.Sprintf("/%s/%s", namespace, packageName) |
| } else if len(namespace) > 0 && len(entityName) > 0 { |
| fullName = fmt.Sprintf("/%s/%s", namespace, entityName) |
| } else if len(namespace) > 0 { |
| fullName = fmt.Sprintf("/%s", namespace) |
| } |
| |
| return fullName |
| } |
| |
| func deleteKey(key string, keyValueArr whisk.KeyValueArr) whisk.KeyValueArr { |
| for i := 0; i < len(keyValueArr); i++ { |
| if keyValueArr[i].Key == key { |
| keyValueArr = append(keyValueArr[:i], keyValueArr[i+1:]...) |
| break |
| } |
| } |
| |
| return keyValueArr |
| } |
| |
| func addKeyValue(key string, value interface{}, keyValueArr whisk.KeyValueArr) whisk.KeyValueArr { |
| keyValue := whisk.KeyValue{ |
| Key: key, |
| Value: value, |
| } |
| |
| return append(keyValueArr, keyValue) |
| } |
| |
| func getKeys(keyValueArr whisk.KeyValueArr) []string { |
| var res []string |
| |
| for i := 0; i < len(keyValueArr); i++ { |
| res = append(res, keyValueArr[i].Key) |
| } |
| |
| sort.Strings(res) |
| whisk.Debug(whisk.DbgInfo, "Got keys '%v' from '%v'\n", res, keyValueArr) |
| |
| return res |
| } |
| |
| func getValueString(keyValueArr whisk.KeyValueArr, key string) string { |
| var value interface{} |
| var res string |
| |
| value = keyValueArr.GetValue(key) |
| castedValue, canCast := value.(string) |
| |
| if canCast { |
| res = castedValue |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "Got string value '%v' for key '%s'\n", res, key) |
| |
| return res |
| } |
| |
| func getValueBool(keyValueArr whisk.KeyValueArr, key string) bool { |
| var value interface{} |
| var res bool |
| |
| value = keyValueArr.GetValue(key) |
| castedValue, canCast := value.(bool) |
| |
| if canCast { |
| res = castedValue |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "Got bool value '%v' for key '%s'\n", res, key) |
| |
| return res |
| } |
| |
| func getChildValues(keyValueArr whisk.KeyValueArr, key string, childKey string) []interface{} { |
| var value interface{} |
| var res []interface{} |
| |
| value = keyValueArr.GetValue(key) |
| |
| castedValue, canCast := value.([]interface{}) |
| if canCast { |
| for i := 0; i < len(castedValue); i++ { |
| castedValue, canCast := castedValue[i].(map[string]interface{}) |
| if canCast { |
| for subKey, subValue := range castedValue { |
| if subKey == childKey { |
| res = append(res, subValue) |
| } |
| } |
| } |
| } |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "Got values '%s' from '%v' for key '%s' and child key '%s'\n", res, keyValueArr, key, |
| childKey) |
| |
| return res |
| } |
| |
| func getChildValueStrings(keyValueArr whisk.KeyValueArr, key string, childKey string) []string { |
| var keys []interface{} |
| var res []string |
| |
| keys = getChildValues(keyValueArr, key, childKey) |
| |
| for i := 0; i < len(keys); i++ { |
| castedValue, canCast := keys[i].(string) |
| if canCast { |
| res = append(res, castedValue) |
| } |
| } |
| |
| sort.Strings(res) |
| whisk.Debug(whisk.DbgInfo, "Got values '%s' from '%v' for key '%s' and child key '%s'\n", res, keyValueArr, key, |
| childKey) |
| |
| return res |
| } |
| |
| func getValueFromJSONResponse(field string, response map[string]interface{}) interface{} { |
| var res interface{} |
| |
| for key, value := range response { |
| if key == field { |
| res = value |
| break |
| } |
| } |
| |
| return res |
| } |
| |
| func logoText() string { |
| logo := ` |
| ____ ___ _ _ _ _ _ |
| /\ \ / _ \ _ __ ___ _ __ | | | | |__ (_)___| | __ |
| /\ /__\ \ | | | | '_ \ / _ \ '_ \| | | | '_ \| / __| |/ / |
| / \____ \ / | |_| | |_) | __/ | | | |/\| | | | | \__ \ < |
| \ \ / \/ \___/| .__/ \___|_| |_|__/\__|_| |_|_|___/_|\_\ |
| \___\/ tm |_| |
| ` |
| |
| return logo |
| } |
| |
| func printJSON(v interface{}, stream ...io.Writer) { |
| // Can't use prettyjson util issue https://github.com/hokaccha/go-prettyjson/issues/1 is fixed |
| //output, _ := prettyjson.Marshal(v) |
| // |
| //if len(stream) > 0 { |
| // fmt.Fprintf(stream[0], string(output)) |
| //} else { |
| // fmt.Fprintf(color.Output, string(output)) |
| //} |
| printJsonNoColor(v, stream...) |
| } |
| |
| func printJsonNoColor(decoded interface{}, stream ...io.Writer) { |
| var output bytes.Buffer |
| |
| buffer := new(bytes.Buffer) |
| encoder := json.NewEncoder(buffer) |
| encoder.SetEscapeHTML(false) |
| encoder.Encode(&decoded) |
| json.Indent(&output, buffer.Bytes(), "", " ") |
| |
| if len(stream) > 0 { |
| fmt.Fprintf(stream[0], "%s", string(output.Bytes())) |
| } else { |
| fmt.Fprintf(os.Stdout, "%s", string(output.Bytes())) |
| } |
| } |
| |
| func unpackGzip(inpath string, outpath string) error { |
| var exists bool |
| var err error |
| |
| exists, err = FileExists(outpath) |
| |
| if err != nil { |
| return err |
| } |
| |
| if exists { |
| errStr := wski18n.T("The file '{{.name}}' already exists. Delete it and retry.", |
| map[string]interface{}{"name": outpath}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| |
| exists, err = FileExists(inpath) |
| |
| if err != nil { |
| return err |
| } |
| |
| if !exists { |
| errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist", |
| map[string]interface{}{ |
| "name": inpath, |
| }) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| |
| return whiskErr |
| } |
| |
| unGzFile, err := os.Create(outpath) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Create(%s) failed: %s\n", outpath, err) |
| errStr := wski18n.T("Error creating unGzip file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": outpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| defer unGzFile.Close() |
| |
| gzFile, err := os.Open(inpath) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Open(%s) failed: %s\n", inpath, err) |
| errStr := wski18n.T("Error opening Gzip file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| defer gzFile.Close() |
| |
| gzReader, err := gzip.NewReader(gzFile) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "gzip.NewReader() failed: %s\n", err) |
| errStr := wski18n.T("Unable to unzip file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| |
| _, err = io.Copy(unGzFile, gzReader) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "io.Copy() failed: %s\n", err) |
| errStr := wski18n.T("Unable to unzip file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| |
| return nil |
| } |
| |
| func unpackZip(inpath string) error { |
| exists, err := FileExists(inpath) |
| |
| if err != nil { |
| return err |
| } |
| |
| if !exists { |
| errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist", |
| map[string]interface{}{ |
| "name": inpath, |
| }) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| |
| return whiskErr |
| } |
| zipFileReader, err := zip.OpenReader(inpath) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "zip.OpenReader(%s) failed: %s\n", inpath, err) |
| errStr := wski18n.T("Unable to opens '{{.name}}' for unzipping: {{.err}}", |
| map[string]interface{}{"name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| defer zipFileReader.Close() |
| |
| // Loop through the files in the zipfile |
| for _, item := range zipFileReader.File { |
| itemName := item.Name |
| itemType := item.Mode() |
| |
| whisk.Debug(whisk.DbgInfo, "file item - %#v\n", item) |
| |
| if itemType.IsDir() { |
| if err := os.MkdirAll(item.Name, item.Mode()); err != nil { |
| whisk.Debug(whisk.DbgError, "os.MkdirAll(%s, %d) failed: %s\n", item.Name, item.Mode(), err) |
| errStr := wski18n.T("Unable to create directory '{{.dir}}' while unzipping '{{.name}}': {{.err}}", |
| map[string]interface{}{"dir": item.Name, "name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| } |
| |
| if itemType.IsRegular() { |
| unzipFile, err := item.Open() |
| defer unzipFile.Close() |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "'%s' Open() failed: %s\n", item.Name, err) |
| errStr := wski18n.T("Unable to open zipped file '{{.file}}' while unzipping '{{.name}}': {{.err}}", |
| map[string]interface{}{"file": item.Name, "name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| targetFile, err := os.Create(itemName) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Create(%s) failed: %s\n", itemName, err) |
| errStr := wski18n.T("Unable to create file '{{.file}}' while unzipping '{{.name}}': {{.err}}", |
| map[string]interface{}{"file": item.Name, "name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| if _, err := io.Copy(targetFile, unzipFile); err != nil { |
| whisk.Debug(whisk.DbgError, "io.Copy() of '%s' failed: %s\n", itemName, err) |
| errStr := wski18n.T("Unable to unzip file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": itemName, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func unpackTar(inpath string) error { |
| exists, err := FileExists(inpath) |
| |
| if err != nil { |
| return err |
| } |
| |
| if !exists { |
| errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist", |
| map[string]interface{}{ |
| "name": inpath, |
| }) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| |
| return whiskErr |
| } |
| |
| tarFileReader, err := os.Open(inpath) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Open(%s) failed: %s\n", inpath, err) |
| errStr := wski18n.T("Error opening tar file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| defer tarFileReader.Close() |
| |
| // Loop through the files in the tarfile |
| tReader := tar.NewReader(tarFileReader) |
| for { |
| item, err := tReader.Next() |
| if err == io.EOF { |
| whisk.Debug(whisk.DbgError, "EOF reach during untar\n") |
| break // end of tar |
| } |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "tReader.Next() failed: %s\n", err) |
| errStr := wski18n.T("Error reading tar file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "tar file item - %#v\n", item) |
| switch item.Typeflag { |
| case tar.TypeDir: |
| if err := os.MkdirAll(item.Name, os.FileMode(item.Mode)); err != nil { |
| whisk.Debug(whisk.DbgError, "os.MkdirAll(%s, %d) failed: %s\n", item.Name, item.Mode, err) |
| errStr := wski18n.T("Unable to create directory '{{.dir}}' while untarring '{{.name}}': {{.err}}", |
| map[string]interface{}{"dir": item.Name, "name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| case tar.TypeReg: |
| untarFile, err := os.OpenFile(item.Name, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(item.Mode)) |
| defer untarFile.Close() |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Create(%s) failed: %s\n", item.Name, err) |
| errStr := wski18n.T("Unable to create file '{{.file}}' while untarring '{{.name}}': {{.err}}", |
| map[string]interface{}{"file": item.Name, "name": inpath, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| if _, err := io.Copy(untarFile, tReader); err != nil { |
| whisk.Debug(whisk.DbgError, "io.Copy() of '%s' failed: %s\n", item.Name, err) |
| errStr := wski18n.T("Unable to untar file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": item.Name, "err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| default: |
| whisk.Debug(whisk.DbgError, "Unexpected tar file type of %q\n", item.Typeflag) |
| errStr := wski18n.T("Unable to untar '{{.name}}' due to unexpected tar file type\n", |
| map[string]interface{}{"name": item.Name}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| } |
| return nil |
| } |
| |
| func CheckArgs(args []string, minimumArgNumber int, maximumArgNumber int, commandName string, |
| requiredArgMsg string) *whisk.WskError { |
| exactlyOrAtLeast := wski18n.T("exactly") |
| exactlyOrNoMoreThan := wski18n.T("exactly") |
| |
| if minimumArgNumber != maximumArgNumber { |
| exactlyOrAtLeast = wski18n.T("at least") |
| exactlyOrNoMoreThan = wski18n.T("no more than") |
| } |
| |
| if len(args) < minimumArgNumber { |
| whisk.Debug(whisk.DbgError, fmt.Sprintf("%s command must have %s %d argument(s)\n", commandName, |
| exactlyOrAtLeast, minimumArgNumber)) |
| errMsg := wski18n.T("Invalid argument(s). {{.required}}", map[string]interface{}{"required": requiredArgMsg}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return whiskErr |
| } else if len(args) > maximumArgNumber { |
| whisk.Debug(whisk.DbgError, fmt.Sprintf("%s command must have %s %d argument(s)\n", commandName, |
| exactlyOrNoMoreThan, maximumArgNumber)) |
| errMsg := wski18n.T("Invalid argument(s): {{.args}}. {{.required}}", |
| map[string]interface{}{"args": strings.Join(args[maximumArgNumber:], ", "), "required": requiredArgMsg}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return whiskErr |
| } else { |
| return nil |
| } |
| } |
| |
| func normalizeNamespace(namespace string) string { |
| if namespace == "_" { |
| namespace = wski18n.T("default") |
| } |
| |
| return namespace |
| } |
| |
| func getClientNamespace() string { |
| return normalizeNamespace(Client.Config.Namespace) |
| } |
| |
| func ReadFile(filename string) (string, error) { |
| exists, err := FileExists(filename) |
| |
| if err != nil { |
| return "", err |
| } |
| |
| if !exists { |
| errMsg := wski18n.T("File '{{.name}}' is not a valid file or it does not exist", |
| map[string]interface{}{ |
| "name": filename, |
| }) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_USAGE, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| |
| return "", whiskErr |
| } |
| |
| file, err := ioutil.ReadFile(filename) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.ioutil.ReadFile(%s) error: %s\n", filename, err) |
| errMsg := wski18n.T("Unable to read the file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": filename, "err": err}) |
| whiskErr := whisk.MakeWskErrorFromWskError(errors.New(errMsg), err, whisk.EXIT_CODE_ERR_GENERAL, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return "", whiskErr |
| } |
| |
| return string(file), nil |
| } |
| |
| func writeFile(filename string, content string) error { |
| file, err := os.Create(filename) |
| |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Create(%s) error: %#v\n", filename, err) |
| errMsg := wski18n.T("Cannot create file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": filename, "err": err}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_USAGE, whisk.DISPLAY_MSG, |
| whisk.DISPLAY_USAGE) |
| return whiskErr |
| } |
| |
| defer file.Close() |
| |
| if _, err = file.WriteString(content); err != nil { |
| whisk.Debug(whisk.DbgError, "File.WriteString(%s) error: %#v\n", content, err) |
| errMsg := wski18n.T("Cannot create file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": filename, "err": err}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_USAGE, whisk.DISPLAY_MSG, |
| whisk.DISPLAY_USAGE) |
| return whiskErr |
| } |
| |
| return nil |
| } |
| |
| func FileExists(file string) (bool, error) { |
| _, err := os.Stat(file) |
| |
| if err != nil { |
| if os.IsNotExist(err) == true { |
| return false, nil |
| } else { |
| whisk.Debug(whisk.DbgError, "os.Stat(%s) error: %#v\n", file, err) |
| errMsg := wski18n.T("Cannot access file '{{.name}}': {{.err}}", |
| map[string]interface{}{"name": file, "err": err}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_USAGE, |
| whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) |
| return true, whiskErr |
| } |
| } |
| |
| return true, nil |
| } |
| |
| func fieldExists(value interface{}, field string) bool { |
| element := reflect.ValueOf(value).Elem() |
| |
| for i := 0; i < element.NumField(); i++ { |
| if strings.ToLower(element.Type().Field(i).Name) == strings.ToLower(field) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| func printField(value interface{}, field string) { |
| var matchFunc = func(structField string) bool { |
| return strings.ToLower(structField) == strings.ToLower(field) |
| } |
| |
| structValue := reflect.ValueOf(value) |
| fieldValue := reflect.Indirect(structValue).FieldByNameFunc(matchFunc) |
| |
| printJSON(fieldValue.Interface()) |
| } |
| |
| func parseShared(shared string) (bool, bool, error) { |
| var isShared, isSet bool |
| |
| if strings.ToLower(shared) == "yes" { |
| isShared = true |
| isSet = true |
| } else if strings.ToLower(shared) == "no" { |
| isShared = false |
| isSet = true |
| } else if len(shared) == 0 { |
| isSet = false |
| } else { |
| whisk.Debug(whisk.DbgError, "Cannot use value '%s' for shared.\n", shared) |
| errMsg := wski18n.T("Cannot use value '{{.arg}}' for shared.", map[string]interface{}{"arg": shared}) |
| whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, |
| whisk.DISPLAY_USAGE) |
| return false, false, whiskErr |
| } |
| |
| whisk.Debug(whisk.DbgError, "Sharing is '%t'\n", isShared) |
| |
| return isShared, isSet, nil |
| } |
| |
| func max(a int, b int) int { |
| if a > b { |
| return a |
| } |
| return b |
| } |
| |
| func min(a int, b int) int { |
| if a < b { |
| return a |
| } |
| return b |
| } |
| |
| func ReadProps(path string) (map[string]string, error) { |
| |
| props := map[string]string{} |
| |
| file, err := os.Open(path) |
| if err != nil { |
| // If file does not exist, just return props |
| whisk.Debug(whisk.DbgWarn, "Unable to read whisk properties file '%s' (file open error: %s); falling back to default properties\n", path, err) |
| return props, nil |
| } |
| defer file.Close() |
| |
| lines := []string{} |
| scanner := bufio.NewScanner(file) |
| for scanner.Scan() { |
| lines = append(lines, scanner.Text()) |
| } |
| |
| props = map[string]string{} |
| for _, line := range lines { |
| re := regexp.MustCompile("#.*") |
| line = re.ReplaceAllString(line, "") |
| line = strings.TrimSpace(line) |
| kv := strings.Split(line, "=") |
| if len(kv) != 2 { |
| // Invalid format; skip |
| continue |
| } |
| props[kv[0]] = kv[1] |
| } |
| |
| return props, nil |
| |
| } |
| |
| func WriteProps(path string, props map[string]string) error { |
| |
| file, err := os.Create(path) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "os.Create(%s) failed: %s\n", path, err) |
| errStr := wski18n.T("Whisk properties file write failure: {{.err}}", map[string]interface{}{"err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| defer file.Close() |
| |
| writer := bufio.NewWriter(file) |
| defer writer.Flush() |
| for key, value := range props { |
| line := fmt.Sprintf("%s=%s", strings.ToUpper(key), value) |
| _, err = fmt.Fprintln(writer, line) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "fmt.Fprintln() write to '%s' failed: %s\n", path, err) |
| errStr := wski18n.T("Whisk properties file write failure: {{.err}}", map[string]interface{}{"err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return werr |
| } |
| } |
| return nil |
| } |
| |
| func getSpaceGuid() (string, error) { |
| // get current props |
| props, err := ReadProps(Properties.PropsFile) |
| if err != nil { |
| whisk.Debug(whisk.DbgError, "readProps(%s) failed: %s\n", Properties.PropsFile, err) |
| errStr := wski18n.T("Unable to obtain the `auth` property value: {{.err}}", map[string]interface{}{"err": err}) |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return "", werr |
| } |
| |
| // get the auth key and parse out the space guid |
| if authToken, hasProp := props["AUTH"]; hasProp { |
| spaceGuid := strings.Split(authToken, ":")[0] |
| return spaceGuid, nil |
| } |
| |
| whisk.Debug(whisk.DbgError, "auth not found in properties: %#q\n", props) |
| errStr := wski18n.T("Auth key property value is not set") |
| werr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) |
| return "", werr |
| } |
| |
| func isBlockingTimeout(err error) bool { |
| var blockingTimeout bool |
| |
| whiskErr, isWhiskErr := err.(*whisk.WskError) |
| |
| if isWhiskErr && whiskErr.TimedOut { |
| blockingTimeout = true |
| } |
| |
| return blockingTimeout |
| } |
| |
| func isApplicationError(err error) bool { |
| var applicationError bool |
| |
| whiskErr, isWhiskErr := err.(*whisk.WskError) |
| |
| if isWhiskErr && whiskErr.ApplicationError { |
| applicationError = true |
| } |
| |
| return applicationError |
| } |
| |
| func contains(arr []string, element string) bool { |
| for _, e := range arr { |
| if e == element { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func ExitOnError(err error) { |
| if err == nil { |
| return |
| } |
| |
| whisk.Debug(whisk.DbgInfo, "err object type: %s\n", reflect.TypeOf(err).String()) |
| |
| T := wski18n.T |
| var exitCode int = 0 |
| var displayUsage bool = false |
| var displayMsg bool = false |
| var msgDisplayed bool = true |
| var displayPrefix bool = true |
| |
| werr, isWskError := err.(*whisk.WskError) // Is the err a WskError? |
| if isWskError { |
| whisk.Debug(whisk.DbgError, "Got a *whisk.WskError error: %#v\n", werr) |
| displayUsage = werr.DisplayUsage |
| displayMsg = werr.DisplayMsg |
| msgDisplayed = werr.MsgDisplayed |
| displayPrefix = werr.DisplayPrefix |
| exitCode = werr.ExitCode |
| } else { |
| whisk.Debug(whisk.DbgError, "Got some other error: %s\n", err) |
| fmt.Fprintf(os.Stderr, "%s\n", err) |
| |
| displayUsage = false // Cobra already displayed the usage message |
| exitCode = 1 |
| } |
| |
| outputStream := colorable.NewColorableStderr() |
| |
| // If the err msg should be displayed to the console and it has not already been |
| // displayed, display it now. |
| if displayMsg && !msgDisplayed && displayPrefix && exitCode != 0 { |
| fmt.Fprintf(outputStream, "%s%s\n", color.RedString(T("error: ")), err) |
| } else if displayMsg && !msgDisplayed && !displayPrefix && exitCode != 0 { |
| fmt.Fprintf(outputStream, "%s\n", err) |
| } else if displayMsg && !msgDisplayed && exitCode == 0 { |
| fmt.Fprintf(outputStream, "%s\n", err) |
| } |
| |
| // Displays usage |
| if displayUsage { |
| fmt.Fprintf(outputStream, T("Run '{{.Name}} --help' for usage.\n", |
| map[string]interface{}{"Name": WskCmd.CommandPath()})) |
| } |
| |
| os.Exit(exitCode) |
| } |