commands/activation.go (397 lines of code) (raw):

/* * 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 ( "errors" "fmt" "os" "os/signal" "strconv" "syscall" "time" "github.com/apache/openwhisk-cli/wski18n" "github.com/apache/openwhisk-client-go/whisk" "github.com/fatih/color" "github.com/spf13/cobra" ) const ( PollInterval = time.Second * 2 Delay = time.Second * 5 MAX_ACTIVATION_LIMIT = 200 DEFAULT_ACTIVATION_LIMIT = 30 ) var activationCmd = &cobra.Command{ Use: "activation", Short: wski18n.T("work with activations"), } var activationListCmd = &cobra.Command{ Use: "list [NAMESPACE or NAME]", Short: wski18n.T("list activations"), SilenceUsage: true, SilenceErrors: true, PreRunE: SetupClientConfig, RunE: func(cmd *cobra.Command, args []string) error { var err error var qualifiedName = new(QualifiedName) var orderedFilteredRow []whisk.ActivationFilteredRow if whiskErr := CheckArgs(args, 0, 1, "Activation list", wski18n.T("An optional namespace is the only valid argument.")); whiskErr != nil { return whiskErr } // Specifying an activation item name filter is optional if len(args) == 1 { whisk.Debug(whisk.DbgInfo, "Activation item name filter '%s' provided\n", args[0]) if qualifiedName, err = NewQualifiedName(args[0]); err != nil { return NewQualifiedNameError(args[0], err) } Client.Namespace = qualifiedName.GetNamespace() } options := &whisk.ActivationListOptions{ Name: qualifiedName.GetEntityName(), Limit: Flags.common.limit, Skip: Flags.common.skip, Upto: Flags.activation.upto, Since: Flags.activation.since, Docs: Flags.common.full, } activations, _, err := Client.Activations.List(options) if err != nil { whisk.Debug(whisk.DbgError, "Client.Activations.List() error: %s\n", err) errStr := wski18n.T("Unable to obtain the list of activations for namespace '{{.name}}': {{.err}}", map[string]interface{}{"name": getClientNamespace(), "err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } // When the --full (URL contains "?docs=true") option is specified, display the entire activation details if options.Docs == true { printFullActivationList(activations) } else { maxKindSize := max(len("Kind"), getLargestKindSize(activations)) maxStatusSize := max(len("Status"), getLargestStatusSize(activations)) // Header string should show "Datetime", "Activation ID", "Kind", "Start", "Duration", "Status", "Entity", with Kind and Status being // dynamically sized. The last column Entity will be sized correctly when printed, so no need to calculate size here headerFmt := "%-19s %-32s %-" + strconv.Itoa(maxKindSize) + "s %-6s%-10s %-" + strconv.Itoa(maxStatusSize) + "s %-6s\n" rowFmt := "%d-%02d-%02d %02d:%02d:%02d %-32s %-" + strconv.Itoa(maxKindSize) + "s %-5s %-10v %-" + strconv.Itoa(maxStatusSize) + "s %-" for i := 0; i < len(activations); i++ { orderedFilteredRow = append(orderedFilteredRow, whisk.ActivationFilteredRow{Row: activations[i], HeaderFmt: headerFmt, RowFmt: rowFmt}) } printList(orderedFilteredRow, false) // Default sorting for Activations are by creation time, hence sortByName is always false } return nil }, } var activationGetCmd = &cobra.Command{ Use: "get (ACTIVATION_ID | --last) [FIELD_FILTER]", Short: wski18n.T("get activation"), SilenceUsage: true, SilenceErrors: true, PreRunE: SetupClientConfig, RunE: func(cmd *cobra.Command, args []string) error { var field string var err error if args, err = lastFlag(args); err != nil { // Checks if any errors occurred in lastFlag(args) whisk.Debug(whisk.DbgError, "lastFlag(%#v) failed: %s\n", args, err) errStr := wski18n.T("Unable to get activation: {{.err}}", map[string]interface{}{"err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } if whiskErr := CheckArgs(args, 1, 2, "Activation get", wski18n.T("An activation ID is required.")); whiskErr != nil { return whiskErr } if len(args) > 1 { field = args[1] if !fieldExists(&whisk.Activation{}, field) { errMsg := wski18n.T("Invalid field filter '{{.arg}}'.", map[string]interface{}{"arg": field}) whiskErr := whisk.MakeWskError(errors.New(errMsg), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return whiskErr } } id := args[0] activation, _, err := Client.Activations.Get(id) if err != nil { whisk.Debug(whisk.DbgError, "Client.Activations.Get(%s) failed: %s\n", id, err) errStr := wski18n.T("Unable to get activation '{{.id}}': {{.err}}", map[string]interface{}{"id": id, "err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } if Flags.common.summary { fmt.Printf( wski18n.T("activation result for '/{{.namespace}}/{{.name}}' ({{.status}} at {{.time}})\n", map[string]interface{}{ "namespace": activation.Namespace, "name": activation.Name, "status": activation.Response.Status, "time": time.Unix(activation.End/1000, 0)})) printJSON(activation.Response.Result) } else if Flags.activation.logs { printStrippedActivationLogs(activation.Logs) } else { if len(field) > 0 { fmt.Fprintf(color.Output, wski18n.T("{{.ok}} got activation {{.id}}, displaying field {{.field}}\n", map[string]interface{}{"ok": color.GreenString("ok:"), "id": boldString(id), "field": boldString(field)})) printField(activation, field) } else { fmt.Fprintf(color.Output, wski18n.T("{{.ok}} got activation {{.id}}\n", map[string]interface{}{"ok": color.GreenString("ok:"), "id": boldString(id)})) printJSON(activation) } } return nil }, } var activationLogsCmd = &cobra.Command{ Use: "logs (ACTIVATION_ID | --last)", Short: wski18n.T("get the logs of an activation"), SilenceUsage: true, SilenceErrors: true, PreRunE: SetupClientConfig, RunE: func(cmd *cobra.Command, args []string) error { var err error if args, err = lastFlag(args); err != nil { // Checks if any errors occurred in lastFlag(args) whisk.Debug(whisk.DbgError, "lastFlag(%#v) failed: %s\n", args, err) errStr := wski18n.T("Unable to get logs for activation: {{.err}}", map[string]interface{}{"err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } if whiskErr := CheckArgs(args, 1, 1, "Activation logs", wski18n.T("An activation ID is required.")); whiskErr != nil { return whiskErr } id := args[0] activation, _, err := Client.Activations.Logs(id) if err != nil { whisk.Debug(whisk.DbgError, "Client.Activations.Logs(%s) failed: %s\n", id, err) errStr := wski18n.T("Unable to get logs for activation '{{.id}}': {{.err}}", map[string]interface{}{"id": id, "err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } if !Flags.activation.strip { printActivationLogs(activation.Logs) } else { printStrippedActivationLogs(activation.Logs) } return nil }, } var activationResultCmd = &cobra.Command{ Use: "result (ACTIVATION_ID | --last)", Short: "get the result of an activation", SilenceUsage: true, SilenceErrors: true, PreRunE: SetupClientConfig, RunE: func(cmd *cobra.Command, args []string) error { var err error if args, err = lastFlag(args); err != nil { // Checks if any errors occurred in lastFlag(args) whisk.Debug(whisk.DbgError, "lastFlag(%#v) failed: %s\n", args, err) errStr := wski18n.T("Unable to get result for activation: {{.err}}", map[string]interface{}{"err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } if whiskErr := CheckArgs(args, 1, 1, "Activation result", wski18n.T("An activation ID is required.")); whiskErr != nil { return whiskErr } id := args[0] result, _, err := Client.Activations.Result(id) if err != nil { whisk.Debug(whisk.DbgError, "Client.Activations.result(%s) failed: %s\n", id, err) errStr := wski18n.T("Unable to get result for activation '{{.id}}': {{.err}}", map[string]interface{}{"id": id, "err": err}) werr := whisk.MakeWskErrorFromWskError(errors.New(errStr), err, whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.NO_DISPLAY_USAGE) return werr } printJSON(result.Result) return nil }, } // lastFlag(args) retrieves the last activation with flag -l or --last // Param: Brings in []strings from args // Return: Returns a []string with the latest ID or the original args and any errors func lastFlag(args []string) ([]string, error) { if Flags.activation.last { options := &whisk.ActivationListOptions{ Limit: 1, Skip: 0, } activations, _, err := Client.Activations.List(options) if err != nil { // Checks Activations.List for errors when retrieving latest activation whisk.Debug(whisk.DbgError, "Client.Activations.List(%#v) error during lastFlag: %s\n", options, err) return args, err } if len(activations) == 0 { // Checks to see if there are activations available whisk.Debug(whisk.DbgError, "No activations found in activation list\n") errStr := wski18n.T("Activation list does not contain any activations.") whiskErr := whisk.MakeWskError(errors.New(errStr), whisk.EXIT_CODE_ERR_GENERAL, whisk.DISPLAY_MSG, whisk.DISPLAY_USAGE) return args, whiskErr } if len(args) == 0 { whisk.Debug(whisk.DbgInfo, "Appending most recent activation ID(%s) into args\n", activations[0].ActivationID) args = append(args, activations[0].ActivationID) } else { whisk.Debug(whisk.DbgInfo, "Appending most recent activation ID(%s) into args\n", activations[0].ActivationID) args = append(args, activations[0].ActivationID) whisk.Debug(whisk.DbgInfo, "Allocating appended ID to correct position in args\n") args[0], args[len(args)-1] = args[len(args)-1], args[0] // IDs should be located at args[0], if 1 or more arguments are given ID has to be moved to args[0] } } return args, nil } var activationPollCmd = &cobra.Command{ Use: "poll [ NAMESPACE | ACTION_NAME ]", Short: wski18n.T("poll continuously for log messages from currently running actions"), SilenceUsage: true, SilenceErrors: true, PreRunE: SetupClientConfig, RunE: func(cmd *cobra.Command, args []string) error { var name string var pollSince int64 // Represents an instant in time (in milliseconds since Jan 1 1970) if len(args) == 1 { name = args[0] } else if whiskErr := CheckArgs(args, 0, 1, "Activation poll", wski18n.T("An optional namespace is the only valid argument.")); whiskErr != nil { return whiskErr } c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) signal.Notify(c, syscall.SIGTERM) go func() { <-c fmt.Println(wski18n.T("Poll terminated")) os.Exit(1) }() fmt.Println(wski18n.T("Enter Ctrl-c to exit.")) // Map used to track activation records already displayed to the console reported := make(map[string]bool) if Flags.activation.sinceSeconds+ Flags.activation.sinceMinutes+ Flags.activation.sinceHours+ Flags.activation.sinceDays == 0 { options := &whisk.ActivationListOptions{ Limit: 1, Docs: true, } activationList, _, err := Client.Activations.List(options) if err != nil { whisk.Debug(whisk.DbgWarn, "Client.Activations.List() error: %s\n", err) whisk.Debug(whisk.DbgWarn, "Ignoring Client.Activations.List failure; polling for activations since 'now'\n") pollSince = time.Now().Unix() * 1000 // Convert to milliseconds } else { if len(activationList) > 0 { lastActivation := activationList[0] // Activation.Start is in milliseconds since Jan 1 1970 pollSince = lastActivation.Start + 1 // Use it's start time as the basis of the polling } } } else { pollSince = time.Now().Unix() * 1000 // Convert to milliseconds // ParseDuration takes a string like "2h45m15s"; create this duration string from the command arguments durationStr := fmt.Sprintf("%dh%dm%ds", Flags.activation.sinceHours+Flags.activation.sinceDays*24, Flags.activation.sinceMinutes, Flags.activation.sinceSeconds, ) duration, err := time.ParseDuration(durationStr) if err == nil { pollSince = pollSince - duration.Nanoseconds()/1000/1000 // Convert to milliseconds } else { whisk.Debug(whisk.DbgError, "time.ParseDuration(%s) failure: %s\n", durationStr, err) } } fmt.Printf(wski18n.T("Polling for activation logs\n")) whisk.Verbose("Polling starts from %s\n", time.Unix(pollSince/1000, 0)) localStartTime := time.Now() // Polling loop for { if Flags.activation.exit > 0 { localDuration := time.Since(localStartTime) if int(localDuration.Seconds()) > Flags.activation.exit { whisk.Debug(whisk.DbgInfo, "Poll time (%d seconds) expired; polling loop stopped\n", Flags.activation.exit) return nil } } whisk.Verbose("Polling for activations since %s\n", time.Unix(pollSince/1000, 0)) options := &whisk.ActivationListOptions{ Name: name, Since: pollSince, Docs: true, Limit: 0, Skip: 0, } activations, _, err := Client.Activations.List(options) if err != nil { whisk.Debug(whisk.DbgWarn, "Client.Activations.List() error: %s\n", err) whisk.Debug(whisk.DbgWarn, "Ignoring Client.Activations.List failure; continuing to poll for activations\n") continue } for _, activation := range activations { if reported[activation.ActivationID] == true { continue } else { fmt.Printf( wski18n.T("\nActivation: '{{.name}}' ({{.id}})\n", map[string]interface{}{"name": activation.Name, "id": activation.ActivationID})) printJSON(activation.Logs) reported[activation.ActivationID] = true } if activation.Start > pollSince { pollSince = activation.Start + 1 } } time.Sleep(time.Second * 2) } return nil }, } // Find the size needed for the Kind column when listing activations func getLargestKindSize(activations []whisk.Activation) int { var maxLen = 0 var curLen int var kind interface{} for i := 0; i < len(activations); i++ { kind = activations[i].Annotations.GetValue("kind") if kind == nil { kind = "unknown" } curLen = len(kind.(string)) if curLen > maxLen { maxLen = curLen } } return maxLen } // Find the size needed for the Status column when listing activations func getLargestStatusSize(activations []whisk.Activation) int { // The first array in the StatusCodes variable is "success" var maxLen = len(whisk.StatusCodes[0]) var curLen int for i := 0; i < len(activations); i++ { if activations[i].StatusCode > 0 && activations[i].StatusCode < len(whisk.StatusCodes) { curLen = len(whisk.StatusCodes[activations[i].StatusCode]) if curLen > maxLen { maxLen = curLen } } } return maxLen } func init() { activationListCmd.Flags().IntVarP(&Flags.common.skip, "skip", "s", 0, wski18n.T("exclude the first `SKIP` number of activations from the result")) activationListCmd.Flags().IntVarP(&Flags.common.limit, "limit", "l", DEFAULT_ACTIVATION_LIMIT, wski18n.T("only return `LIMIT` number of activations from the collection with a maximum LIMIT of {{.max}} activations", map[string]interface{}{"max": MAX_ACTIVATION_LIMIT})) activationListCmd.Flags().BoolVarP(&Flags.common.full, "full", "f", false, wski18n.T("include full activation description")) activationListCmd.Flags().Int64Var(&Flags.activation.upto, "upto", 0, wski18n.T("return activations with timestamps earlier than `UPTO`; measured in milliseconds since Th, 01, Jan 1970")) activationListCmd.Flags().Int64Var(&Flags.activation.since, "since", 0, wski18n.T("return activations with timestamps later than `SINCE`; measured in milliseconds since Th, 01, Jan 1970")) activationGetCmd.Flags().BoolVarP(&Flags.common.summary, "summary", "s", false, wski18n.T("summarize activation details")) activationGetCmd.Flags().BoolVarP(&Flags.activation.last, "last", "l", false, wski18n.T("retrieves the last activation")) activationGetCmd.Flags().BoolVarP(&Flags.activation.logs, "logs", "g", false, wski18n.T("emit only the logs, stripped of time stamps and stream identifier")) activationLogsCmd.Flags().BoolVarP(&Flags.activation.last, "last", "l", false, wski18n.T("retrieves the last activation")) activationLogsCmd.Flags().BoolVarP(&Flags.activation.strip, "strip", "r", false, wski18n.T("strip timestamp and stream information")) activationResultCmd.Flags().BoolVarP(&Flags.activation.last, "last", "l", false, wski18n.T("retrieves the last activation")) activationPollCmd.Flags().IntVarP(&Flags.activation.exit, "exit", "e", 0, wski18n.T("stop polling after `SECONDS` seconds")) activationPollCmd.Flags().IntVar(&Flags.activation.sinceSeconds, "since-seconds", 0, wski18n.T("start polling for activations `SECONDS` seconds ago")) activationPollCmd.Flags().IntVar(&Flags.activation.sinceMinutes, "since-minutes", 0, wski18n.T("start polling for activations `MINUTES` minutes ago")) activationPollCmd.Flags().IntVar(&Flags.activation.sinceHours, "since-hours", 0, wski18n.T("start polling for activations `HOURS` hours ago")) activationPollCmd.Flags().IntVar(&Flags.activation.sinceDays, "since-days", 0, wski18n.T("start polling for activations `DAYS` days ago")) activationCmd.AddCommand( activationListCmd, activationGetCmd, activationLogsCmd, activationResultCmd, activationPollCmd, ) }