From a8e649eb671a119e6697248895fe35e566fbaa3d Mon Sep 17 00:00:00 2001 From: Ian Howell Date: Fri, 31 May 2019 15:57:07 -0500 Subject: [PATCH] Move workflow listing to its own package * adds the AGE, DURATION, and PRIORITY columns * adds the --all-namespaces flag --- .../workflow-list-empty.golden | 2 +- .../workflow-list-nonempty.golden | 4 +- cmd/workflow/workflow_list.go | 65 ++++++++++++++++--- cmd/workflow/workflow_list_test.go | 11 ++++ go.mod | 2 + go.sum | 4 ++ pkg/util/util.go | 27 ++++++++ pkg/workflow/environment/settings.go | 4 ++ pkg/workflow/list.go | 51 +++++++++++++++ pkg/workflow/util/util.go | 23 +++++++ test/utilities.go | 3 + 11 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 pkg/workflow/list.go create mode 100644 pkg/workflow/util/util.go diff --git a/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-empty.golden b/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-empty.golden index 76f5db6be..9cb7eedaf 100644 --- a/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-empty.golden +++ b/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-empty.golden @@ -1 +1 @@ -NAME PHASE +NAME STATUS AGE DURATION PRIORITY diff --git a/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-nonempty.golden b/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-nonempty.golden index 59e3f4cfc..8a0dae61c 100644 --- a/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-nonempty.golden +++ b/cmd/workflow/testdata/TestWorkflowListGoldenOutput/workflow-list-nonempty.golden @@ -1,2 +1,2 @@ -NAME PHASE -fake-wf completed +NAME STATUS AGE DURATION PRIORITY +fake-wf completed 5m 3m 0 diff --git a/cmd/workflow/workflow_list.go b/cmd/workflow/workflow_list.go index 8f7b263f8..eaa60cddc 100644 --- a/cmd/workflow/workflow_list.go +++ b/cmd/workflow/workflow_list.go @@ -2,13 +2,17 @@ package workflow import ( "fmt" + "io" + "github.com/argoproj/pkg/humanize" "github.com/spf13/cobra" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/ian-howell/airshipctl/pkg/apis/workflow/v1alpha1" "github.com/ian-howell/airshipctl/pkg/environment" "github.com/ian-howell/airshipctl/pkg/util" + "github.com/ian-howell/airshipctl/pkg/workflow" wfenv "github.com/ian-howell/airshipctl/pkg/workflow/environment" + wfutil "github.com/ian-howell/airshipctl/pkg/workflow/util" ) // NewWorkflowListCommand is a command for listing argo workflows @@ -24,19 +28,60 @@ func NewWorkflowListCommand(rootSettings *environment.AirshipCTLSettings) *cobra fmt.Fprintf(out, "settings for %s were not registered\n", PluginSettingsID) return } - clientSet := wfSettings.ArgoClient.ArgoprojV1alpha1() - wflist, err := clientSet.Workflows(wfSettings.Namespace).List(v1.ListOptions{}) + wflist, err := workflow.ListWorkflows(wfSettings) if err != nil { - panic(err.Error()) - } - w := util.NewTabWriter(out) - defer w.Flush() - fmt.Fprintf(w, "%s\t%s\n", "NAME", "PHASE") - for _, wf := range wflist.Items { - fmt.Fprintf(w, "%s\t%s\n", wf.Name, wf.Status.Phase) + fmt.Fprintf(out, "Could not list workflows: %s\n", err.Error()) + return } + printTable(out, wflist, wfSettings) }, } return workflowListCmd } + +// printTable pretty prints the list of workflows to out +func printTable(out io.Writer, wfList []v1alpha1.Workflow, wfSettings *wfenv.Settings) { + w := util.NewTabWriter(out) + defer w.Flush() + if wfSettings.AllNamespaces { + fmt.Fprint(w, "NAMESPACE\t") + } + fmt.Fprint(w, "NAME\tSTATUS\tAGE\tDURATION\tPRIORITY") + fmt.Fprint(w, "\n") + for _, wf := range wfList { + ageStr := humanize.RelativeDurationShort(wf.ObjectMeta.CreationTimestamp.Time, util.Now()) + durationStr := humanize.RelativeDurationShort(wf.Status.StartedAt.Time, wf.Status.FinishedAt.Time) + if wfSettings.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\n", wf.ObjectMeta.Name, workflowStatus(&wf), ageStr, durationStr, priority) + } +} + +// workflowStatus returns a human readable inferred workflow status based on workflow phase and conditions +func workflowStatus(wf *v1alpha1.Workflow) v1alpha1.NodePhase { + switch wf.Status.Phase { + case v1alpha1.NodeRunning: + if wfutil.IsWorkflowSuspended(wf) { + return "Running (Suspended)" + } + return wf.Status.Phase + case v1alpha1.NodeFailed: + if wfutil.IsWorkflowTerminated(wf) { + return "Failed (Terminated)" + } + return wf.Status.Phase + case "", v1alpha1.NodePending: + if !wf.ObjectMeta.CreationTimestamp.IsZero() { + return v1alpha1.NodePending + } + return "Unknown" + default: + return wf.Status.Phase + } +} diff --git a/cmd/workflow/workflow_list_test.go b/cmd/workflow/workflow_list_test.go index 3eadfd946..d391ce8eb 100644 --- a/cmd/workflow/workflow_list_test.go +++ b/cmd/workflow/workflow_list_test.go @@ -2,6 +2,7 @@ package workflow_test import ( "testing" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -10,6 +11,7 @@ import ( "github.com/ian-howell/airshipctl/cmd/workflow" "github.com/ian-howell/airshipctl/pkg/apis/workflow/v1alpha1" argofake "github.com/ian-howell/airshipctl/pkg/client/clientset/versioned/fake" + "github.com/ian-howell/airshipctl/pkg/util" wfenv "github.com/ian-howell/airshipctl/pkg/workflow/environment" "github.com/ian-howell/airshipctl/test" ) @@ -41,9 +43,18 @@ func TestWorkflowList(t *testing.T) { &v1alpha1.Workflow{ ObjectMeta: metav1.ObjectMeta{ Name: "fake-wf", + CreationTimestamp: metav1.Time{ + Time: util.Clock().Add(5 * time.Minute), + }, }, Status: v1alpha1.WorkflowStatus{ Phase: "completed", + StartedAt: metav1.Time{ + Time: util.Clock().Add(5 * time.Minute), + }, + FinishedAt: metav1.Time{ + Time: util.Clock().Add(8 * time.Minute), + }, }, }, }, diff --git a/go.mod b/go.mod index b47ccb03c..c4961fe78 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/ian-howell/airshipctl go 1.12 require ( + github.com/argoproj/pkg v0.0.0-20190409001913-7e3ef65c8d44 + github.com/dustin/go-humanize v1.0.0 // indirect github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead // indirect github.com/go-toolsmith/pkgload v0.0.0-20181120203407-5122569a890b // indirect github.com/gogo/protobuf v1.2.1 // indirect diff --git a/go.sum b/go.sum index 18843fbf2..52683e619 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Quasilyte/go-consistent v0.0.0-20181230194409-8f8379e70f99/go.mod h1:ds1OLa3HF2x4OGKCx0pNTVL1s9Ii/2mT0Bg/8PtW6AM= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +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/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= @@ -29,6 +31,8 @@ github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11 github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/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/evanphx/json-patch v0.0.0-20190203023257-5858425f7550 h1:mV9jbLoSW/8m4VK16ZkHTozJa8sesK5u5kTMFysTYac= diff --git a/pkg/util/util.go b/pkg/util/util.go index e6d795e97..6704edea3 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -2,6 +2,7 @@ package util import ( "os" + "time" ) // IsReadable returns nil if the file at path is readable. An error is returned otherwise. @@ -12,3 +13,29 @@ func IsReadable(path string) error { } return f.Close() } + +// Clock is mostly useful for unit testing, allowing for mocking out of time +var clock *time.Time + +// Now returns time.Now() if the Clock is nil. If the Clock is not nil, it is +// returned instead. This is useful for testing +func Now() time.Time { + if clock == nil { + return time.Now() + } + return *clock +} + +// InitClock creates a clock for unit testing +func InitClock() { + mockClock := time.Date(2000, time.January, 0, 0, 0, 0, 0, time.UTC) + clock = &mockClock +} + +// Clock gives access to the mocked clock +func Clock() time.Time { + if clock == nil { + InitClock() + } + return *clock +} diff --git a/pkg/workflow/environment/settings.go b/pkg/workflow/environment/settings.go index 386a9df1b..93f69beeb 100644 --- a/pkg/workflow/environment/settings.go +++ b/pkg/workflow/environment/settings.go @@ -14,6 +14,9 @@ type Settings struct { // Namespace is the kubernetes namespace to be used during the context of this action Namespace string + // AllNamespaces denotes whether or not to use all namespaces. It will override the Namespace string + AllNamespaces bool + // KubeConfigFilePath is the path to the kubernetes configuration file. // This flag is only needed when airshipctl is being used // out-of-cluster @@ -37,6 +40,7 @@ func (s *Settings) InitFlags(cmd *cobra.Command) { flags := cmd.PersistentFlags() flags.StringVar(&s.KubeConfigFilePath, "kubeconfig", "", "path to kubeconfig") flags.StringVar(&s.Namespace, "namespace", "default", "kubernetes namespace to use for the context of this command") + flags.BoolVar(&s.AllNamespaces, "all-namespaces", false, "use all kubernetes namespaces for the context of this command") } // Init assigns default values diff --git a/pkg/workflow/list.go b/pkg/workflow/list.go new file mode 100644 index 000000000..38af8d1fb --- /dev/null +++ b/pkg/workflow/list.go @@ -0,0 +1,51 @@ +package workflow + +import ( + "sort" + + apiv1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ian-howell/airshipctl/pkg/apis/workflow/v1alpha1" + v1alpha1client "github.com/ian-howell/airshipctl/pkg/client/clientset/versioned/typed/workflow/v1alpha1" + "github.com/ian-howell/airshipctl/pkg/workflow/environment" +) + +// ListWorkflows returns a list of Workflows +func ListWorkflows(settings *environment.Settings) ([]v1alpha1.Workflow, error) { + var clientSet v1alpha1client.WorkflowInterface + if settings.AllNamespaces { + clientSet = settings.ArgoClient.ArgoprojV1alpha1().Workflows(apiv1.NamespaceAll) + } else { + clientSet = settings.ArgoClient.ArgoprojV1alpha1().Workflows(settings.Namespace) + } + wflist, err := clientSet.List(v1.ListOptions{}) + if err != nil { + return []v1alpha1.Workflow{}, err + } + workflows := wflist.Items + sort.Sort(ByFinishedAt(workflows)) + return workflows, nil +} + +// ByFinishedAt is a sort interface which sorts running jobs earlier before considering FinishedAt +type ByFinishedAt []v1alpha1.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) +} diff --git a/pkg/workflow/util/util.go b/pkg/workflow/util/util.go new file mode 100644 index 000000000..f76191318 --- /dev/null +++ b/pkg/workflow/util/util.go @@ -0,0 +1,23 @@ +package util + +import ( + "github.com/ian-howell/airshipctl/pkg/apis/workflow/v1alpha1" +) + +// IsWorkflowSuspended returns whether or not a workflow is considered suspended +func IsWorkflowSuspended(wf *v1alpha1.Workflow) bool { + if wf.Spec.Suspend != nil && *wf.Spec.Suspend { + return true + } + for _, node := range wf.Status.Nodes { + if node.Type == v1alpha1.NodeTypeSuspend && node.Phase == v1alpha1.NodeRunning { + return true + } + } + return false +} + +// IsWorkflowTerminated returns whether or not a workflow is considered terminated +func IsWorkflowTerminated(wf *v1alpha1.Workflow) bool { + return wf.Spec.ActiveDeadlineSeconds != nil && *wf.Spec.ActiveDeadlineSeconds == 0 +} diff --git a/test/utilities.go b/test/utilities.go index 7cb978f9b..e2254cb0d 100644 --- a/test/utilities.go +++ b/test/utilities.go @@ -11,6 +11,8 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" + + "github.com/ian-howell/airshipctl/pkg/util" ) // UpdateGolden writes out the golden files with the latest values, rather than failing the test. @@ -33,6 +35,7 @@ type CmdTest struct { // output from its golden file, or generates golden files if the -update flag // is passed func RunTest(t *testing.T, test *CmdTest, cmd *cobra.Command) { + util.InitClock() actual := &bytes.Buffer{} cmd.SetOutput(actual) args := strings.Fields(test.CmdLine)