diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go index 678b3981..d413b0a0 100644 --- a/pkg/actions/actions.go +++ b/pkg/actions/actions.go @@ -48,6 +48,20 @@ func list(gr schema.GroupVersionResource, dynamic dynamic.Interface, discovery d return allRes, nil } +func Get(gr schema.GroupVersionResource, c *cli.Clients, opts *cli.Options) (*v1alpha1.ApprovalTask, error) { + gvr, err := GetGroupVersionResource(gr, c.ApprovalTask.Discovery()) + if err != nil { + return nil, err + } + + at, err := get(gvr, c, opts) + if err != nil { + return &v1alpha1.ApprovalTask{}, err + } + + return at, nil +} + func get(gvr *schema.GroupVersionResource, c *cli.Clients, opts *cli.Options) (*v1alpha1.ApprovalTask, error) { result, err := c.Dynamic.Resource(*gvr).Namespace(opts.Namespace).Get(context.Background(), opts.Name, metav1.GetOptions{}) if err != nil { diff --git a/pkg/cli/cmd/describe/describe.go b/pkg/cli/cmd/describe/describe.go new file mode 100644 index 00000000..0860561c --- /dev/null +++ b/pkg/cli/cmd/describe/describe.go @@ -0,0 +1,145 @@ +package describe + +import ( + "fmt" + "log" + "text/tabwriter" + "text/template" + + "github.com/openshift-pipelines/manual-approval-gate/pkg/actions" + "github.com/openshift-pipelines/manual-approval-gate/pkg/apis/approvaltask/v1alpha1" + "github.com/openshift-pipelines/manual-approval-gate/pkg/cli" + "github.com/openshift-pipelines/manual-approval-gate/pkg/cli/flags" + "github.com/openshift-pipelines/manual-approval-gate/pkg/cli/formatter" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var taskTemplate = `📦 Name: {{ .ApprovalTask.Name }} +🗂 Namespace: {{ .ApprovalTask.Namespace }} +{{- $pipelineRunRef := pipelineRunRef .ApprovalTask }} +{{- if ne $pipelineRunRef "" }} +🏷️ PipelineRunRef: {{ $pipelineRunRef }} +{{- end }} + +👥 Approvers +{{- range .ApprovalTask.Spec.Approvers }} + * {{ .Name }} +{{- end }} + + +{{- if gt (len .ApprovalTask.Status.ApproversResponse) 0 }} + +👨‍💻 ApproverResponse + +Name ApproverResponse Message +{{- range .ApprovalTask.Status.ApproversResponse }} +{{ .Name }} {{response .Response }} {{message .Message }} +{{- end }} +{{- end }} + +🌡️ Status + +NumberOfApprovalsRequired PendingApprovals STATUS +{{.ApprovalTask.Spec.NumberOfApprovalsRequired}} {{pendingApprovals .ApprovalTask}} {{state .ApprovalTask}} +` + +var ( + taskGroupResource = schema.GroupVersionResource{Group: "openshift-pipelines.org", Resource: "approvaltasks"} +) + +func pendingApprovals(at *v1alpha1.ApprovalTask) int { + return at.Spec.NumberOfApprovalsRequired - len(at.Status.ApproversResponse) +} + +func pipelineRunRef(at *v1alpha1.ApprovalTask) string { + var pipelineRunReference string + for k, v := range at.Labels { + if k == "tekton.dev/pipelineRun" { + pipelineRunReference = v + } + } + + return pipelineRunReference +} + +func message(msg string) string { + if msg == "" { + return "---" + } + return msg +} + +func response(response string) string { + if response == "approved" { + return "✅" + } + return "❌" +} + +func Command(p cli.Params) *cobra.Command { + opts := &cli.Options{} + + funcMap := template.FuncMap{ + "pipelineRunRef": pipelineRunRef, + "pendingApprovals": pendingApprovals, + "message": message, + "response": response, + "state": formatter.State, + } + + c := &cobra.Command{ + Use: "describe", + Short: "Describe approval task", + Long: `This command describe the approval task.`, + Annotations: map[string]string{ + "commandType": "main", + }, + Args: cobra.ExactArgs(1), + PersistentPreRunE: flags.PersistentPreRunE(p), + RunE: func(cmd *cobra.Command, args []string) error { + cs, err := p.Clients() + if err != nil { + return err + } + + ns := p.Namespace() + if opts.AllNamespaces { + ns = "" + } + + opts = &cli.Options{ + Namespace: ns, + Name: args[0], + } + + at, err := actions.Get(taskGroupResource, cs, opts) + if err != nil { + return fmt.Errorf("failed to Get ApprovalTasks %s from %s namespace", args[0], ns) + } + + var data = struct { + ApprovalTask *v1alpha1.ApprovalTask + }{ + ApprovalTask: at, + } + + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 5, 3, ' ', tabwriter.TabIndent) + t := template.Must(template.New("Describe ApprovalTask").Funcs(funcMap).Parse(taskTemplate)) + + if err != nil { + return err + } + + if err := t.Execute(w, data); err != nil { + log.Fatal(err) + return err + } + + return w.Flush() + }, + } + flags.AddOptions(c) + + return c +} diff --git a/pkg/cli/cmd/describe/describe_test.go b/pkg/cli/cmd/describe/describe_test.go new file mode 100644 index 00000000..a5bfa500 --- /dev/null +++ b/pkg/cli/cmd/describe/describe_test.go @@ -0,0 +1,108 @@ +package describe + +import ( + "fmt" + "strings" + "testing" + + "github.com/openshift-pipelines/manual-approval-gate/pkg/apis/approvaltask/v1alpha1" + "github.com/openshift-pipelines/manual-approval-gate/pkg/test" + cb "github.com/openshift-pipelines/manual-approval-gate/pkg/test/builder" + testDynamic "github.com/openshift-pipelines/manual-approval-gate/pkg/test/dynamic" + "github.com/spf13/cobra" + "gotest.tools/v3/golden" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +func TestDescribeApprovalTask(t *testing.T) { + approvaltasks := []*v1alpha1.ApprovalTask{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "at-1", + Namespace: "foo", + }, + Spec: v1alpha1.ApprovalTaskSpec{ + Approvers: []v1alpha1.ApproverDetails{ + { + Name: "tekton", + Input: "reject", + }, + { + Name: "cli", + Input: "pending", + }, + }, + NumberOfApprovalsRequired: 2, + }, + Status: v1alpha1.ApprovalTaskStatus{ + Approvers: []string{ + "tekton", + "cli", + }, + ApproversResponse: []v1alpha1.ApproverState{ + { + Name: "tekton", + Response: "rejected", + }, + }, + State: "rejected", + }, + }, + } + + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "namespace", + }, + }, + } + + dc, err := testDynamic.Client( + cb.UnstructuredV1alpha1(approvaltasks[0], "v1alpha1"), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + + c := command(t, approvaltasks, ns, dc) + args := []string{"at-1", "-n", "foo"} + + output, err := test.ExecuteCommand(c, args...) + golden.Assert(t, output, strings.ReplaceAll(fmt.Sprintf("%s.golden", t.Name()), "/", "-")) +} + +func TestDescribeApprovalTaskNotFound(t *testing.T) { + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "namespace", + }, + }, + } + + dc, err := testDynamic.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + + c := command(t, []*v1alpha1.ApprovalTask{}, ns, dc) + args := []string{"at-1", "-n", "foo"} + + output, err := test.ExecuteCommand(c, args...) + + expectedOutput := "Error: failed to Get ApprovalTasks at-1 from foo namespace\n" + if output != expectedOutput { + t.Errorf("Expected output to be %q, but got %q", expectedOutput, output) + } +} + +func command(t *testing.T, approvaltasks []*v1alpha1.ApprovalTask, ns []*corev1.Namespace, dc dynamic.Interface) *cobra.Command { + cs, _ := test.SeedTestData(t, test.Data{Approvaltasks: approvaltasks, Namespaces: ns}) + p := &test.Params{ApprovalTask: cs.ApprovalTask, Kube: cs.Kube, Dynamic: dc} + cs.ApprovalTask.Resources = cb.APIResourceList("v1alpha1", []string{"approvaltask"}) + + return Command(p) +} diff --git a/pkg/cli/cmd/describe/testdata/TestDescribeApprovalTask.golden b/pkg/cli/cmd/describe/testdata/TestDescribeApprovalTask.golden new file mode 100644 index 00000000..8dde37f2 --- /dev/null +++ b/pkg/cli/cmd/describe/testdata/TestDescribeApprovalTask.golden @@ -0,0 +1,16 @@ +📦 Name: at-1 +🗂 Namespace: foo + +👥 Approvers + * tekton + * cli + +👨‍💻 ApproverResponse + +Name ApproverResponse Message +tekton ❌ --- + +🌡️ Status + +NumberOfApprovalsRequired PendingApprovals STATUS +2 1 Rejected diff --git a/pkg/cli/cmd/root.go b/pkg/cli/cmd/root.go index 4c7511a3..c1aacd3e 100644 --- a/pkg/cli/cmd/root.go +++ b/pkg/cli/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( cli "github.com/openshift-pipelines/manual-approval-gate/pkg/cli" "github.com/openshift-pipelines/manual-approval-gate/pkg/cli/cmd/approve" + "github.com/openshift-pipelines/manual-approval-gate/pkg/cli/cmd/describe" "github.com/openshift-pipelines/manual-approval-gate/pkg/cli/cmd/list" "github.com/openshift-pipelines/manual-approval-gate/pkg/cli/flags" "github.com/spf13/cobra" @@ -21,6 +22,7 @@ func Root(p cli.Params) *cobra.Command { c.AddCommand(list.Command(p)) c.AddCommand(approve.Command(p)) + c.AddCommand(describe.Command(p)) return c } diff --git a/pkg/cli/formatter/field.go b/pkg/cli/formatter/field.go new file mode 100644 index 00000000..badb5c50 --- /dev/null +++ b/pkg/cli/formatter/field.go @@ -0,0 +1,30 @@ +package formatter + +import ( + "github.com/fatih/color" + "github.com/openshift-pipelines/manual-approval-gate/pkg/apis/approvaltask/v1alpha1" +) + +var ConditionColor = map[string]color.Attribute{ + "Rejected": color.FgHiRed, + "Approved": color.FgHiGreen, + "Pending": color.FgHiBlue, +} + +func ColorStatus(status string) string { + return color.New(ConditionColor[status]).Sprint(status) +} + +func State(at *v1alpha1.ApprovalTask) string { + var state string + + switch at.Status.State { + case "approved": + state = "Approved" + case "rejected": + state = "Rejected" + case "pending": + state = "Pending" + } + return ColorStatus(state) +} diff --git a/test/cli/describe/describe_test.go b/test/cli/describe/describe_test.go new file mode 100644 index 00000000..5bdc4d4b --- /dev/null +++ b/test/cli/describe/describe_test.go @@ -0,0 +1,33 @@ +//go:build e2e +// +build e2e + +package describe + +import ( + "fmt" + "strings" + "testing" + + "github.com/openshift-pipelines/manual-approval-gate/test/cli" + "github.com/openshift-pipelines/manual-approval-gate/test/client" + "github.com/openshift-pipelines/manual-approval-gate/test/resources" + "github.com/stretchr/testify/assert" + "gotest.tools/v3/golden" +) + +func TestApprovalTaskDescribeCommand(t *testing.T) { + tknApprovaltask, err := cli.NewTknApprovalTaskRunner() + assert.Nil(t, err) + + clients := client.Setup(t, "default") + + cr := resources.Create(t, clients, "./testdata/cr-1.yaml") + + _, err = resources.WaitForApprovalTaskCreation(clients.ApprovalTaskClient, cr.GetName(), cr.GetNamespace()) + if err != nil { + t.Fatal("Failed to get the approval task") + } + + res := tknApprovaltask.MustSucceed(t, "describe", cr.GetName(), "-n", "test-3") + golden.Assert(t, res.Stdout(), strings.ReplaceAll(fmt.Sprintf("%s.golden", t.Name()), "/", "-")) +} diff --git a/test/cli/describe/testdata/TestApprovalTaskDescribeCommand.golden b/test/cli/describe/testdata/TestApprovalTaskDescribeCommand.golden new file mode 100644 index 00000000..fa4d9402 --- /dev/null +++ b/test/cli/describe/testdata/TestApprovalTaskDescribeCommand.golden @@ -0,0 +1,13 @@ +📦 Name: at-1 +🗂 Namespace: test-3 + +👥 Approvers + * foo + * bar + * tekton + * kubernetes-admin + +🌡️ Status + +NumberOfApprovalsRequired PendingApprovals STATUS +2 2 Pending diff --git a/test/cli/describe/testdata/cr-1.yaml b/test/cli/describe/testdata/cr-1.yaml new file mode 100644 index 00000000..0b4c7228 --- /dev/null +++ b/test/cli/describe/testdata/cr-1.yaml @@ -0,0 +1,19 @@ +apiVersion: tekton.dev/v1beta1 +kind: CustomRun +metadata: + name: at-1 + namespace: test-3 +spec: + retries: 2 + customRef: + apiVersion: openshift-pipelines.org/v1alpha1 + kind: ApprovalTask + params: + - name: approvers + value: + - foo + - bar + - tekton + - kubernetes-admin + - name: numberOfApprovalsRequired + value: 2 \ No newline at end of file