Skip to content

Commit

Permalink
OPA policy validation supports specifying whether an action-item is e…
Browse files Browse the repository at this point in the history
…xpected (#66)
  • Loading branch information
ivanfetch-wt authored Jul 5, 2022
1 parent b59fd9b commit 5c01559
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 14 deletions.
10 changes: 7 additions & 3 deletions cmd/insights/validate_opa.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
)

var regoFileName, objectFileName, batchDir, objectNamespaceOverride, insightsInfoCluster, insightsInfoContext string
var expectActionItem opavalidation.ExpectActionItemOptions

// OPACmd represents the validate opa command
var OPACmd = &cobra.Command{
Expand All @@ -34,7 +35,7 @@ var OPACmd = &cobra.Command{
os.Exit(1)
}
if regoFileName != "" {
_, err := opavalidation.Run(regoFileName, objectFileName, fwrego.InsightsInfo{InsightsContext: insightsInfoContext, Cluster: insightsInfoCluster}, objectNamespaceOverride)
_, err := opavalidation.Run(regoFileName, objectFileName, expectActionItem, fwrego.InsightsInfo{InsightsContext: insightsInfoContext, Cluster: insightsInfoCluster}, objectNamespaceOverride)
if err != nil {
fmt.Printf("OPA policy failed validation: %v\n", err)
os.Exit(1)
Expand All @@ -43,7 +44,7 @@ var OPACmd = &cobra.Command{
}

if batchDir != "" {
_, failedPolicies, err := opavalidation.RunBatch(batchDir, fwrego.InsightsInfo{InsightsContext: insightsInfoContext, Cluster: insightsInfoCluster}, objectNamespaceOverride)
_, failedPolicies, err := opavalidation.RunBatch(batchDir, expectActionItem, fwrego.InsightsInfo{InsightsContext: insightsInfoContext, Cluster: insightsInfoCluster}, objectNamespaceOverride)
fmt.Println() // separate output from RunBatch
if err != nil {
fmt.Printf("OPA policies failed validation: %v\n", err)
Expand Down Expand Up @@ -82,8 +83,11 @@ func init() {
validateCmd.AddCommand(OPACmd)
OPACmd.Flags().StringVarP(&batchDir, "batch-directory", "b", "", "A directory containing OPA policy .rego files and corresponding Kubernetes manifest .yaml input files to validate. This option validates multiple OPA policies at once, and is mutually exclusive with the rego-file option.")
OPACmd.Flags().StringVarP(&regoFileName, "rego-file", "r", "", "An OPA policy file containing rego to validate. The --kube-object-file option is also required. This option validates a single policy, and is mutually exclusive with the batch-directory option.")
OPACmd.Flags().StringVarP(&objectFileName, "kube-object-file", "k", "", "A Kubernetes manifest to provide as input when validating a single OPA policy. This option is mutually exclusive with the batch-directory option.")
OPACmd.Flags().StringVarP(&objectFileName, "kube-object-file", "k", "", "A Kubernetes manifest to provide as input when validating a single OPA policy. This option is mutually exclusive with the batch-directory option. A manifest file ending in a .success.yaml extension is expected to return 0 action items. A manifest file ending in a .failure.yaml extension is expected to output one action item. See also the --expect-action-item option.")
OPACmd.Flags().StringVarP(&expectActionItem.SuccessFileExtension, "kube-manifest-success-ext", "e", ".success.yaml", "The extension for a Kubernetes manifest file name which, if found, indicates an OPA policy is NOT expected to return an action item.")
OPACmd.Flags().StringVarP(&expectActionItem.FailureFileExtension, "kube-manifest-failure-ext", "E", ".failure.yaml", "The extension for a Kubernetes manifest file name which, if found, indicates an OPA policy is expected to return an action item.")
OPACmd.Flags().StringVarP(&objectNamespaceOverride, "object-namespace", "N", "", "A Kubernetes namespace to override any defined in the Kubernetes object being passed as input to an OPA policy.")
OPACmd.Flags().StringVarP(&insightsInfoCluster, "insightsinfo-cluster", "l", "test", "A Kubernetes cluster name returned by the Insights-provided insightsinfo() rego function.")
OPACmd.Flags().StringVarP(&insightsInfoContext, "insightsinfo-context", "t", "Agent", "An Insights context returned by the Insights-provided insightsinfo() rego function. The context returned by Insights plugins is typically one of: CI/CD, Admission, or Agent.")
OPACmd.Flags().BoolVarP(&expectActionItem.Default, "expect-action-item", "i", true, "Whether to expect the OPA policy to output one action item (true) or 0 action items (false). This option is applied to Kubernetes manifest files with no .success.yaml nor .failure.yaml extension.")
}
40 changes: 29 additions & 11 deletions pkg/opavalidation/opavalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/open-policy-agent/opa/topdown"
"github.com/open-policy-agent/opa/types"
"github.com/sirupsen/logrus"
"github.com/thoas/go-funk"
)

const (
Expand All @@ -24,7 +25,7 @@ const (

// Run is a ValidateRego() wrapper that validates and prints resulting actionItems. This is
// meant to be called from a cobra.Command{}.
func Run(regoFileName, objectFileName string, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride string) (actionItems, error) {
func Run(regoFileName, objectFileName string, expectAIOptions ExpectActionItemOptions, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride string) (actionItems, error) {
b, err := ioutil.ReadFile(regoFileName)
if err != nil {
return nil, fmt.Errorf("error reading OPA policy %s: %v", regoFileName, err)
Expand All @@ -47,8 +48,12 @@ func Run(regoFileName, objectFileName string, insightsInfo fwrego.InsightsInfo,
if err != nil {
return actionItems, err
}
if len(actionItems) != 1 {
return actionItems, fmt.Errorf("%d action items were returned, but 1 is required", len(actionItems))
expectAI := expectAIOptions.ForFileName(objectFileName)
if expectAI && len(actionItems) != 1 {
return actionItems, fmt.Errorf("%d action items were returned, but 1 is expected", len(actionItems))
}
if !expectAI && len(actionItems) > 0 {
return actionItems, fmt.Errorf("%d action items were returned but none are expected", len(actionItems))
}
return actionItems, nil
}
Expand All @@ -57,21 +62,34 @@ func Run(regoFileName, objectFileName string, insightsInfo fwrego.InsightsInfo,
// not return the actionItems from each call to Run(), as there would not be correlation of
// actionItems to their OPA policy.
// This is meant to be called from a cobra.Command{}.
func RunBatch(batchDir string, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride string) (successfulPolicies, failedPolicies []string, err error) {
// Each OPA policy is validated with a Kubernetes manifest file named of the
// form {base rego filename} and the extensions .yaml, .success.yaml, and
// .failure.yaml (the last two of which are configurable).
func RunBatch(batchDir string, expectAIOptions ExpectActionItemOptions, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride string) (successfulPolicies, failedPolicies []string, err error) {
regoFiles, err := FindFilesWithExtension(batchDir, ".rego")
if err != nil {
return successfulPolicies, failedPolicies, fmt.Errorf("unable to list .rego files: %v", err)
}
for _, regoFileName := range regoFiles {
objectFileName := strings.TrimSuffix(regoFileName, filepath.Ext(regoFileName)) + ".yaml"
logrus.Infof("Starting validation of OPA policy %s with input %s\n", regoFileName, objectFileName)
_, err := Run(regoFileName, objectFileName, insightsInfo, objectNamespaceOverride)
if err != nil {
logrus.Errorf("Failed validation of OPA policy %s: %v\n", regoFileName, err)
objectFileNames, ok := expectAIOptions.getObjectFileNamesForPolicy(regoFileName)
if !ok {
logrus.Errorf("No Kubernetes manifest files found to use as input for validation of OPA policy %s", regoFileName)
failedPolicies = append(failedPolicies, regoFileName)
} else {
logrus.Infof("Success validating OPA policy %s\n", regoFileName)
continue
}
for _, objectFileName := range objectFileNames {
logrus.Infof("Validating OPA policy %s with input %s (expectActionItem=%v)", regoFileName, objectFileName, expectAIOptions.ForFileName(objectFileName))
_, err := Run(regoFileName, objectFileName, expectAIOptions, insightsInfo, objectNamespaceOverride)
if err != nil {
logrus.Errorf("Failed validation of OPA policy %s using input %s: %v\n", regoFileName, objectFileName, err)
if !funk.ContainsString(failedPolicies, regoFileName) {
failedPolicies = append(failedPolicies, regoFileName)
}
}
}
if !funk.ContainsString(failedPolicies, regoFileName) {
successfulPolicies = append(successfulPolicies, regoFileName)
logrus.Infof("Success validating OPA policy %s\n", regoFileName)
}
}
if len(failedPolicies) > 0 {
Expand Down
56 changes: 56 additions & 0 deletions pkg/opavalidation/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package opavalidation
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/fatih/color"
Expand Down Expand Up @@ -157,3 +159,57 @@ func (AIs actionItems) setEventType(ET string) {
AIs[n].EventType = ET
}
}

// ExpectActionItemOptions bundles multiple settings about whether and when
// OPA policies are expected to output an action item.
// If a Kubernetes manifest file has the SuccessFileExtension, no action item
// is expected.
// If a Kubernetes manifest file has the FailureFileExtension, an action item
// is expected.
// The default expectation of an action item is used when the Kubernetes
// manifest file has neither file extension.
type ExpectActionItemOptions struct {
Default bool // Used if none of the below filename extensions applies.
SuccessFileExtension, FailureFileExtension string
}

// ForFileName returns true if the given Kubernetes manifest file name should
// expectan OPA policy to output an action item.
func (o ExpectActionItemOptions) ForFileName(fileName string) bool {
LCFileName := strings.ToLower(fileName)
LCSuccessExtension := strings.ToLower(o.SuccessFileExtension)
LCFailureExtension := strings.ToLower(o.FailureFileExtension)
if strings.HasSuffix(LCFileName, LCSuccessExtension) {
logrus.Debugf("ExpectActionItem=%v for Kube manifest file %s due to its file extension %q", false, fileName, o.SuccessFileExtension)
return false
}
if strings.HasSuffix(LCFileName, LCFailureExtension) {
logrus.Debugf("ExpectActionItem=%v for Kube manifest file %s due to its file extension %q", true, fileName, o.FailureFileExtension)
return true
}
logrus.Debugf("ExpectActionItem=%v for Kube manifest file %s due to the default or the command-line flag.", o.Default, fileName)
return o.Default
}

// getObjectFileNamesForPolicy returns a list of existing file names matching
// the pattern {base rego file name}.yaml|.success.yaml|.failure.yaml (the
// latter two being configurable via the expectActionItemOptions struct).
func (o ExpectActionItemOptions) getObjectFileNamesForPolicy(regoFileName string) (objectFileNames []string, foundAny bool) {
baseFileName := strings.TrimSuffix(regoFileName, filepath.Ext(regoFileName))
var lookForFileNames []string = []string{
baseFileName + ".yaml",
baseFileName + o.SuccessFileExtension,
baseFileName + o.FailureFileExtension}
logrus.Debugf("Looking for these object files for policy %s: %v", regoFileName, lookForFileNames)
for _, potentialFileName := range lookForFileNames {
_, err := os.Stat(potentialFileName)
if err == nil {
objectFileNames = append(objectFileNames, potentialFileName)
foundAny = true
}
}
if foundAny {
logrus.Debugf("Matched these object files for policy %s: %v", regoFileName, objectFileNames)
}
return
}

0 comments on commit 5c01559

Please sign in to comment.