Move workflow listing to its own package

* adds the AGE, DURATION, and PRIORITY columns
* adds the --all-namespaces flag
This commit is contained in:
Ian Howell 2019-05-31 15:57:07 -05:00
parent 3b4a9fb82c
commit a8e649eb67
11 changed files with 183 additions and 13 deletions

View File

@ -1 +1 @@
NAME PHASE
NAME STATUS AGE DURATION PRIORITY

View File

@ -1,2 +1,2 @@
NAME PHASE
fake-wf completed
NAME STATUS AGE DURATION PRIORITY
fake-wf completed 5m 3m 0

View File

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

View File

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

2
go.mod
View File

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

4
go.sum
View File

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

View File

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

View File

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

51
pkg/workflow/list.go Normal file
View File

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

23
pkg/workflow/util/util.go Normal file
View File

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

View File

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