Add argo as a subcommand

This is a drop-in of the argo cmd package, with some slight tweaks to
adhere to airshipctl's models and linter. It still needs to be combed
over and modified to meet airshipctl's needs, as well as requiring unit
tests
This commit is contained in:
Ian Howell 2019-06-13 08:58:19 -05:00
parent 3c02f845d0
commit 5999d17323
19 changed files with 2066 additions and 4 deletions

117
cmd/argo/common.go Normal file
View File

@ -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)
}

84
cmd/argo/delete.go Normal file
View File

@ -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)
}
}

488
cmd/argo/get.go Normal file
View File

@ -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, ",")
}

59
cmd/argo/lint.go Normal file
View File

@ -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
}

229
cmd/argo/list.go Normal file
View File

@ -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
}
}

392
cmd/argo/logs.go Normal file
View File

@ -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]
}

46
cmd/argo/resubmit.go Normal file
View File

@ -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
}

33
cmd/argo/resume.go Normal file
View File

@ -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
}

45
cmd/argo/retry.go Normal file
View File

@ -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
}

56
cmd/argo/root.go Normal file
View File

@ -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)
}

158
cmd/argo/submit.go Normal file
View File

@ -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])
}
}

33
cmd/argo/suspend.go Normal file
View File

@ -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
}

31
cmd/argo/terminate.go Normal file
View File

@ -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
}

83
cmd/argo/wait.go Normal file
View File

@ -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
}
}
}

67
cmd/argo/watch.go Normal file
View File

@ -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()
}

View File

@ -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
}

30
go.mod
View File

@ -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
)

104
go.sum
View File

@ -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=

View File

@ -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"
)