Move workflow listing to its own package
* adds the AGE, DURATION, and PRIORITY columns * adds the --all-namespaces flag
This commit is contained in:
parent
3b4a9fb82c
commit
a8e649eb67
@ -1 +1 @@
|
||||
NAME PHASE
|
||||
NAME STATUS AGE DURATION PRIORITY
|
||||
|
@ -1,2 +1,2 @@
|
||||
NAME PHASE
|
||||
fake-wf completed
|
||||
NAME STATUS AGE DURATION PRIORITY
|
||||
fake-wf completed 5m 3m 0
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
51
pkg/workflow/list.go
Normal 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
23
pkg/workflow/util/util.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user