Skip to content

Commit

Permalink
Merge pull request #888 from stacklok/table-policy-status
Browse files Browse the repository at this point in the history
cli: Add table output for `policy_status` sub-command
  • Loading branch information
JAORMX authored Sep 7, 2023
2 parents 192a58d + bcb3a48 commit e56ea82
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 13 deletions.
17 changes: 10 additions & 7 deletions cmd/cli/app/policy_status/policy_status_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/stacklok/mediator/cmd/cli/app"
"github.com/stacklok/mediator/internal/util"
"github.com/stacklok/mediator/pkg/entities"
pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
Expand Down Expand Up @@ -55,10 +56,9 @@ mediator control plane for an specific provider/group or policy id, entity type
entityType := viper.GetString("entity-type")
format := viper.GetString("output")

// the linter complains that this should be a constant doing that should probably be part
// of a larger refactor of the CLI (see issue #690)
// nolint: goconst
if format != "json" && format != "yaml" {
switch format {
case app.JSON, app.YAML, app.Table:
default:
return fmt.Errorf("error: invalid format: %s", format)
}

Expand Down Expand Up @@ -96,14 +96,17 @@ mediator control plane for an specific provider/group or policy id, entity type
return fmt.Errorf("error getting policy status: %w", err)
}

if format == "json" {
switch format {
case app.JSON:
out, err := util.GetJsonFromProto(resp)
util.ExitNicelyOnError(err, "Error getting json from proto")
fmt.Println(out)
} else {
case app.YAML:
out, err := util.GetYamlFromProto(resp)
util.ExitNicelyOnError(err, "Error getting yaml from proto")
fmt.Println(out)
case app.Table:
handlePolicyStatusListTable(cmd, resp)
}

return nil
Expand All @@ -118,5 +121,5 @@ func init() {
policystatus_getCmd.Flags().StringP("entity-type", "t", "",
fmt.Sprintf("the entity type to get policy status for (one of %s)", entities.KnownTypesCSV()))
policystatus_getCmd.Flags().Int32P("entity", "e", 0, "entity id to get policy status for")
policystatus_getCmd.Flags().StringP("output", "o", "yaml", "Output format (json or yaml)")
policystatus_getCmd.Flags().StringP("output", "o", app.Table, "Output format (json, yaml or table)")
}
41 changes: 35 additions & 6 deletions cmd/cli/app/policy_status/policy_status_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/stacklok/mediator/cmd/cli/app"
"github.com/stacklok/mediator/internal/util"
pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
)
Expand Down Expand Up @@ -51,9 +52,11 @@ mediator control plane for an specific provider/group or policy id.`,
group := viper.GetString("group")
policyId := viper.GetInt32("policy")
format := viper.GetString("output")
all := viper.GetBool("all")
all := viper.GetBool("detailed")

if format != "json" && format != "yaml" {
switch format {
case app.JSON, app.YAML, app.Table:
default:
return fmt.Errorf("error: invalid format: %s", format)
}

Expand Down Expand Up @@ -84,14 +87,21 @@ mediator control plane for an specific provider/group or policy id.`,
return fmt.Errorf("error getting policy status: %w", err)
}

if format == "json" {
switch format {
case app.JSON:
out, err := util.GetJsonFromProto(resp)
util.ExitNicelyOnError(err, "Error getting json from proto")
fmt.Println(out)
} else {
case app.YAML:
out, err := util.GetYamlFromProto(resp)
util.ExitNicelyOnError(err, "Error getting yaml from proto")
fmt.Println(out)
case app.Table:
handlePolicyStatusListTable(cmd, resp)

if all {
handleRuleEvaluationStatusListTable(cmd, resp)
}
}

return nil
Expand All @@ -103,6 +113,25 @@ func init() {
policystatus_listCmd.Flags().StringP("provider", "p", "github", "Provider to list policy status for")
policystatus_listCmd.Flags().StringP("group", "g", "", "group id to list policy status for")
policystatus_listCmd.Flags().Int32P("policy", "i", 0, "policy id to list policy status for")
policystatus_listCmd.Flags().StringP("output", "o", "yaml", "Output format (json or yaml)")
policystatus_listCmd.Flags().BoolP("all", "a", false, "List all policy violations")
policystatus_listCmd.Flags().StringP("output", "o", app.Table, "Output format (json, yaml or table)")
policystatus_listCmd.Flags().BoolP("detailed", "d", false, "List all policy violations")
}

func handlePolicyStatusListTable(cmd *cobra.Command, resp *pb.GetPolicyStatusByIdResponse) {
table := initializePolicyStatusTable(cmd)

renderPolicyStatusTable(resp.PolicyStatus, table)

table.Render()
}

func handleRuleEvaluationStatusListTable(cmd *cobra.Command, resp *pb.GetPolicyStatusByIdResponse) {
table := initializeRuleEvaluationStatusTable(cmd)

for idx := range resp.RuleEvaluationStatus {
reval := resp.RuleEvaluationStatus[idx]
renderRuleEvaluationStatusTable(reval, table)
}

table.Render()
}
164 changes: 164 additions & 0 deletions cmd/cli/app/policy_status/table_render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package policy_status

import (
"fmt"
"strings"
"time"

"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"

pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
)

const (
successStatus = "success"
failureStatus = "failure"
errorStatus = "error"
skippedStatus = "skipped"
pendingStatus = "pending"
)

func initializePolicyStatusTable(cmd *cobra.Command) *tablewriter.Table {
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader([]string{"Id", "Name", "Overall Status", "Last Updated"})
table.SetRowLine(true)
table.SetRowSeparator("-")
table.SetAutoWrapText(true)
table.SetReflowDuringAutoWrap(true)

return table
}

func renderPolicyStatusTable(
ps *pb.PolicyStatus,
table *tablewriter.Table,
) {
row := []string{
fmt.Sprintf("%d", ps.PolicyId),
ps.PolicyName,
getStatusText(ps.PolicyStatus),
ps.LastUpdated.AsTime().Format(time.RFC3339),
}

table.Rich(row, []tablewriter.Colors{
{},
{},
getStatusColor(ps.PolicyStatus),
{},
})
}

func initializeRuleEvaluationStatusTable(cmd *cobra.Command) *tablewriter.Table {
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader([]string{
"Policy ID", "Rule ID", "Rule Name", "Entity", "Status", "Entity Info", "Guidance"})
table.SetRowLine(true)
table.SetRowSeparator("-")
table.SetAutoMergeCellsByColumnIndex([]int{0})
// This is needed for the rule definition and rule parameters
table.SetAutoWrapText(false)

return table
}

func renderRuleEvaluationStatusTable(
reval *pb.RuleEvaluationStatus,
table *tablewriter.Table,
) {
row := []string{
fmt.Sprintf("%d", reval.PolicyId),
fmt.Sprintf("%d", reval.RuleId),
reval.RuleName,
reval.Entity,
getStatusText(reval.Status),
mapToYAMLOrEmpty(reval.EntityInfo),
guidanceOrEncouragement(reval.Status, reval.Guidance),
}

table.Rich(row, []tablewriter.Colors{
{},
{},
{},
{},
getStatusColor(reval.Status),
{},
{},
})
}

// Gets a friendly status text with an emoji
func getStatusText(status string) string {
// statuses can be 'success', 'failure', 'error', 'skipped', 'pending'
switch strings.ToLower(status) {
case successStatus:
return "✅ Success"
case failureStatus:
return "❌ Failure"
case errorStatus:
return "❌ Error"
case skippedStatus:
return "⚠️ Skipped"
case pendingStatus:
return "⏳ Pending"
default:
return "⚠️ Unknown"
}
}

func getStatusColor(status string) tablewriter.Colors {
// statuses can be 'success', 'failure', 'error', 'skipped', 'pending'
switch strings.ToLower(status) {
case successStatus:
return tablewriter.Colors{tablewriter.FgGreenColor}
case failureStatus:
return tablewriter.Colors{tablewriter.FgRedColor}
case errorStatus:
return tablewriter.Colors{tablewriter.FgRedColor}
case skippedStatus:
return tablewriter.Colors{tablewriter.FgYellowColor}
default:
return tablewriter.Colors{}
}
}

func mapToYAMLOrEmpty(m map[string]string) string {
if m == nil {
return ""
}

yamlText, err := yaml.Marshal(m)
if err != nil {
return ""
}

return string(yamlText)
}

func guidanceOrEncouragement(status, guidance string) string {
if status == successStatus && guidance == "" {
return "👍"
}

if guidance == "" {
return "No guidance available for this rule 😞"
}

return guidance
}

0 comments on commit e56ea82

Please sign in to comment.