Skip to content

Commit

Permalink
feat(cli): Add cost optimization nudges for Argo CLI (#5168)
Browse files Browse the repository at this point in the history
* feat(cli): Add cost optimization nudges for Argo CLI

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>

* feat(cli): Add --no-nudges option to disable nudges output for argo list

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>

* feat(cli): Add security nudges for get command

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>

* feat(cli): Discard returned values for consistency

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>

* feat(cli): Update docs

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>

* feat(cli): goimports

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>

* refactor(cli): Move security nudges printing code to the printer util pkg

Signed-off-by: Peixuan Ding <dingpeixuan911@gmail.com>
  • Loading branch information
dinever committed Feb 25, 2021
1 parent 4635d35 commit 4881111
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 2 deletions.
1 change: 1 addition & 0 deletions USERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Currently, the following organizations are **officially** using Argo Workflows:
1. [Equinor](https://www.equinor.com/)
1. [Fairwinds](https://fairwinds.com/)
1. [FOLIO](http://corp.folio-sec.com/)
1. [FreeWheel](https://freewheel.com/)
1. [Fynd Trak](https://trak.fynd.com/)
1. [Gardener](https://gardener.cloud/)
1. [GitHub](https://github.com/)
Expand Down
3 changes: 3 additions & 0 deletions cmd/argo/commands/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ func printWorkflowHelper(wf *wfv1.Workflow, getArgs getFlags) string {
_ = w.Flush()
out += writerBuffer.String()
}
writerBuffer := new(bytes.Buffer)
printer.PrintSecurityNudges(*wf, writerBuffer)
out += writerBuffer.String()
return out
}

Expand Down
29 changes: 29 additions & 0 deletions cmd/argo/commands/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1"
Expand Down Expand Up @@ -366,5 +367,33 @@ status:
├─ sleep(9:nine) sleep many-items-z26lj-2619926859 19s
├─ sleep(10:ten) sleep many-items-z26lj-1052882686 23s
├─ sleep(11:eleven) sleep many-items-z26lj-3011405271 22s`)
assert.Contains(t, output, "This workflow does not have security context set. "+
"You can run your workflow pods more securely by setting it.\n"+
"Learn more at https://argoproj.github.io/argo-workflows/workflow-pod-security-context/\n")
})
}

func Test_printWorkflowHelperNudges(t *testing.T) {
securedWf := wfv1.Workflow{
ObjectMeta: metav1.ObjectMeta{},
Spec: wfv1.WorkflowSpec{
SecurityContext: &corev1.PodSecurityContext{},
},
}

insecureWf := securedWf
insecureWf.Spec.SecurityContext = nil

securityNudges := "This workflow does not have security context set. " +
"You can run your workflow pods more securely by setting it.\n" +
"Learn more at https://argoproj.github.io/argo-workflows/workflow-pod-security-context/\n"

t.Run("SecuredWorkflow", func(t *testing.T) {
output := printWorkflowHelper(&securedWf, getFlags{})
assert.NotContains(t, output, securityNudges)
})
t.Run("InsecureWorkflow", func(t *testing.T) {
output := printWorkflowHelper(&insecureWf, getFlags{})
assert.Contains(t, output, securityNudges)
})
}
48 changes: 46 additions & 2 deletions util/printer/workflow-printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func PrintWorkflows(workflows wfv1.Workflows, out io.Writer, opts PrintOpts) err
switch opts.Output {
case "", "wide":
printTable(workflows, out, opts)
printCostOptimizationNudges(workflows, out)
case "name":
for _, wf := range workflows {
_, _ = fmt.Fprintln(out, wf.ObjectMeta.Name)
Expand Down Expand Up @@ -76,7 +77,7 @@ func printTable(wfList []wfv1.Workflow, out io.Writer, opts PrintOpts) {
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d", wf.ObjectMeta.Name, WorkflowStatus(&wf), ageStr, durationStr, priority)
if opts.Output == "wide" {
pending, running, completed := countPendingRunningCompleted(&wf)
pending, running, completed := countPendingRunningCompletedNodes(&wf)
_, _ = fmt.Fprintf(w, "\t%d/%d/%d", pending, running, completed)
_, _ = fmt.Fprintf(w, "\t%s", parameterString(wf.Spec.Arguments.Parameters))
}
Expand All @@ -85,7 +86,50 @@ func printTable(wfList []wfv1.Workflow, out io.Writer, opts PrintOpts) {
_ = w.Flush()
}

func countPendingRunningCompleted(wf *wfv1.Workflow) (int, int, int) {
// printCostOptimizationNudges prints cost optimization nudges for workflows
func printCostOptimizationNudges(wfList []wfv1.Workflow, out io.Writer) {
completed, incomplete := countCompletedWorkflows(wfList)
if completed > 100 || incomplete > 100 {
_, _ = fmt.Fprint(out, "\nYou have at least ")
if incomplete > 100 {
_, _ = fmt.Fprintf(out, "%d incomplete ", incomplete)
}
if incomplete > 100 && completed > 100 {
_, _ = fmt.Fprint(out, "and ")
}
if completed > 100 {
_, _ = fmt.Fprintf(out, "%d completed ", completed)
}
_, _ = fmt.Fprintln(out, "workflows. Reducing the total number of workflows will reduce your costs.")
_, _ = fmt.Fprintln(out, "Learn more at https://argoproj.github.io/argo-workflows/cost-optimisation/")
}
}

// PrintSecurityNudges prints security nudges for single workflow
func PrintSecurityNudges(wf wfv1.Workflow, out io.Writer) {
if wf.Spec.SecurityContext == nil {
_, _ = fmt.Fprintln(out, "\nThis workflow does not have security context set. "+
"You can run your workflow pods more securely by setting it.")
_, _ = fmt.Fprintln(out, "Learn more at https://argoproj.github.io/argo-workflows/workflow-pod-security-context/")
}
}

// countCompletedWorkflows returns the number of completed and incomplete workflows
func countCompletedWorkflows(wfList []wfv1.Workflow) (int, int) {
completed := 0
incomplete := 0
for _, wf := range wfList {
if wf.Status.Phase.Completed() {
completed++
} else {
incomplete++
}
}
return completed, incomplete
}

// countPendingRunningCompletedNodes returns the number of pending, running and completed workflow nodes
func countPendingRunningCompletedNodes(wf *wfv1.Workflow) (int, int, int) {
pending := 0
running := 0
completed := 0
Expand Down
69 changes: 69 additions & 0 deletions util/printer/workflow-printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func TestPrintWorkflows(t *testing.T) {
Templates: []wfv1.Template{
{Name: "t0", Container: &corev1.Container{}},
},
SecurityContext: &corev1.PodSecurityContext{},
},
Status: wfv1.WorkflowStatus{
Phase: wfv1.WorkflowRunning,
Expand Down Expand Up @@ -94,3 +95,71 @@ my-wf Running 0s 3s 2 1/2/3 my-param=my-value
assert.NotEmpty(t, b.String())
})
}

func TestPrintWorkflowCostOptimizationNudges(t *testing.T) {
completedWorkflows := wfv1.Workflows{}
for i := 0; i < 101; i++ {
completedWorkflows = append(completedWorkflows,
wfv1.Workflow{
Status: wfv1.WorkflowStatus{
Phase: wfv1.WorkflowSucceeded,
},
})
}
incompleteWorkflows := wfv1.Workflows{}
for i := 0; i < 101; i++ {
incompleteWorkflows = append(incompleteWorkflows,
wfv1.Workflow{
Status: wfv1.WorkflowStatus{
Phase: wfv1.WorkflowRunning,
},
})
}
completedAndIncompleteWorkflows := append(completedWorkflows, incompleteWorkflows...)

t.Run("CostOptimizationOnCompletedWorkflows", func(t *testing.T) {
var b bytes.Buffer
assert.NoError(t, PrintWorkflows(completedWorkflows, &b, PrintOpts{}))
assert.Contains(t, b.String(), "\nYou have at least 101 completed workflows. "+
"Reducing the total number of workflows will reduce your costs."+
"\nLearn more at https://argoproj.github.io/argo-workflows/cost-optimisation/\n")
})
t.Run("CostOptimizationOnIncompleteWorkflows", func(t *testing.T) {
var b bytes.Buffer
assert.NoError(t, PrintWorkflows(incompleteWorkflows, &b, PrintOpts{}))
assert.Contains(t, b.String(), "\nYou have at least 101 incomplete workflows. "+
"Reducing the total number of workflows will reduce your costs."+
"\nLearn more at https://argoproj.github.io/argo-workflows/cost-optimisation/\n")
})
t.Run("CostOptimizationOnCompletedAndIncompleteWorkflows", func(t *testing.T) {
var b bytes.Buffer
assert.NoError(t, PrintWorkflows(completedAndIncompleteWorkflows, &b, PrintOpts{}))
assert.Contains(t, b.String(), "\nYou have at least 101 incomplete and 101 completed workflows. "+
"Reducing the total number of workflows will reduce your costs."+
"\nLearn more at https://argoproj.github.io/argo-workflows/cost-optimisation/\n")
})
}

func TestPrintWorkflowSecurityNudges(t *testing.T) {
secureWorkflow := wfv1.Workflow{
Spec: wfv1.WorkflowSpec{
SecurityContext: &corev1.PodSecurityContext{},
},
}
insecureWorkflow := wfv1.Workflow{
Spec: wfv1.WorkflowSpec{},
}

t.Run("SecurityNudgesForSingleInsecureWorkflow", func(t *testing.T) {
var b bytes.Buffer
PrintSecurityNudges(insecureWorkflow, &b)
assert.Contains(t, b.String(), "\nThis workflow does not have security context set. "+
"You can run your workflow pods more securely by setting it.\n"+
"Learn more at https://argoproj.github.io/argo-workflows/workflow-pod-security-context/\n")
})
t.Run("NoSecurityNudgesForSecureWorkflow", func(t *testing.T) {
var b bytes.Buffer
PrintSecurityNudges(secureWorkflow, &b)
assert.NotContains(t, b.String(), "security context")
})
}

0 comments on commit 4881111

Please sign in to comment.