diff --git a/cmd/argo/common.go b/cmd/argo/common.go new file mode 100644 index 000000000..ee6fdd5ea --- /dev/null +++ b/cmd/argo/common.go @@ -0,0 +1,117 @@ +package argo + +import ( + "fmt" + "log" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + wfclientset "github.com/argoproj/argo/pkg/client/clientset/versioned" + "github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1" +) + +// Global variables +var ( + restConfig *rest.Config + clientConfig clientcmd.ClientConfig + clientset *kubernetes.Clientset + wfClient v1alpha1.WorkflowInterface + jobStatusIconMap map[wfv1.NodePhase]string + noColor bool + namespace string +) + +func init() { + cobra.OnInitialize(initializeSession) +} + +// ANSI escape codes +const ( + escape = "\x1b" + noFormat = 0 + Bold = 1 + FgBlack = 30 + FgRed = 31 + FgGreen = 32 + FgYellow = 33 + FgBlue = 34 + FgMagenta = 35 + FgCyan = 36 + FgWhite = 37 + FgDefault = 39 +) + +func initializeSession() { + jobStatusIconMap = map[wfv1.NodePhase]string{ + wfv1.NodePending: ansiFormat("◷", FgYellow), + wfv1.NodeRunning: ansiFormat("●", FgCyan), + wfv1.NodeSucceeded: ansiFormat("✔", FgGreen), + wfv1.NodeSkipped: ansiFormat("○", FgDefault), + wfv1.NodeFailed: ansiFormat("✖", FgRed), + wfv1.NodeError: ansiFormat("⚠", FgRed), + } +} + +func initKubeClient() *kubernetes.Clientset { + if clientset != nil { + return clientset + } + var err error + restConfig, err = clientConfig.ClientConfig() + if err != nil { + log.Fatal(err) + } + + // create the clientset + clientset, err = kubernetes.NewForConfig(restConfig) + if err != nil { + log.Fatal(err) + } + return clientset +} + +// InitWorkflowClient creates a new client for the Kubernetes Workflow CRD. +func InitWorkflowClient(ns ...string) v1alpha1.WorkflowInterface { + if wfClient != nil && (len(ns) == 0 || ns[0] == namespace) { + return wfClient + } + initKubeClient() + var err error + if len(ns) > 0 { + namespace = ns[0] + } else { + namespace, _, err = clientConfig.Namespace() + if err != nil { + log.Fatal(err) + } + } + wfcs := wfclientset.NewForConfigOrDie(restConfig) + wfClient = wfcs.ArgoprojV1alpha1().Workflows(namespace) + return wfClient +} + +// ansiFormat wraps ANSI escape codes to a string to format the string to a desired color. +// NOTE: we still apply formatting even if there is no color formatting desired. +// The purpose of doing this is because when we apply ANSI color escape sequences to our +// output, this confuses the tabwriter library which miscalculates widths of columns and +// misaligns columns. By always applying a ANSI escape sequence (even when we don't want +// color, it provides more consistent string lengths so that tabwriter can calculate +// widths correctly. +func ansiFormat(s string, codes ...int) string { + if noColor || os.Getenv("TERM") == "dumb" || len(codes) == 0 { + return s + } + codeStrs := make([]string, len(codes)) + for i, code := range codes { + codeStrs[i] = strconv.Itoa(code) + } + sequence := strings.Join(codeStrs, ";") + return fmt.Sprintf("%s[%sm%s%s[%dm", escape, sequence, s, escape, noFormat) +} diff --git a/cmd/argo/delete.go b/cmd/argo/delete.go new file mode 100644 index 000000000..116ca2b5d --- /dev/null +++ b/cmd/argo/delete.go @@ -0,0 +1,84 @@ +package argo + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/argoproj/argo/workflow/common" + argotime "github.com/argoproj/pkg/time" +) + +var ( + completedWorkflowListOption = metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=true", common.LabelKeyCompleted), + } +) + +// NewDeleteCommand returns a new instance of an `argo delete` command +func NewDeleteCommand() *cobra.Command { + var ( + all bool + completed bool + older string + ) + + var command = &cobra.Command{ + Use: "delete WORKFLOW", + Short: "delete a workflow and its associated pods", + Run: func(cmd *cobra.Command, args []string) { + wfClient = InitWorkflowClient() + if all { + deleteWorkflows(metav1.ListOptions{}, nil) + } else if older != "" { + olderTime, err := argotime.ParseSince(older) + if err != nil { + log.Fatal(err) + } + deleteWorkflows(completedWorkflowListOption, olderTime) + } else if completed { + deleteWorkflows(completedWorkflowListOption, nil) + } else { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + for _, wfName := range args { + deleteWorkflow(wfName) + } + } + }, + } + + command.Flags().BoolVar(&all, "all", false, "Delete all workflows") + command.Flags().BoolVar(&completed, "completed", false, "Delete completed workflows") + command.Flags().StringVar(&older, "older", "", "Delete completed workflows older than the specified duration (e.g. 10m, 3h, 1d)") + return command +} + +func deleteWorkflow(wfName string) { + err := wfClient.Delete(wfName, &metav1.DeleteOptions{}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Workflow '%s' deleted\n", wfName) +} + +func deleteWorkflows(options metav1.ListOptions, older *time.Time) { + wfList, err := wfClient.List(options) + if err != nil { + log.Fatal(err) + } + for _, wf := range wfList.Items { + if older != nil { + if wf.Status.FinishedAt.IsZero() || wf.Status.FinishedAt.After(*older) { + continue + } + } + deleteWorkflow(wf.ObjectMeta.Name) + } +} diff --git a/cmd/argo/get.go b/cmd/argo/get.go new file mode 100644 index 000000000..c3d2e8ea1 --- /dev/null +++ b/cmd/argo/get.go @@ -0,0 +1,488 @@ +package argo + +import ( + "encoding/json" + "fmt" + "log" + "os" + "strings" + "text/tabwriter" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/humanize" + + env "github.com/ian-howell/airshipctl/pkg/environment" +) + +const onExitSuffix = "onExit" + +type getFlags struct { + output string + status string +} + +func NewGetCommand() *cobra.Command { + var ( + getArgs getFlags + ) + + var command = &cobra.Command{ + Use: "get WORKFLOW", + Short: "display details about a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + wfClient := InitWorkflowClient() + wf, err := wfClient.Get(args[0], metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + err = util.DecompressWorkflow(wf) + if err != nil { + log.Fatal(err) + } + printWorkflow(wf, getArgs.output, getArgs.status) + }, + } + + command.Flags().StringVarP(&getArgs.output, "output", "o", "", "Output format. One of: json|yaml|wide") + command.Flags().BoolVar(&noColor, "no-color", false, "Disable colorized output") + command.Flags().StringVar(&getArgs.status, "status", "", "Filter by status (Pending, Running, Succeeded, Skipped, Failed, Error)") + return command +} + +func printWorkflow(wf *wfv1.Workflow, output, status string) { + getArgs := getFlags{ + output: output, + status: status, + } + switch getArgs.output { + case env.NameOnly: + fmt.Println(wf.ObjectMeta.Name) + case env.JSON: + outBytes, _ := json.MarshalIndent(wf, "", " ") + fmt.Println(string(outBytes)) + case env.YAML: + outBytes, _ := yaml.Marshal(wf) + fmt.Print(string(outBytes)) + case env.Wide, env.Default: + printWorkflowHelper(wf, getArgs) + default: + log.Fatalf("Unknown output format: %s", getArgs.output) + } +} + +func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) { + const fmtStr = "%-20s %v\n" + fmt.Printf(fmtStr, "Name:", wf.ObjectMeta.Name) + fmt.Printf(fmtStr, "Namespace:", wf.ObjectMeta.Namespace) + serviceAccount := wf.Spec.ServiceAccountName + if serviceAccount == "" { + serviceAccount = "default" + } + fmt.Printf(fmtStr, "ServiceAccount:", serviceAccount) + fmt.Printf(fmtStr, "Status:", workflowStatus(wf)) + if wf.Status.Message != "" { + fmt.Printf(fmtStr, "Message:", wf.Status.Message) + } + fmt.Printf(fmtStr, "Created:", humanize.Timestamp(wf.ObjectMeta.CreationTimestamp.Time)) + if !wf.Status.StartedAt.IsZero() { + fmt.Printf(fmtStr, "Started:", humanize.Timestamp(wf.Status.StartedAt.Time)) + } + if !wf.Status.FinishedAt.IsZero() { + fmt.Printf(fmtStr, "Finished:", humanize.Timestamp(wf.Status.FinishedAt.Time)) + } + if !wf.Status.StartedAt.IsZero() { + fmt.Printf(fmtStr, "Duration:", humanize.RelativeDuration(wf.Status.StartedAt.Time, wf.Status.FinishedAt.Time)) + } + + if len(wf.Spec.Arguments.Parameters) > 0 { + fmt.Printf(fmtStr, "Parameters:", "") + for _, param := range wf.Spec.Arguments.Parameters { + if param.Value == nil { + continue + } + fmt.Printf(fmtStr, " "+param.Name+":", *param.Value) + } + } + if wf.Status.Outputs != nil { + //fmt.Printf(fmtStr, "Outputs:", "") + if len(wf.Status.Outputs.Parameters) > 0 { + fmt.Printf(fmtStr, "Output Parameters:", "") + for _, param := range wf.Status.Outputs.Parameters { + fmt.Printf(fmtStr, " "+param.Name+":", *param.Value) + } + } + if len(wf.Status.Outputs.Artifacts) > 0 { + fmt.Printf(fmtStr, "Output Artifacts:", "") + for _, art := range wf.Status.Outputs.Artifacts { + if art.S3 != nil { + fmt.Printf(fmtStr, " "+art.Name+":", art.S3.String()) + } else if art.Artifactory != nil { + fmt.Printf(fmtStr, " "+art.Name+":", art.Artifactory.String()) + } + } + } + } + printTree := true + if wf.Status.Nodes == nil { + printTree = false + } else if _, ok := wf.Status.Nodes[wf.ObjectMeta.Name]; !ok { + printTree = false + } + if printTree { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Println() + // apply a dummy FgDefault format to align tabwriter with the rest of the columns + if getArgs.output == env.Wide { + fmt.Fprintf(w, "%s\tPODNAME\tDURATION\tARTIFACTS\tMESSAGE\n", ansiFormat("STEP", FgDefault)) + } else { + fmt.Fprintf(w, "%s\tPODNAME\tDURATION\tMESSAGE\n", ansiFormat("STEP", FgDefault)) + } + + // Convert Nodes to Render Trees + roots := convertToRenderTrees(wf) + + // Print main and onExit Trees + mainRoot := roots[wf.ObjectMeta.Name] + mainRoot.renderNodes(w, wf, 0, " ", " ", getArgs) + + onExitID := wf.NodeID(wf.ObjectMeta.Name + "." + onExitSuffix) + if onExitRoot, ok := roots[onExitID]; ok { + fmt.Fprintf(w, "\t\t\t\t\t\n") + onExitRoot.renderNodes(w, wf, 0, " ", " ", getArgs) + } + _ = w.Flush() + } +} + +type nodeInfoInterface interface { + getID() string + getNodeStatus(wf *wfv1.Workflow) wfv1.NodeStatus + getStartTime(wf *wfv1.Workflow) metav1.Time +} + +type nodeInfo struct { + id string +} + +func (n *nodeInfo) getID() string { + return n.id +} + +func (n *nodeInfo) getNodeStatus(wf *wfv1.Workflow) wfv1.NodeStatus { + return wf.Status.Nodes[n.id] +} + +func (n *nodeInfo) getStartTime(wf *wfv1.Workflow) metav1.Time { + return wf.Status.Nodes[n.id].StartedAt +} + +// Interface to represent Nodes in render form types +type renderNode interface { + // Render this renderNode and its children + renderNodes(w *tabwriter.Writer, wf *wfv1.Workflow, depth int, nodePrefix string, childPrefix string, getArgs getFlags) + nodeInfoInterface +} + +// Currently this is Pod or Resource Nodes +type executionNode struct { + nodeInfo +} + +// Currently this is the step groups or retry nodes +type nonBoundaryParentNode struct { + nodeInfo + children []renderNode // Can be boundaryNode or executionNode +} + +// Currently this is the virtual Template node +type boundaryNode struct { + nodeInfo + boundaryContained []renderNode // Can be nonBoundaryParent or executionNode or boundaryNode +} + +func isBoundaryNode(node wfv1.NodeType) bool { + return (node == wfv1.NodeTypeDAG) || (node == wfv1.NodeTypeSteps) +} + +func isNonBoundaryParentNode(node wfv1.NodeType) bool { + return (node == wfv1.NodeTypeStepGroup) || (node == wfv1.NodeTypeRetry) +} + +func isExecutionNode(node wfv1.NodeType) bool { + return (node == wfv1.NodeTypePod) || (node == wfv1.NodeTypeSkipped) || (node == wfv1.NodeTypeSuspend) +} + +func insertSorted(wf *wfv1.Workflow, sortedArray []renderNode, item renderNode) []renderNode { + insertTime := item.getStartTime(wf) + var index int + for index = 0; index < len(sortedArray); index++ { + existingItem := sortedArray[index] + t := existingItem.getStartTime(wf) + if insertTime.Before(&t) { + break + } else if insertTime.Equal(&t) { + // If they are equal apply alphabetical order so we + // get some consistent printing + insertName := item.getNodeStatus(wf).DisplayName + equalName := existingItem.getNodeStatus(wf).DisplayName + if insertName < equalName { + break + } + } + } + sortedArray = append(sortedArray, nil) + copy(sortedArray[index+1:], sortedArray[index:]) + sortedArray[index] = item + return sortedArray +} + +// Attach render node n to its parent based on what has been parsed previously +// In some cases add it to list of things that still needs to be attached to parent +// Return if I am a possible root +func attachToParent(wf *wfv1.Workflow, n renderNode, + nonBoundaryParentChildrenMap map[string]*nonBoundaryParentNode, boundaryID string, + boundaryNodeMap map[string]*boundaryNode, parentBoundaryMap map[string][]renderNode) bool { + + // Check first if I am a child of a nonBoundaryParent + // that implies I attach to that instead of my boundary. This was already + // figured out in Pass 1 + if nonBoundaryParent, ok := nonBoundaryParentChildrenMap[n.getID()]; ok { + nonBoundaryParent.children = insertSorted(wf, nonBoundaryParent.children, n) + return false + } + + // If I am not attached to a nonBoundaryParent and I have no Boundary ID then + // I am a possible root + if boundaryID == "" { + return true + } + if parentBoundary, ok := boundaryNodeMap[boundaryID]; ok { + parentBoundary.boundaryContained = insertSorted(wf, parentBoundary.boundaryContained, n) + } else { + // put ourselves to be added by the parent when we get to it later + if _, ok := parentBoundaryMap[boundaryID]; !ok { + parentBoundaryMap[boundaryID] = make([]renderNode, 0) + } + parentBoundaryMap[boundaryID] = append(parentBoundaryMap[boundaryID], n) + } + return false +} + +// This takes the map of NodeStatus and converts them into a forrest +// of trees of renderNodes and returns the set of roots for each tree +func convertToRenderTrees(wf *wfv1.Workflow) map[string]renderNode { + + renderTreeRoots := make(map[string]renderNode) + + // Used to store all boundary nodes so future render children can attach + // Maps node Name -> *boundaryNode + boundaryNodeMap := make(map[string]*boundaryNode) + // Used to store children of a boundary node that has not been parsed yet + // Maps boundary Node name -> array of render Children + parentBoundaryMap := make(map[string][]renderNode) + + // Used to store Non Boundary Parent nodes so render children can attach + // Maps non Boundary Parent Node name -> *nonBoundaryParentNode + nonBoundaryParentMap := make(map[string]*nonBoundaryParentNode) + // Used to store children which have a Non Boundary Parent from rendering perspective + // Maps non Boundary render Children name -> *nonBoundaryParentNode + nonBoundaryParentChildrenMap := make(map[string]*nonBoundaryParentNode) + + // We have to do a 2 pass approach because anything that is a child + // of a nonBoundaryParent and also has a boundaryID we may not know which + // parent to attach to if we didn't see the nonBoundaryParent earlier + // in a 1 pass strategy + + // 1st Pass Process enough of nonBoundaryParent nodes to know all their children + for id, status := range wf.Status.Nodes { + if status.Type == "" { + log.Fatal("Missing node type in status node. Cannot get workflows created with Argo <= 2.0 using the default or wide output option.") + return nil + } + if isNonBoundaryParentNode(status.Type) { + n := nonBoundaryParentNode{nodeInfo: nodeInfo{id: id}} + nonBoundaryParentMap[id] = &n + + for _, child := range status.Children { + nonBoundaryParentChildrenMap[child] = &n + } + } + } + + // 2nd Pass process everything + for id, status := range wf.Status.Nodes { + switch { + case isBoundaryNode(status.Type): + n := boundaryNode{nodeInfo: nodeInfo{id: id}} + boundaryNodeMap[id] = &n + // Attach to my parent if needed + if attachToParent(wf, &n, nonBoundaryParentChildrenMap, + status.BoundaryID, boundaryNodeMap, parentBoundaryMap) { + renderTreeRoots[n.getID()] = &n + } + // Attach nodes who are in my boundary already seen before me to me + for _, val := range parentBoundaryMap[id] { + n.boundaryContained = insertSorted(wf, n.boundaryContained, val) + } + case isNonBoundaryParentNode(status.Type): + nPtr, ok := nonBoundaryParentMap[id] + if !ok { + log.Fatal("Unable to lookup node " + id) + return nil + } + // Attach to my parent if needed + if attachToParent(wf, nPtr, nonBoundaryParentChildrenMap, + status.BoundaryID, boundaryNodeMap, parentBoundaryMap) { + renderTreeRoots[nPtr.getID()] = nPtr + } + // All children attach directly to the nonBoundaryParents since they are already created + // in pass 1 so no need to do that here + case isExecutionNode(status.Type): + n := executionNode{nodeInfo: nodeInfo{id: id}} + // Attach to my parent if needed + if attachToParent(wf, &n, nonBoundaryParentChildrenMap, + status.BoundaryID, boundaryNodeMap, parentBoundaryMap) { + renderTreeRoots[n.getID()] = &n + } + // Execution nodes don't have other render nodes as children + } + } + + return renderTreeRoots +} + +// This function decides if a Node will be filtered from rendering and returns +// two things. First argument tells if the node is filtered and second argument +// tells whether the children need special indentation due to filtering +// Return Values: (is node filtered, do children need special indent) +func filterNode(node wfv1.NodeStatus) (bool, bool) { + if node.Type == wfv1.NodeTypeRetry && len(node.Children) == 1 { + return true, false + } else if node.Type == wfv1.NodeTypeStepGroup { + return true, true + } + return false, false +} + +// Render the child of a given node based on information about the parent such as: +// whether it was filtered and does this child need special indent +func renderChild(w *tabwriter.Writer, wf *wfv1.Workflow, nInfo renderNode, depth int, + nodePrefix string, childPrefix string, parentFiltered bool, + childIndex int, maxIndex int, childIndent bool, getArgs getFlags) { + var part, subp string + if parentFiltered && childIndent { + if maxIndex == 0 { + part = "--" + subp = " " + } else if childIndex == 0 { + part = "·-" + subp = "| " + } else if childIndex == maxIndex { + part = "└-" + subp = " " + } else { + part = "├-" + subp = "| " + } + } else if !parentFiltered { + if childIndex == maxIndex { + part = "└-" + subp = " " + } else { + part = "├-" + subp = "| " + } + } + var childNodePrefix, childChldPrefix string + if !parentFiltered { + depth = depth + 1 + childNodePrefix = childPrefix + part + childChldPrefix = childPrefix + subp + } else { + if childIndex == 0 { + childNodePrefix = nodePrefix + part + } else { + childNodePrefix = childPrefix + part + } + childChldPrefix = childPrefix + subp + } + nInfo.renderNodes(w, wf, depth, childNodePrefix, childChldPrefix, getArgs) +} + +// Main method to print information of node in get +func printNode(w *tabwriter.Writer, node wfv1.NodeStatus, nodePrefix string, getArgs getFlags) { + if getArgs.status != "" && string(node.Phase) != getArgs.status { + return + } + nodeName := fmt.Sprintf("%s %s", jobStatusIconMap[node.Phase], node.DisplayName) + var args []interface{} + duration := humanize.RelativeDurationShort(node.StartedAt.Time, node.FinishedAt.Time) + if node.Type == wfv1.NodeTypePod { + args = []interface{}{nodePrefix, nodeName, node.ID, duration, node.Message} + } else { + args = []interface{}{nodePrefix, nodeName, "", "", node.Message} + } + if getArgs.output == env.Wide { + msg := args[len(args)-1] + args[len(args)-1] = getArtifactsString(node) + args = append(args, msg) + fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\t%s\n", args...) + } else { + fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n", args...) + } +} + +// renderNodes for each renderNode Type +// boundaryNode +func (nodeInfo *boundaryNode) renderNodes(w *tabwriter.Writer, wf *wfv1.Workflow, depth int, nodePrefix string, childPrefix string, getArgs getFlags) { + filtered, childIndent := filterNode(nodeInfo.getNodeStatus(wf)) + if !filtered { + printNode(w, nodeInfo.getNodeStatus(wf), nodePrefix, getArgs) + } + + for i, nInfo := range nodeInfo.boundaryContained { + renderChild(w, wf, nInfo, depth, nodePrefix, childPrefix, filtered, i, + len(nodeInfo.boundaryContained)-1, childIndent, getArgs) + } +} + +// nonBoundaryParentNode +func (nodeInfo *nonBoundaryParentNode) renderNodes(w *tabwriter.Writer, wf *wfv1.Workflow, depth int, nodePrefix string, childPrefix string, getArgs getFlags) { + filtered, childIndent := filterNode(nodeInfo.getNodeStatus(wf)) + if !filtered { + printNode(w, nodeInfo.getNodeStatus(wf), nodePrefix, getArgs) + } + + for i, nInfo := range nodeInfo.children { + renderChild(w, wf, nInfo, depth, nodePrefix, childPrefix, filtered, i, + len(nodeInfo.children)-1, childIndent, getArgs) + } +} + +// executionNode +func (nodeInfo *executionNode) renderNodes(w *tabwriter.Writer, wf *wfv1.Workflow, depth int, nodePrefix string, childPrefix string, getArgs getFlags) { + filtered, _ := filterNode(nodeInfo.getNodeStatus(wf)) + if !filtered { + printNode(w, nodeInfo.getNodeStatus(wf), nodePrefix, getArgs) + } +} + +func getArtifactsString(node wfv1.NodeStatus) string { + if node.Outputs == nil { + return "" + } + artNames := []string{} + for _, art := range node.Outputs.Artifacts { + artNames = append(artNames, art.Name) + } + return strings.Join(artNames, ",") +} diff --git a/cmd/argo/lint.go b/cmd/argo/lint.go new file mode 100644 index 000000000..6828fd3a2 --- /dev/null +++ b/cmd/argo/lint.go @@ -0,0 +1,59 @@ +package argo + +import ( + "fmt" + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + cmdutil "github.com/argoproj/argo/util/cmd" + "github.com/argoproj/argo/workflow/validate" +) + +func NewLintCommand() *cobra.Command { + var ( + strict bool + ) + var command = &cobra.Command{ + Use: "lint (DIRECTORY | FILE1 FILE2 FILE3...)", + Short: "validate a file or directory of workflow manifests", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + validateDir := cmdutil.MustIsDir(args[0]) + var err error + if validateDir { + if len(args) > 1 { + fmt.Printf("Validation of a single directory supported") + os.Exit(1) + } + fmt.Printf("Verifying all workflow manifests in directory: %s\n", args[0]) + err = validate.LintWorkflowDir(args[0], strict) + } else { + yamlFiles := make([]string, 0) + for _, filePath := range args { + if cmdutil.MustIsDir(filePath) { + fmt.Printf("Validate against a list of files or a single directory, not both") + os.Exit(1) + } + yamlFiles = append(yamlFiles, filePath) + } + for _, yamlFile := range yamlFiles { + err = validate.LintWorkflowFile(yamlFile, strict) + if err != nil { + break + } + } + } + if err != nil { + log.Fatal(err) + } + fmt.Printf("Workflow manifests validated\n") + }, + } + command.Flags().BoolVar(&strict, "strict", true, "perform strict workflow validatation") + return command +} diff --git a/cmd/argo/list.go b/cmd/argo/list.go new file mode 100644 index 000000000..d130a3eee --- /dev/null +++ b/cmd/argo/list.go @@ -0,0 +1,229 @@ +package argo + +import ( + "fmt" + "log" + "os" + "sort" + "strings" + "text/tabwriter" + "time" + + "github.com/spf13/cobra" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1" + "github.com/argoproj/argo/workflow/common" + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/humanize" + argotime "github.com/argoproj/pkg/time" + + env "github.com/ian-howell/airshipctl/pkg/environment" +) + +type listFlags struct { + allNamespaces bool // --all-namespaces + status string // --status + completed bool // --completed + running bool // --running + output string // --output + since string // --since +} + +func NewListCommand() *cobra.Command { + var ( + listArgs listFlags + ) + var command = &cobra.Command{ + Use: "list", + Short: "list workflows", + Run: func(cmd *cobra.Command, args []string) { + var wfClient v1alpha1.WorkflowInterface + if listArgs.allNamespaces { + wfClient = InitWorkflowClient(apiv1.NamespaceAll) + } else { + wfClient = InitWorkflowClient() + } + listOpts := metav1.ListOptions{} + labelSelector := labels.NewSelector() + if listArgs.status != "" { + req, _ := labels.NewRequirement(common.LabelKeyPhase, selection.In, strings.Split(listArgs.status, ",")) + labelSelector = labelSelector.Add(*req) + } + if listArgs.completed { + req, _ := labels.NewRequirement(common.LabelKeyCompleted, selection.Equals, []string{"true"}) + labelSelector = labelSelector.Add(*req) + } + if listArgs.running { + req, _ := labels.NewRequirement(common.LabelKeyCompleted, selection.NotEquals, []string{"true"}) + labelSelector = labelSelector.Add(*req) + } + listOpts.LabelSelector = labelSelector.String() + wfList, err := wfClient.List(listOpts) + if err != nil { + log.Fatal(err) + } + var workflows []wfv1.Workflow + if listArgs.since == "" { + workflows = wfList.Items + } else { + workflows = make([]wfv1.Workflow, 0) + minTime, err := argotime.ParseSince(listArgs.since) + if err != nil { + log.Fatal(err) + } + for _, wf := range wfList.Items { + if wf.Status.FinishedAt.IsZero() || wf.ObjectMeta.CreationTimestamp.After(*minTime) { + workflows = append(workflows, wf) + } + } + } + sort.Sort(ByFinishedAt(workflows)) + + switch listArgs.output { + case env.Default, env.Wide: + printTable(workflows, &listArgs) + case env.NameOnly: + for _, wf := range workflows { + fmt.Println(wf.ObjectMeta.Name) + } + default: + log.Fatalf("Unknown output mode: %s", listArgs.output) + } + }, + } + command.Flags().BoolVar(&listArgs.allNamespaces, "all-namespaces", false, "Show workflows from all namespaces") + command.Flags().StringVar(&listArgs.status, "status", "", "Filter by status (comma separated)") + command.Flags().BoolVar(&listArgs.completed, "completed", false, "Show only completed workflows") + command.Flags().BoolVar(&listArgs.running, "running", false, "Show only running workflows") + command.Flags().StringVarP(&listArgs.output, "output", "o", "", "Output format. One of: wide|name") + command.Flags().StringVar(&listArgs.since, "since", "", "Show only workflows newer than a relative duration") + return command +} + +func printTable(wfList []wfv1.Workflow, listArgs *listFlags) { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) + if listArgs.allNamespaces { + fmt.Fprint(w, "NAMESPACE\t") + } + fmt.Fprint(w, "NAME\tSTATUS\tAGE\tDURATION\tPRIORITY") + if listArgs.output == env.Wide { + fmt.Fprint(w, "\tP/R/C\tPARAMETERS") + } + fmt.Fprint(w, "\n") + for _, wf := range wfList { + ageStr := humanize.RelativeDurationShort(wf.ObjectMeta.CreationTimestamp.Time, time.Now()) + durationStr := humanize.RelativeDurationShort(wf.Status.StartedAt.Time, wf.Status.FinishedAt.Time) + if listArgs.allNamespaces { + fmt.Fprintf(w, "%s\t", wf.ObjectMeta.Namespace) + } + var priority int + if wf.Spec.Priority != nil { + priority = int(*wf.Spec.Priority) + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d", wf.ObjectMeta.Name, workflowStatus(&wf), ageStr, durationStr, priority) + if listArgs.output == env.Wide { + pending, running, completed := countPendingRunningCompleted(&wf) + fmt.Fprintf(w, "\t%d/%d/%d", pending, running, completed) + fmt.Fprintf(w, "\t%s", parameterString(wf.Spec.Arguments.Parameters)) + } + fmt.Fprintf(w, "\n") + } + _ = w.Flush() +} + +func countPendingRunningCompleted(wf *wfv1.Workflow) (int, int, int) { + pending := 0 + running := 0 + completed := 0 + err := util.DecompressWorkflow(wf) + if err != nil { + log.Fatal(err) + } + for _, node := range wf.Status.Nodes { + tmpl := wf.GetTemplate(node.TemplateName) + if tmpl == nil || !tmpl.IsPodType() { + continue + } + if node.Completed() { + completed++ + } else if node.Phase == wfv1.NodeRunning { + running++ + } else { + pending++ + } + } + return pending, running, completed +} + +// parameterString returns a human readable display string of the parameters, truncating if necessary +func parameterString(params []wfv1.Parameter) string { + truncateString := func(str string, num int) string { + bnoden := str + if len(str) > num { + if num > 3 { + num -= 3 + } + bnoden = str[0:num-15] + "..." + str[len(str)-15:] + } + return bnoden + } + + pStrs := make([]string, 0) + for _, p := range params { + if p.Value != nil { + str := fmt.Sprintf("%s=%s", p.Name, truncateString(*p.Value, 50)) + pStrs = append(pStrs, str) + } + } + return strings.Join(pStrs, ",") +} + +// ByFinishedAt is a sort interface which sorts running jobs earlier before considering FinishedAt +type ByFinishedAt []wfv1.Workflow + +func (f ByFinishedAt) Len() int { return len(f) } +func (f ByFinishedAt) Swap(i, j int) { f[i], f[j] = f[j], f[i] } +func (f ByFinishedAt) Less(i, j int) bool { + iStart := f[i].ObjectMeta.CreationTimestamp + iFinish := f[i].Status.FinishedAt + jStart := f[j].ObjectMeta.CreationTimestamp + jFinish := f[j].Status.FinishedAt + if iFinish.IsZero() && jFinish.IsZero() { + return !iStart.Before(&jStart) + } + if iFinish.IsZero() && !jFinish.IsZero() { + return true + } + if !iFinish.IsZero() && jFinish.IsZero() { + return false + } + return jFinish.Before(&iFinish) +} + +// workflowStatus returns a human readable inferred workflow status based on workflow phase and conditions +func workflowStatus(wf *wfv1.Workflow) wfv1.NodePhase { + switch wf.Status.Phase { + case wfv1.NodeRunning: + if util.IsWorkflowSuspended(wf) { + return "Running (Suspended)" + } + return wf.Status.Phase + case wfv1.NodeFailed: + if util.IsWorkflowTerminated(wf) { + return "Failed (Terminated)" + } + return wf.Status.Phase + case "", wfv1.NodePending: + if !wf.ObjectMeta.CreationTimestamp.IsZero() { + return wfv1.NodePending + } + return "Unknown" + default: + return wf.Status.Phase + } +} diff --git a/cmd/argo/logs.go b/cmd/argo/logs.go new file mode 100644 index 000000000..db5ce360f --- /dev/null +++ b/cmd/argo/logs.go @@ -0,0 +1,392 @@ +package argo + +import ( + "bufio" + "context" + "fmt" + "hash/fnv" + "math" + "os" + "strconv" + "strings" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + pkgwatch "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/watch" + + "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + workflowv1 "github.com/argoproj/argo/pkg/client/clientset/versioned/typed/workflow/v1alpha1" + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/errors" +) + +type logEntry struct { + displayName string + pod string + time time.Time + line string +} + +func NewLogsCommand() *cobra.Command { + var ( + printer logPrinter + workflow bool + since string + sinceTime string + tail int64 + ) + var command = &cobra.Command{ + Use: "logs POD/WORKFLOW", + Short: "view logs of a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + conf, err := clientConfig.ClientConfig() + errors.CheckError(err) + printer.kubeClient = kubernetes.NewForConfigOrDie(conf) + if tail > 0 { + printer.tail = &tail + } + if sinceTime != "" { + parsedTime, err := time.Parse(time.RFC3339, sinceTime) + errors.CheckError(err) + meta1Time := metav1.NewTime(parsedTime) + printer.sinceTime = &meta1Time + } else if since != "" { + parsedSince, err := strconv.ParseInt(since, 10, 64) + errors.CheckError(err) + printer.sinceSeconds = &parsedSince + } + + if workflow { + err = printer.PrintWorkflowLogs(args[0]) + errors.CheckError(err) + } else { + err = printer.PrintPodLogs(args[0]) + errors.CheckError(err) + } + + }, + } + command.Flags().StringVarP(&printer.container, "container", "c", "main", "Print the logs of this container") + command.Flags().BoolVarP(&workflow, "workflow", "w", false, "Specify that whole workflow logs should be printed") + command.Flags().BoolVarP(&printer.follow, "follow", "f", false, "Specify if the logs should be streamed.") + command.Flags().StringVar(&since, "since", "", "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") + command.Flags().StringVar(&sinceTime, "since-time", "", "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.") + command.Flags().Int64Var(&tail, "tail", -1, "Lines of recent log file to display. Defaults to -1 with no selector, showing all log lines otherwise 10, if a selector is provided.") + command.Flags().BoolVar(&printer.timestamps, "timestamps", false, "Include timestamps on each line in the log output") + return command +} + +type logPrinter struct { + container string + follow bool + sinceSeconds *int64 + sinceTime *metav1.Time + tail *int64 + timestamps bool + kubeClient kubernetes.Interface +} + +// PrintWorkflowLogs prints logs for all workflow pods +func (p *logPrinter) PrintWorkflowLogs(workflow string) error { + wfClient := InitWorkflowClient() + wf, err := wfClient.Get(workflow, metav1.GetOptions{}) + if err != nil { + return err + } + timeByPod := p.printRecentWorkflowLogs(wf) + if p.follow { + p.printLiveWorkflowLogs(wf.Name, wfClient, timeByPod) + } + return nil +} + +// PrintPodLogs prints logs for a single pod +func (p *logPrinter) PrintPodLogs(podName string) error { + namespace, _, err := clientConfig.Namespace() + if err != nil { + return err + } + var logs []logEntry + err = p.getPodLogs(context.Background(), "", podName, namespace, p.follow, p.tail, p.sinceSeconds, p.sinceTime, func(entry logEntry) { + logs = append(logs, entry) + }) + if err != nil { + return err + } + for _, entry := range logs { + p.printLogEntry(entry) + } + return nil +} + +// Prints logs for workflow pod steps and return most recent log timestamp per pod name +func (p *logPrinter) printRecentWorkflowLogs(wf *v1alpha1.Workflow) map[string]*time.Time { + var podNodes []v1alpha1.NodeStatus + err := util.DecompressWorkflow(wf) + if err != nil { + log.Warn(err) + return nil + } + for _, node := range wf.Status.Nodes { + if node.Type == v1alpha1.NodeTypePod && node.Phase != v1alpha1.NodeError { + podNodes = append(podNodes, node) + } + } + var logs [][]logEntry + var wg sync.WaitGroup + wg.Add(len(podNodes)) + var mux sync.Mutex + + for i := range podNodes { + node := podNodes[i] + go func() { + defer wg.Done() + var podLogs []logEntry + err := p.getPodLogs(context.Background(), getDisplayName(node), node.ID, wf.Namespace, false, p.tail, p.sinceSeconds, p.sinceTime, func(entry logEntry) { + podLogs = append(podLogs, entry) + }) + + if err != nil { + log.Warn(err) + return + } + + mux.Lock() + logs = append(logs, podLogs) + mux.Unlock() + }() + + } + wg.Wait() + + flattenLogs := mergeSorted(logs) + + if p.tail != nil { + tail := *p.tail + if int64(len(flattenLogs)) < tail { + tail = int64(len(flattenLogs)) + } + flattenLogs = flattenLogs[0:tail] + } + timeByPod := make(map[string]*time.Time) + for _, entry := range flattenLogs { + p.printLogEntry(entry) + timeByPod[entry.pod] = &entry.time + } + return timeByPod +} + +// Prints live logs for workflow pods, starting from time specified in timeByPod name. +func (p *logPrinter) printLiveWorkflowLogs(workflowName string, wfClient workflowv1.WorkflowInterface, timeByPod map[string]*time.Time) { + logs := make(chan logEntry) + streamedPods := make(map[string]bool) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + processPods := func(wf *v1alpha1.Workflow) { + err := util.DecompressWorkflow(wf) + if err != nil { + log.Warn(err) + return + } + for id := range wf.Status.Nodes { + node := wf.Status.Nodes[id] + if node.Type == v1alpha1.NodeTypePod && node.Phase != v1alpha1.NodeError && !streamedPods[node.ID] { + streamedPods[node.ID] = true + go func() { + var sinceTimePtr *metav1.Time + podTime := timeByPod[node.ID] + if podTime != nil { + sinceTime := metav1.NewTime(podTime.Add(time.Second)) + sinceTimePtr = &sinceTime + } + err := p.getPodLogs(ctx, getDisplayName(node), node.ID, wf.Namespace, true, nil, nil, sinceTimePtr, func(entry logEntry) { + logs <- entry + }) + if err != nil { + log.Warn(err) + } + }() + } + } + } + + go func() { + defer close(logs) + fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", workflowName)) + listOpts := metav1.ListOptions{FieldSelector: fieldSelector.String()} + lw := &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + return wfClient.List(listOpts) + }, + WatchFunc: func(options metav1.ListOptions) (pkgwatch.Interface, error) { + return wfClient.Watch(listOpts) + }, + } + _, err := watch.UntilWithSync(ctx, lw, &v1alpha1.Workflow{}, nil, func(event pkgwatch.Event) (b bool, e error) { + if wf, ok := event.Object.(*v1alpha1.Workflow); ok { + if !wf.Status.Completed() { + processPods(wf) + } + return wf.Status.Completed(), nil + } + return true, nil + }) + if err != nil { + log.Fatal(err) + } + }() + + for entry := range logs { + p.printLogEntry(entry) + } +} + +func getDisplayName(node v1alpha1.NodeStatus) string { + res := node.DisplayName + if res == "" { + res = node.Name + } + return res +} + +func (p *logPrinter) printLogEntry(entry logEntry) { + line := entry.line + if p.timestamps { + line = entry.time.Format(time.RFC3339) + " " + line + } + if entry.displayName != "" { + colors := []int{FgRed, FgGreen, FgYellow, FgBlue, FgMagenta, FgCyan, FgWhite, FgDefault} + h := fnv.New32a() + _, err := h.Write([]byte(entry.displayName)) + errors.CheckError(err) + colorIndex := int(math.Mod(float64(h.Sum32()), float64(len(colors)))) + line = ansiFormat(entry.displayName, colors[colorIndex]) + ": " + line + } + fmt.Println(line) +} + +func (p *logPrinter) hasContainerStarted(podName string, podNamespace string, container string) (bool, error) { + pod, err := p.kubeClient.CoreV1().Pods(podNamespace).Get(podName, metav1.GetOptions{}) + if err != nil { + return false, err + } + var containerStatus *v1.ContainerStatus + for _, status := range pod.Status.ContainerStatuses { + if status.Name == container { + containerStatus = &status + break + } + } + if containerStatus == nil { + return false, nil + } + + if containerStatus.State.Waiting != nil { + return false, nil + } + return true, nil +} + +func (p *logPrinter) getPodLogs( + ctx context.Context, + displayName string, + podName string, + podNamespace string, + follow bool, + tail *int64, + sinceSeconds *int64, + sinceTime *metav1.Time, + callback func(entry logEntry)) error { + + for ctx.Err() == nil { + hasStarted, err := p.hasContainerStarted(podName, podNamespace, p.container) + + if err != nil { + return err + } + if !hasStarted { + if follow { + time.Sleep(1 * time.Second) + } else { + return nil + } + } else { + break + } + } + + stream, err := p.kubeClient.CoreV1().Pods(podNamespace).GetLogs(podName, &v1.PodLogOptions{ + Container: p.container, + Follow: follow, + Timestamps: true, + SinceSeconds: sinceSeconds, + SinceTime: sinceTime, + TailLines: tail, + }).Stream() + if err == nil { + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, " ") + logTime, err := time.Parse(time.RFC3339, parts[0]) + if err == nil { + lines := strings.Join(parts[1:], " ") + for _, line := range strings.Split(lines, "\r") { + if line != "" { + callback(logEntry{ + pod: podName, + displayName: displayName, + time: logTime, + line: line, + }) + } + } + } + } + } + return err +} + +func mergeSorted(logs [][]logEntry) []logEntry { + if len(logs) == 0 { + return make([]logEntry, 0) + } + for len(logs) > 1 { + left := logs[0] + right := logs[1] + size, i, j := len(left)+len(right), 0, 0 + merged := make([]logEntry, size) + + for k := 0; k < size; k++ { + if i > len(left)-1 && j <= len(right)-1 { + merged[k] = right[j] + j++ + } else if j > len(right)-1 && i <= len(left)-1 { + merged[k] = left[i] + i++ + } else if left[i].time.Before(right[j].time) { + merged[k] = left[i] + i++ + } else { + merged[k] = right[j] + j++ + } + } + logs = append(logs[2:], merged) + } + return logs[0] +} diff --git a/cmd/argo/resubmit.go b/cmd/argo/resubmit.go new file mode 100644 index 000000000..d2a589e21 --- /dev/null +++ b/cmd/argo/resubmit.go @@ -0,0 +1,46 @@ +package argo + +import ( + "os" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/errors" + + env "github.com/ian-howell/airshipctl/pkg/environment" +) + +func NewResubmitCommand() *cobra.Command { + var ( + memoized bool + cliSubmitOpts cliSubmitOpts + ) + var command = &cobra.Command{ + Use: "resubmit WORKFLOW", + Short: "resubmit a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + + wfClient := InitWorkflowClient() + wf, err := wfClient.Get(args[0], metav1.GetOptions{}) + errors.CheckError(err) + newWF, err := util.FormulateResubmitWorkflow(wf, memoized) + errors.CheckError(err) + created, err := util.SubmitWorkflow(wfClient, newWF, nil) + errors.CheckError(err) + printWorkflow(created, cliSubmitOpts.output, env.Default) + waitOrWatch([]string{created.Name}, cliSubmitOpts) + }, + } + + command.Flags().StringVarP(&cliSubmitOpts.output, "output", "o", "", "Output format. One of: name|json|yaml|wide") + command.Flags().BoolVarP(&cliSubmitOpts.wait, "wait", "w", false, "wait for the workflow to complete") + command.Flags().BoolVar(&cliSubmitOpts.watch, "watch", false, "watch the workflow until it completes") + command.Flags().BoolVar(&memoized, "memoized", false, "re-use successful steps & outputs from the previous run (experimental)") + return command +} diff --git a/cmd/argo/resume.go b/cmd/argo/resume.go new file mode 100644 index 000000000..5435a2eab --- /dev/null +++ b/cmd/argo/resume.go @@ -0,0 +1,33 @@ +package argo + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/cobra" + + "github.com/argoproj/argo/workflow/util" +) + +func NewResumeCommand() *cobra.Command { + var command = &cobra.Command{ + Use: "resume WORKFLOW1 WORKFLOW2...", + Short: "resume a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + InitWorkflowClient() + for _, wfName := range args { + err := util.ResumeWorkflow(wfClient, wfName) + if err != nil { + log.Fatalf("Failed to resume %s: %+v", wfName, err) + } + fmt.Printf("workflow %s resumed\n", wfName) + } + }, + } + return command +} diff --git a/cmd/argo/retry.go b/cmd/argo/retry.go new file mode 100644 index 000000000..cda02bd3f --- /dev/null +++ b/cmd/argo/retry.go @@ -0,0 +1,45 @@ +package argo + +import ( + "os" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/argoproj/argo/workflow/util" + + env "github.com/ian-howell/airshipctl/pkg/environment" +) + +func NewRetryCommand() *cobra.Command { + var ( + cliSubmitOpts cliSubmitOpts + ) + var command = &cobra.Command{ + Use: "retry WORKFLOW", + Short: "retry a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + kubeClient := initKubeClient() + wfClient := InitWorkflowClient() + wf, err := wfClient.Get(args[0], metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + wf, err = util.RetryWorkflow(kubeClient, wfClient, wf) + if err != nil { + log.Fatal(err) + } + printWorkflow(wf, cliSubmitOpts.output, env.Default) + waitOrWatch([]string{wf.Name}, cliSubmitOpts) + }, + } + command.Flags().StringVarP(&cliSubmitOpts.output, "output", "o", "", "Output format. One of: name|json|yaml|wide") + command.Flags().BoolVarP(&cliSubmitOpts.wait, "wait", "w", false, "wait for the workflow to complete") + command.Flags().BoolVar(&cliSubmitOpts.watch, "watch", false, "watch the workflow until it completes") + return command +} diff --git a/cmd/argo/root.go b/cmd/argo/root.go new file mode 100644 index 000000000..f9883275e --- /dev/null +++ b/cmd/argo/root.go @@ -0,0 +1,56 @@ +package argo + +import ( + "os" + + "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" + + "github.com/argoproj/argo/util/cmd" +) + +const ( + // CLIName is the name of the CLI + CLIName = "argo" +) + +// NewArgoCommand returns a new instance of an argo command +func NewArgoCommand() *cobra.Command { + var pluginRootCmd = &cobra.Command{ + Use: CLIName, + Short: "argo is the command line interface to Argo", + Run: func(cmd *cobra.Command, args []string) { + cmd.HelpFunc()(cmd, args) + }, + } + + pluginRootCmd.AddCommand(NewDeleteCommand()) + pluginRootCmd.AddCommand(NewGetCommand()) + pluginRootCmd.AddCommand(NewLintCommand()) + pluginRootCmd.AddCommand(NewListCommand()) + pluginRootCmd.AddCommand(NewLogsCommand()) + pluginRootCmd.AddCommand(NewResubmitCommand()) + pluginRootCmd.AddCommand(NewResumeCommand()) + pluginRootCmd.AddCommand(NewRetryCommand()) + pluginRootCmd.AddCommand(NewSubmitCommand()) + pluginRootCmd.AddCommand(NewSuspendCommand()) + pluginRootCmd.AddCommand(NewWaitCommand()) + pluginRootCmd.AddCommand(NewWatchCommand()) + pluginRootCmd.AddCommand(NewTerminateCommand()) + pluginRootCmd.AddCommand(cmd.NewVersionCmd(CLIName)) + + addKubectlFlagsToCmd(pluginRootCmd) + + return pluginRootCmd +} + +func addKubectlFlagsToCmd(cmd *cobra.Command) { + // The "usual" clientcmd/kubectl flags + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig + overrides := clientcmd.ConfigOverrides{} + kflags := clientcmd.RecommendedConfigOverrideFlags("") + // cmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster") + clientcmd.BindOverrideFlags(&overrides, cmd.PersistentFlags(), kflags) + clientConfig = clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin) +} diff --git a/cmd/argo/submit.go b/cmd/argo/submit.go new file mode 100644 index 000000000..f83e1dd88 --- /dev/null +++ b/cmd/argo/submit.go @@ -0,0 +1,158 @@ +package argo + +import ( + "bufio" + "io/ioutil" + "log" + "net/http" + "os" + + "github.com/spf13/cobra" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + cmdutil "github.com/argoproj/argo/util/cmd" + "github.com/argoproj/argo/workflow/common" + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/json" + + env "github.com/ian-howell/airshipctl/pkg/environment" +) + +// cliSubmitOpts holds submition options specific to CLI submission (e.g. controlling output) +type cliSubmitOpts struct { + output string // --output + wait bool // --wait + watch bool // --watch + strict bool // --strict + priority *int32 // --priority +} + +func NewSubmitCommand() *cobra.Command { + var ( + submitOpts util.SubmitOpts + cliSubmitOpts cliSubmitOpts + priority int32 + ) + var command = &cobra.Command{ + Use: "submit FILE1 FILE2...", + Short: "submit a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + if cmd.Flag("priority").Changed { + cliSubmitOpts.priority = &priority + } + + SubmitWorkflows(args, &submitOpts, &cliSubmitOpts) + }, + } + command.Flags().StringVar(&submitOpts.Name, "name", "", "override metadata.name") + command.Flags().StringVar(&submitOpts.GenerateName, "generate-name", "", "override metadata.generateName") + command.Flags().StringVar(&submitOpts.Entrypoint, "entrypoint", "", "override entrypoint") + command.Flags().StringArrayVarP(&submitOpts.Parameters, "parameter", "p", []string{}, "pass an input parameter") + command.Flags().StringVarP(&submitOpts.ParameterFile, "parameter-file", "f", "", "pass a file containing all input parameters") + command.Flags().StringVar(&submitOpts.ServiceAccount, "serviceaccount", "", "run all pods in the workflow using specified serviceaccount") + command.Flags().StringVar(&submitOpts.InstanceID, "instanceid", "", "submit with a specific controller's instance id label") + command.Flags().StringVarP(&cliSubmitOpts.output, "output", "o", "", "Output format. One of: name|json|yaml|wide") + command.Flags().BoolVarP(&cliSubmitOpts.wait, "wait", "w", false, "wait for the workflow to complete") + command.Flags().BoolVar(&cliSubmitOpts.watch, "watch", false, "watch the workflow until it completes") + command.Flags().BoolVar(&cliSubmitOpts.strict, "strict", true, "perform strict workflow validation") + command.Flags().Int32Var(&priority, "priority", 0, "workflow priority") + return command +} + +func SubmitWorkflows(filePaths []string, submitOpts *util.SubmitOpts, cliOpts *cliSubmitOpts) { + if submitOpts == nil { + submitOpts = &util.SubmitOpts{} + } + if cliOpts == nil { + cliOpts = &cliSubmitOpts{} + } + defaultWFClient := InitWorkflowClient() + var workflows []wfv1.Workflow + if len(filePaths) == 1 && filePaths[0] == "-" { + reader := bufio.NewReader(os.Stdin) + body, err := ioutil.ReadAll(reader) + if err != nil { + log.Fatal(err) + } + workflows = unmarshalWorkflows(body, cliOpts.strict) + } else { + for _, filePath := range filePaths { + var body []byte + var err error + if cmdutil.IsURL(filePath) { + response, err := http.Get(filePath) + if err != nil { + log.Fatal(err) + } + body, err = ioutil.ReadAll(response.Body) + _ = response.Body.Close() + if err != nil { + log.Fatal(err) + } + } else { + body, err = ioutil.ReadFile(filePath) + if err != nil { + log.Fatal(err) + } + } + wfs := unmarshalWorkflows(body, cliOpts.strict) + workflows = append(workflows, wfs...) + } + } + + if cliOpts.watch { + if len(workflows) > 1 { + log.Fatalf("Cannot watch more than one workflow") + } + if cliOpts.wait { + log.Fatalf("--wait cannot be combined with --watch") + } + } + + var workflowNames []string + for _, wf := range workflows { + wf.Spec.Priority = cliOpts.priority + wfClient := defaultWFClient + if wf.Namespace != "" { + wfClient = InitWorkflowClient(wf.Namespace) + } + created, err := util.SubmitWorkflow(wfClient, &wf, submitOpts) + if err != nil { + log.Fatalf("Failed to submit workflow: %v", err) + } + printWorkflow(created, cliOpts.output, env.Default) + workflowNames = append(workflowNames, created.Name) + } + waitOrWatch(workflowNames, *cliOpts) +} + +// unmarshalWorkflows unmarshals the input bytes as either json or yaml +func unmarshalWorkflows(wfBytes []byte, strict bool) []wfv1.Workflow { + var wf wfv1.Workflow + var jsonOpts []json.JSONOpt + if strict { + jsonOpts = append(jsonOpts, json.DisallowUnknownFields) + } + err := json.Unmarshal(wfBytes, &wf, jsonOpts...) + if err == nil { + return []wfv1.Workflow{wf} + } + yamlWfs, err := common.SplitYAMLFile(wfBytes, strict) + if err == nil { + return yamlWfs + } + log.Fatalf("Failed to parse workflow: %v", err) + return nil +} + +func waitOrWatch(workflowNames []string, cliSubmitOpts cliSubmitOpts) { + if cliSubmitOpts.wait { + WaitWorkflows(workflowNames, false, !(cliSubmitOpts.output == env.Default || cliSubmitOpts.output == env.Wide)) + } else if cliSubmitOpts.watch { + watchWorkflow(workflowNames[0]) + } +} diff --git a/cmd/argo/suspend.go b/cmd/argo/suspend.go new file mode 100644 index 000000000..73ede4876 --- /dev/null +++ b/cmd/argo/suspend.go @@ -0,0 +1,33 @@ +package argo + +import ( + "fmt" + "log" + "os" + + "github.com/spf13/cobra" + + "github.com/argoproj/argo/workflow/util" +) + +func NewSuspendCommand() *cobra.Command { + var command = &cobra.Command{ + Use: "suspend WORKFLOW1 WORKFLOW2...", + Short: "suspend a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + InitWorkflowClient() + for _, wfName := range args { + err := util.SuspendWorkflow(wfClient, wfName) + if err != nil { + log.Fatalf("Failed to suspend %s: %v", wfName, err) + } + fmt.Printf("workflow %s suspended\n", wfName) + } + }, + } + return command +} diff --git a/cmd/argo/terminate.go b/cmd/argo/terminate.go new file mode 100644 index 000000000..b58004645 --- /dev/null +++ b/cmd/argo/terminate.go @@ -0,0 +1,31 @@ +package argo + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/errors" +) + +func NewTerminateCommand() *cobra.Command { + var command = &cobra.Command{ + Use: "terminate WORKFLOW WORKFLOW2...", + Short: "terminate a workflow", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + InitWorkflowClient() + for _, name := range args { + err := util.TerminateWorkflow(wfClient, name) + errors.CheckError(err) + fmt.Printf("Workflow '%s' terminated\n", name) + } + }, + } + return command +} diff --git a/cmd/argo/wait.go b/cmd/argo/wait.go new file mode 100644 index 000000000..9ee5be8eb --- /dev/null +++ b/cmd/argo/wait.go @@ -0,0 +1,83 @@ +package argo + +import ( + "fmt" + "os" + "sync" + + "github.com/spf13/cobra" + apierr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/pkg/errors" +) + +func NewWaitCommand() *cobra.Command { + var ( + ignoreNotFound bool + ) + var command = &cobra.Command{ + Use: "wait WORKFLOW1 WORKFLOW2..,", + Short: "waits for a workflow to complete", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + InitWorkflowClient() + WaitWorkflows(args, ignoreNotFound, false) + }, + } + command.Flags().BoolVar(&ignoreNotFound, "ignore-not-found", false, "Ignore the wait if the workflow is not found") + return command +} + +// WaitWorkflows waits for the given workflowNames. +func WaitWorkflows(workflowNames []string, ignoreNotFound, quiet bool) { + var wg sync.WaitGroup + for _, workflowName := range workflowNames { + wg.Add(1) + go func(name string) { + waitOnOne(name, ignoreNotFound, quiet) + wg.Done() + }(workflowName) + } + wg.Wait() +} + +func waitOnOne(workflowName string, ignoreNotFound, quiet bool) { + fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", workflowName)) + opts := metav1.ListOptions{ + FieldSelector: fieldSelector.String(), + } + + _, err := wfClient.Get(workflowName, metav1.GetOptions{}) + if err != nil { + if apierr.IsNotFound(err) && ignoreNotFound { + return + } + errors.CheckError(err) + } + + watchIf, err := wfClient.Watch(opts) + errors.CheckError(err) + defer watchIf.Stop() + for { + next := <-watchIf.ResultChan() + wf, _ := next.Object.(*wfv1.Workflow) + if wf == nil { + watchIf.Stop() + watchIf, err = wfClient.Watch(opts) + errors.CheckError(err) + continue + } + if !wf.Status.FinishedAt.IsZero() { + if !quiet { + fmt.Printf("%s completed at %v\n", workflowName, wf.Status.FinishedAt) + } + return + } + } +} diff --git a/cmd/argo/watch.go b/cmd/argo/watch.go new file mode 100644 index 000000000..ebb69a977 --- /dev/null +++ b/cmd/argo/watch.go @@ -0,0 +1,67 @@ +package argo + +import ( + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + + wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1" + "github.com/argoproj/argo/workflow/util" + "github.com/argoproj/pkg/errors" +) + +func NewWatchCommand() *cobra.Command { + var command = &cobra.Command{ + Use: "watch WORKFLOW", + Short: "watch a workflow until it completes", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + cmd.HelpFunc()(cmd, args) + os.Exit(1) + } + InitWorkflowClient() + watchWorkflow(args[0]) + }, + } + return command +} + +func watchWorkflow(name string) { + fieldSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", name)) + opts := metav1.ListOptions{ + FieldSelector: fieldSelector.String(), + } + wf, err := wfClient.Get(name, metav1.GetOptions{}) + errors.CheckError(err) + + watchIf, err := wfClient.Watch(opts) + errors.CheckError(err) + ticker := time.NewTicker(time.Second) + + for { + select { + case next := <-watchIf.ResultChan(): + wf, _ = next.Object.(*wfv1.Workflow) + case <-ticker.C: + } + if wf == nil { + watchIf.Stop() + watchIf, err = wfClient.Watch(opts) + errors.CheckError(err) + continue + } + err := util.DecompressWorkflow(wf) + errors.CheckError(err) + print("\033[H\033[2J") + print("\033[0;0H") + printWorkflowHelper(wf, getFlags{}) + if !wf.Status.FinishedAt.IsZero() { + break + } + } + watchIf.Stop() +} diff --git a/cmd/root.go b/cmd/root.go index bc9c20768..68e59452e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/ian-howell/airshipctl/cmd/argo" "github.com/ian-howell/airshipctl/cmd/bootstrap" "github.com/ian-howell/airshipctl/pkg/environment" "github.com/ian-howell/airshipctl/pkg/log" @@ -41,5 +42,6 @@ func NewRootCmd(out io.Writer) (*cobra.Command, *environment.AirshipCTLSettings, // default commands to airshipctl func AddDefaultAirshipCTLCommands(cmd *cobra.Command, settings *environment.AirshipCTLSettings) *cobra.Command { cmd.AddCommand(bootstrap.NewBootstrapCommand(settings)) + cmd.AddCommand(argo.NewArgoCommand()) return cmd } diff --git a/go.mod b/go.mod index 717d33efa..c7410314d 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,16 @@ go 1.12 require ( github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect + github.com/argoproj/argo v2.3.0+incompatible + github.com/argoproj/pkg v0.0.0-20190409001913-7e3ef65c8d44 + github.com/colinmarc/hdfs v0.0.0-20180802165501-48eb8d6c34a9 + github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/emicklei/go-restful v2.9.6+incompatible // indirect github.com/fatih/color v1.7.0 // indirect + github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-critic/go-critic v0.3.4 // indirect + github.com/go-openapi/spec v0.19.0 // indirect github.com/golang/mock v1.3.1 // indirect github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect @@ -16,31 +24,45 @@ require ( github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect github.com/google/gofuzz v1.0.0 // indirect + github.com/googleapis/gnostic v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.0.0-20190329151158-56bca42c7635 // indirect + github.com/hashicorp/go-uuid v1.0.1 // indirect + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/imdario/mergo v0.3.7 // indirect + github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/magiconair/properties v1.8.1 // indirect github.com/mattn/go-colorable v0.1.2 // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect github.com/pelletier/go-toml v1.4.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/shurcooL/go v0.0.0-20190330031554-6713ea532688 // indirect - github.com/sirupsen/logrus v1.4.2 // indirect - github.com/spf13/afero v1.2.2 // indirect + github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.4.0 // indirect - github.com/stretchr/testify v1.3.0 // indirect + github.com/valyala/fasttemplate v1.0.1 // indirect golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 // indirect golang.org/x/net v0.0.0-20190607181551-461777fb6f67 // indirect golang.org/x/sys v0.0.0-20190610081024-1e42afee0f76 // indirect golang.org/x/text v0.3.2 // indirect golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/apimachinery v0.0.0-20190515023456-b74e4c97951f + gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect + gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect + gopkg.in/jcmturner/gokrb5.v5 v5.3.0 // indirect + gopkg.in/jcmturner/rpc.v0 v0.0.2 // indirect + k8s.io/api v0.0.0-20190516230258-a675ac48af67 + k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d + k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible k8s.io/code-generator v0.0.0-20190511023357-639c964206c2 + k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208 // indirect + k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a // indirect mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect + sigs.k8s.io/yaml v1.1.0 // indirect sourcegraph.com/sqs/pbtypes v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 516683dfa..841c12c34 100644 --- a/go.sum +++ b/go.sum @@ -2,19 +2,34 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA= github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 h1:pFks+oaqVWDFq0KsLQZFB2pQGB5Us0HfMSBENtieXOs= github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/argoproj/argo v2.3.0+incompatible h1:L1OYZ86Q7NK19ahdl/eJOq78Mlf52wUKGmp7VDNQVz8= +github.com/argoproj/argo v2.3.0+incompatible/go.mod h1:KJ0MB+tuhtAklR4jkPM10mIZXfRA0peTYJ1sLUnFLVU= +github.com/argoproj/pkg v0.0.0-20190409001913-7e3ef65c8d44 h1:fqYoz7qu4K8/mBdm/N1p7qKtdPhlwOSHlTQoAu4rATs= +github.com/argoproj/pkg v0.0.0-20190409001913-7e3ef65c8d44/go.mod h1:2EZ44RG/CcgtPTwrRR0apOc7oU6UIw8GjCUJWZ8X3bM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/colinmarc/hdfs v0.0.0-20180802165501-48eb8d6c34a9 h1:hJqIlFDcaaBLEBkCuqyEtzSsloo/h+lm08Qrq1OM/e8= +github.com/colinmarc/hdfs v0.0.0-20180802165501-48eb8d6c34a9/go.mod h1:0DumPviB681UcSuJErAbDIOx6SIaJWj463TymfZG02I= +github.com/colinmarc/hdfs v1.1.3 h1:662salalXLFmp+ctD+x0aG+xOg62lnVnOJHksXYpFBw= +github.com/colinmarc/hdfs v1.1.3/go.mod h1:0DumPviB681UcSuJErAbDIOx6SIaJWj463TymfZG02I= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -22,13 +37,22 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= +github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.6+incompatible h1:tfrHha8zJ01ywiOEC1miGY8st1/igzWB8OmvPgoYX7w= +github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550 h1:mV9jbLoSW/8m4VK16ZkHTozJa8sesK5u5kTMFysTYac= github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= @@ -37,7 +61,11 @@ github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= github.com/go-critic/go-critic v0.3.4 h1:FYaiaLjX0Nqei80KPhm4CyFQUBbmJwSrHxQ73taaGBc= github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc= @@ -48,6 +76,18 @@ github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTD github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4= +github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1 h1:h+1eMw+tZAlgTVclcVN0/rdPaBI/RUzG0peblT6df+Q= github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o= @@ -96,6 +136,7 @@ github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -151,11 +192,17 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= github.com/gostaticanalysis/analysisutil v0.0.0-20190329151158-56bca42c7635 h1:I/ckdXlVHde3unRCAcN/Tcpu7LFwgvyHqnFTeklC9oA= @@ -163,16 +210,26 @@ github.com/gostaticanalysis/analysisutil v0.0.0-20190329151158-56bca42c7635/go.m github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -201,6 +258,9 @@ github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= @@ -221,9 +281,11 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= @@ -253,6 +315,7 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -305,6 +368,7 @@ github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1: github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -315,6 +379,7 @@ github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= @@ -324,8 +389,11 @@ github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiff github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -337,6 +405,7 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk= @@ -347,10 +416,12 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -359,11 +430,13 @@ golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190607181551-461777fb6f67 h1:rJJxsykSlULwd2P2+pg/rtnwN2FrWp4IuCxOSyS0V00= golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -378,6 +451,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/p golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190610081024-1e42afee0f76 h1:QSmW7Q3mFdAGjtAd0byXmFJ55inUydyZ4WQmiuItAIQ= golang.org/x/sys v0.0.0-20190610081024-1e42afee0f76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= @@ -389,6 +463,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -427,6 +502,14 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/gokrb5.v5 v5.3.0 h1:RS1MYApX27Hx1Xw7NECs7XxGxxrm69/4OmaRuX9kwec= +gopkg.in/jcmturner/gokrb5.v5 v5.3.0/go.mod h1:oQz8Wc5GsctOTgCVyKad1Vw4TCWz5G6gfIQr88RPv4k= +gopkg.in/jcmturner/rpc.v0 v0.0.2 h1:wBTgrbL1qmLBUPsYVCqdJiI5aJgQhexmK+JkTHPUNJI= +gopkg.in/jcmturner/rpc.v0 v0.0.2/go.mod h1:NzMq6cRzR9lipgw7WxRBHNx5N8SifBuaCQsOT1kWY/E= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -436,16 +519,36 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190516230258-a675ac48af67 h1:BKg03K4me3EdM340RB08XB3MRlJ57KY+0f5PfI6wPZY= +k8s.io/api v0.0.0-20190516230258-a675ac48af67/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= +k8s.io/api v0.0.0-20190612125737-db0771252981 h1:DN1D/gMpl+h70Ek3Gb2ykCEI0QqIUtJ2e2z9PnAYz+Q= +k8s.io/api v0.0.0-20190612125737-db0771252981/go.mod h1:SR4nMi8IQTDnEi4768MsMCoZ9DyfRls7wy+TbRrFicA= +k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d h1:Jmdtdt1ZnoGfWWIIik61Z7nKYgO3J+swQJtPYsP9wHA= +k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= k8s.io/apimachinery v0.0.0-20190515023456-b74e4c97951f h1:cBrF1gFrJrvimOHZzyEHrvtlfqPV+KM7QZt3M0mepEg= k8s.io/apimachinery v0.0.0-20190515023456-b74e4c97951f/go.mod h1:Ew3b/24/JSgJdn4RsnrLskv3LvMZDlZ1Fl1xopsJftY= +k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad h1:x1lITOfDEbnzt8D1cZJsPbdnx/hnv28FxY2GKkxmxgU= +k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= +k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= +k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible h1:bK03DJulJi9j05gwnXUufcs2j7h4M85YFvJ0dIlQ9k4= +k8s.io/client-go v11.0.1-0.20190516230509-ae8359b20417+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= k8s.io/code-generator v0.0.0-20190511023357-639c964206c2 h1:wfF2JZb8Bl68FNMg/BAkIkkE29Z/bXWBYTtoQh/Cbo0= k8s.io/code-generator v0.0.0-20190511023357-639c964206c2/go.mod h1:YMQ7Lt97nW/I6nHACDccgS/sPAyrHQNans96RwPaSb8= k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af h1:SwjZbO0u5ZuaV6TRMWOGB40iaycX8sbdMQHtjNZ19dk= k8s.io/gengo v0.0.0-20190116091435-f8a0810f38af/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI= k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208 h1:5sW+fEHvlJI3Ngolx30CmubFulwH28DhKjGf70Xmtco= +k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= +k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw= +k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= @@ -459,6 +562,7 @@ mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 h1:B1LAOfRqg2QUyCdzfjf46quTS mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe h1:Ekmnp+NcP2joadI9pbK4Bva87QKZSeY7le//oiMrc9g= mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= diff --git a/pkg/environment/constants.go b/pkg/environment/constants.go new file mode 100644 index 000000000..85f0b048f --- /dev/null +++ b/pkg/environment/constants.go @@ -0,0 +1,13 @@ +package environment + +// OutputFormat denotes the form with which to display tabulated data +type OutputFormat string + +// These are valid values for OutputFormat +const ( + Default = "" + JSON = "json" + YAML = "yaml" + NameOnly = "name" + Wide = "wide" +)