Skip to content

Commit

Permalink
INSIGHTS-519 Support .util validation during validation runtime (#217)
Browse files Browse the repository at this point in the history
* [WIP] INSIGHTS-520 Lib support to OPA validation

* Removed code

* Removed code

* Removed code

* Fixed issue

* Adding etsts

* Upgraded libs

* Revert "Upgraded libs"

This reverts commit 29da67b.

* Upgraded libs

* Added tests

* Added tests

* Added tests

* Added tests

* Added tests

* Merge

* Fixed issue

* Fixed issue
  • Loading branch information
jdesouza authored Dec 4, 2024
1 parent 66c21ae commit 4c9e251
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 15 deletions.
7 changes: 4 additions & 3 deletions pkg/cli/validate_opa.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/spf13/cobra"
)

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

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

if batchDir != "" {
_, failedPolicies, err := opavalidation.RunBatch(batchDir, expectActionItem, fwrego.InsightsInfo{InsightsContext: insightsInfoContext, Cluster: insightsInfoCluster}, objectNamespaceOverride)
_, failedPolicies, err := opavalidation.RunBatch(batchDir, expectActionItem, fwrego.InsightsInfo{InsightsContext: insightsInfoContext, Cluster: insightsInfoCluster}, objectNamespaceOverride, libsDir)
fmt.Println() // separate output from RunBatch
if err != nil {
fmt.Printf("OPA policies failed validation: %v\n", err)
Expand Down Expand Up @@ -88,5 +88,6 @@ func init() {
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().StringVarP(&libsDir, "libs-dir", "L", "", "A directory containing additional rego libraries to load. This option is not required, but can be used to load additional rego libraries.")
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.")
}
67 changes: 56 additions & 11 deletions pkg/opavalidation/opavalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"

fwrego "github.com/fairwindsops/insights-plugins/plugins/opa/pkg/rego"
Expand All @@ -24,7 +26,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, expectAIOptions ExpectActionItemOptions, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride string) (actionItems, error) {
func Run(regoFileName, objectFileName string, expectAIOptions ExpectActionItemOptions, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride, libsDir string) (actionItems, error) {
b, err := os.ReadFile(regoFileName)
if err != nil {
return nil, fmt.Errorf("error reading OPA policy %s: %v", regoFileName, err)
Expand All @@ -34,9 +36,29 @@ func Run(regoFileName, objectFileName string, expectAIOptions ExpectActionItemOp
if err != nil {
return nil, fmt.Errorf("error reading Kubernetes manifest %s: %v", objectFileName, err)
}
libs := map[string]string{}
if libsDir != "" {
files, err := FindFilesWithExtension(libsDir, ".rego")
if err != nil {
return nil, fmt.Errorf("unable to list .rego files in %s: %v", libsDir, err)
}
for _, lib := range files {
libContent, err := os.ReadFile(lib)
if err != nil {
return nil, fmt.Errorf("error reading OPA library %s: %v", lib, err)
}
if !IsOPACustomLibrary(string(libContent)) {
logrus.Warnf("Skipping non-OPA library %s", lib)
continue
}
libName := strings.TrimSuffix(filepath.Base(lib), filepath.Ext(lib))
libs[libName] = string(libContent)
}
}

baseRegoFileName := filepath.Base(regoFileName)
eventType := strings.TrimSuffix(baseRegoFileName, filepath.Ext(baseRegoFileName))
actionItems, err := ValidateRego(context.TODO(), regoContent, b, insightsInfo, eventType, objectNamespaceOverride)
actionItems, err := ValidateRego(context.TODO(), regoContent, b, insightsInfo, eventType, objectNamespaceOverride, libs)
if err != nil {
return actionItems, err
}
Expand Down Expand Up @@ -64,7 +86,7 @@ func Run(regoFileName, objectFileName string, expectAIOptions ExpectActionItemOp
// 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) {
func RunBatch(batchDir string, expectAIOptions ExpectActionItemOptions, insightsInfo fwrego.InsightsInfo, objectNamespaceOverride, libsDir 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)
Expand All @@ -78,7 +100,7 @@ func RunBatch(batchDir string, expectAIOptions ExpectActionItemOptions, insights
}
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)
_, err := Run(regoFileName, objectFileName, expectAIOptions, insightsInfo, objectNamespaceOverride, libsDir)
if err != nil {
logrus.Errorf("Failed validation of OPA policy %s using input %s: %v\n", regoFileName, objectFileName, err)
if !lo.Contains(failedPolicies, regoFileName) {
Expand All @@ -99,7 +121,7 @@ func RunBatch(batchDir string, expectAIOptions ExpectActionItemOptions, insights

// ValidateRego validates rego by executing rego with an input object.
// Validation includes signatures for Insights-provided rego functions.
func ValidateRego(ctx context.Context, regoAsString string, objectAsBytes []byte, insightsInfo fwrego.InsightsInfo, eventType string, objectNamespaceOverride string) (actionItems, error) {
func ValidateRego(ctx context.Context, regoAsString string, objectAsBytes []byte, insightsInfo fwrego.InsightsInfo, eventType string, objectNamespaceOverride string, libs map[string]string) (actionItems, error) {
if !strings.Contains(regoAsString, "package fairwinds") {
return nil, errors.New("policy must be within a fairwinds package. The policy must contain the statement: package fairwinds")
}
Expand All @@ -111,7 +133,7 @@ func ValidateRego(ctx context.Context, regoAsString string, objectAsBytes []byte
if err != nil {
return nil, fmt.Errorf("while overriding object namespace with %q: %v", objectNamespaceOverride, err)
}
regoResult, err := runRegoForObject(ctx, regoAsString, objectAsMap, insightsInfo)
regoResult, err := runRegoForObject(ctx, regoAsString, objectAsMap, insightsInfo, libs)
if err != nil {
return nil, err
}
Expand All @@ -128,8 +150,8 @@ func ValidateRego(ctx context.Context, regoAsString string, objectAsBytes []byte
}

// runRegoForObject executes rego with a Kubernetes object as input.
func runRegoForObject(ctx context.Context, regoAsString string, object map[string]interface{}, insightsInfo fwrego.InsightsInfo) (rego.ResultSet, error) {
query, err := rego.New(rego.EnablePrintStatements(true), rego.PrintHook(topdown.NewPrintHook(os.Stdout)),
func runRegoForObject(ctx context.Context, regoAsString string, object map[string]interface{}, insightsInfo fwrego.InsightsInfo, libs map[string]string) (rego.ResultSet, error) {
opts := []func(r *rego.Rego){rego.EnablePrintStatements(true), rego.PrintHook(topdown.NewPrintHook(os.Stdout)),
rego.Query("results = data"),
rego.Module("fairwinds", regoAsString),
rego.Function2(
Expand All @@ -151,14 +173,37 @@ func runRegoForObject(ctx context.Context, regoAsString string, object map[strin
Name: "insightsinfo",
Decl: types.NewFunction(types.Args(types.S), types.A),
},
fwrego.GetInsightsInfoFunction(&insightsInfo))).PrepareForEval(ctx)
fwrego.GetInsightsInfoFunction(&insightsInfo),
),
}
var libNames []string
for libName := range libs {
libNames = append(libNames, libName)
}
sort.Strings(libNames)
for _, libName := range libNames {
opts = append(opts, rego.Module(libName, libs[libName]))
}
query, err := rego.New(opts...).PrepareForEval(ctx)
if err != nil {
return nil, err
return nil, fmt.Errorf("error preparing rego for evaluation: %v", err)
}
preparedInput := rego.EvalInput(object)
rs, err := query.Eval(ctx, preparedInput)
if err != nil {
return nil, err
return nil, fmt.Errorf("error evaluating rego: %v", err)
}
return rs, nil
}

var isCheckRE = regexp.MustCompile(`^package\s+fairwinds\s*(#.*)?$`)

func IsOPACustomLibrary(rego string) bool {
for _, line := range strings.Split(strings.TrimSuffix(rego, "\n"), "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "package") {
isCheck := isCheckRE.MatchString(strings.TrimSpace(line))
return !isCheck
}
}
return false
}
14 changes: 13 additions & 1 deletion pkg/opavalidation/opavalidation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/fairwindsops/insights-cli/pkg/opavalidation"
fwrego "github.com/fairwindsops/insights-plugins/plugins/opa/pkg/rego"
"github.com/stretchr/testify/assert"
)

func TestValidateRego(t *testing.T) {
Expand Down Expand Up @@ -58,7 +59,7 @@ func TestValidateRego(t *testing.T) {
if err != nil {
t.Fatalf("error reading %s: %v", tc.objectFileName, err)
}
gotActionItems, gotErr := opavalidation.ValidateRego(context.TODO(), regoAsString, objectAsBytes, fwrego.InsightsInfo{}, "TestEvent", "")
gotActionItems, gotErr := opavalidation.ValidateRego(context.TODO(), regoAsString, objectAsBytes, fwrego.InsightsInfo{}, "TestEvent", "", nil)
if !tc.expectError && gotErr != nil {
t.Fatal(gotErr)
}
Expand All @@ -76,3 +77,14 @@ func TestValidateRego(t *testing.T) {
})
}
}

func TestRunWithLibs(t *testing.T) {
ais, err := opavalidation.Run("testdata/fileWithLib.rego", "testdata/pod1.yaml", opavalidation.ExpectActionItemOptions{}, fwrego.InsightsInfo{}, "", "testdata/libs")
assert.NoError(t, err)
assert.Len(t, ais, 0)
ais, err = opavalidation.Run("testdata/fileWithLib.rego", "testdata/pod2.yaml", opavalidation.ExpectActionItemOptions{}, fwrego.InsightsInfo{}, "", "testdata/libs")
assert.Error(t, err)
assert.Equal(t, "1 action items were returned but none are expected", err.Error())
assert.Len(t, ais, 1)
assert.Equal(t, "Label is missing", ais[0].Title)
}
17 changes: 17 additions & 0 deletions pkg/opavalidation/testdata/fileWithLib.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package fairwinds

import data.utils.array_contains

labelrequired[actionItem] {
requiredLabelValue := "development"
provided := [input.metadata.labels[_]]
not array_contains(provided, requiredLabelValue)
description := sprintf("Label value %v is missing", [requiredLabelValue])
actionItem := {
"title": "Label is missing",
"description": description,
"severity": .2,
"remediation": "Add the label",
"category": "Reliability"
}
}
1 change: 1 addition & 0 deletions pkg/opavalidation/testdata/libs/invalid.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
123
17 changes: 17 additions & 0 deletions pkg/opavalidation/testdata/libs/invalidFairwindsLib.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package fairwinds

import data.utils.array_contains

labelrequired[actionItem] {
requiredLabelValue := "development"
provided := [input.metadata.labels[_]]
not array_contains(provided, requiredLabelValue)
description := sprintf("Label value %v is missing", [requiredLabelValue])
actionItem := {
"title": "Label is missing",
"description": description,
"severity": .2,
"remediation": "Add the label",
"category": "Reliability"
}
}
5 changes: 5 additions & 0 deletions pkg/opavalidation/testdata/libs/utils.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package utils

array_contains(array, elem) {
lower(array[_]) = lower(elem)
}
12 changes: 12 additions & 0 deletions pkg/opavalidation/testdata/pod1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: policy-test
labels:
department: development
spec:
replicas: 1
template:
spec:
containers:
- image: busybox
12 changes: 12 additions & 0 deletions pkg/opavalidation/testdata/pod2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: policy-test
labels:
department: hr
spec:
replicas: 1
template:
spec:
containers:
- image: busybox

0 comments on commit 4c9e251

Please sign in to comment.