Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

engine: Refactor to allow for Rego version to be specified #1059

Merged
merged 1 commit into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions internal/commands/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ func NewPushCommand(ctx context.Context, logger *log.Logger) *cobra.Command {
if err := viper.BindPFlag("tls", cmd.Flags().Lookup("tls")); err != nil {
return fmt.Errorf("bind flag: %w", err)
}
if err := viper.BindPFlag("rego-version", cmd.Flags().Lookup("rego-version")); err != nil {
return fmt.Errorf("bind flag: %w", err)
}
return nil
},

Expand Down Expand Up @@ -107,7 +110,8 @@ func NewPushCommand(ctx context.Context, logger *log.Logger) *cobra.Command {
if dataPath == "" {
dataPath = policyPath
}
manifest, err := pushBundle(ctx, repository, policyPath, dataPath)
regoVersion := viper.GetString("rego-version")
manifest, err := pushBundle(ctx, repository, policyPath, dataPath, regoVersion)
if err != nil {
return fmt.Errorf("push bundle: %w", err)
}
Expand All @@ -120,11 +124,12 @@ func NewPushCommand(ctx context.Context, logger *log.Logger) *cobra.Command {
cmd.Flags().StringP("policy", "p", "policy", "Directory to push as a bundle")
cmd.Flags().StringP("data", "d", "", "Directory containing data to include in the bundle, defaults to the value of the policy flag")
cmd.Flags().BoolP("tls", "s", true, "Use TLS to access the registry")
cmd.Flags().String("rego-version", "v0", "Which version of Rego syntax to use. Options: v0, v1")

return &cmd
}

func pushBundle(ctx context.Context, repository, policyPath, dataPath string) (*ocispec.Descriptor, error) {
func pushBundle(ctx context.Context, repository, policyPath, dataPath, regoVersion string) (*ocispec.Descriptor, error) {
dest, err := remote.NewRepository(repository)
if err != nil {
return nil, fmt.Errorf("constructing repository: %w", err)
Expand All @@ -134,7 +139,7 @@ func pushBundle(ctx context.Context, repository, policyPath, dataPath string) (*
return nil, fmt.Errorf("setting up the registry client: %w", err)
}

layers, err := pushLayers(ctx, dest, policyPath, dataPath)
layers, err := pushLayers(ctx, dest, policyPath, dataPath, regoVersion)
if err != nil {
return nil, fmt.Errorf("pushing layers: %w", err)
}
Expand Down Expand Up @@ -171,7 +176,7 @@ func pushBundle(ctx context.Context, repository, policyPath, dataPath string) (*
return &manifestDesc, nil
}

func pushLayers(ctx context.Context, pusher content.Pusher, policyPath, dataPath string) ([]ocispec.Descriptor, error) {
func pushLayers(ctx context.Context, pusher content.Pusher, policyPath, dataPath, regoVersion string) ([]ocispec.Descriptor, error) {
var policyPaths []string
if policyPath != "" {
policyPaths = append(policyPaths, policyPath)
Expand All @@ -180,7 +185,15 @@ func pushLayers(ctx context.Context, pusher content.Pusher, policyPath, dataPath
if dataPath != "" {
dataPaths = append(dataPaths, dataPath)
}
engine, err := policy.LoadWithData(policyPaths, dataPaths, "", false)
capabilities, err := policy.LoadCapabilities("")
if err != nil {
return nil, fmt.Errorf("load capabilities: %w", err)
}
opts := policy.CompilerOptions{
Capabilities: capabilities,
RegoVersion: regoVersion,
}
engine, err := policy.LoadWithData(policyPaths, dataPaths, opts)
if err != nil {
return nil, fmt.Errorf("load: %w", err)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/commands/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func NewTestCommand(ctx context.Context) *cobra.Command {
"policy",
"proto-file-dirs",
"capabilities",
"rego-version",
"trace",
"strict",
"show-builtin-errors",
Expand Down Expand Up @@ -176,6 +177,7 @@ func NewTestCommand(ctx context.Context) *cobra.Command {
cmd.Flags().String("ignore", "", "A regex pattern which can be used for ignoring paths")
cmd.Flags().String("parser", "", fmt.Sprintf("Parser to use to parse the configurations. Valid parsers: %s", parser.Parsers()))
cmd.Flags().String("capabilities", "", "Path to JSON file that can restrict opa functionality against a given policy. Default: all operations allowed")
cmd.Flags().String("rego-version", "v0", "Which version of Rego syntax to use. Options: v0, v1")

cmd.Flags().StringP("output", "o", output.OutputStandard, fmt.Sprintf("Output format for conftest results - valid options are: %s", output.Outputs()))
cmd.Flags().Bool("junit-hide-message", false, "Do not include the violation message in the JUnit test name")
Expand Down
2 changes: 2 additions & 0 deletions internal/commands/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func NewVerifyCommand(ctx context.Context) *cobra.Command {
"quiet",
"junit-hide-message",
"capabilities",
"rego-version",
"strict",
"proto-file-dirs",
"show-builtin-errors",
Expand Down Expand Up @@ -144,6 +145,7 @@ func NewVerifyCommand(ctx context.Context) *cobra.Command {
cmd.Flags().Bool("junit-hide-message", false, "Do not include the violation message in the JUnit test name")

cmd.Flags().String("capabilities", "", "Path to JSON file that can restrict opa functionality against a given policy. Default: all operations allowed")
cmd.Flags().String("rego-version", "v0", "Which version of Rego syntax to use. Options: v0, v1")
cmd.Flags().StringSliceP("data", "d", []string{}, "A list of paths from which data for the rego policies will be recursively loaded")
cmd.Flags().StringSliceP("policy", "p", []string{"policy"}, "Path to the Rego policy files directory")

Expand Down
66 changes: 34 additions & 32 deletions policy/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,48 @@ type Engine struct {
docs map[string]string
}

type compilerOptions struct {
strict bool
capabilities *ast.Capabilities
// CompilerOptions defines the options for the Rego compiler.
type CompilerOptions struct {
Strict bool
RegoVersion string
Capabilities *ast.Capabilities
}

var (
warningRegex = regexp.MustCompile("^warn(_[a-zA-Z0-9]+)*$")
failureRegex = regexp.MustCompile("^(deny|violation)(_[a-zA-Z0-9]+)*$")
)

func newCompilerOptions(strict bool, capabilities string) (compilerOptions, error) {
c := ast.CapabilitiesForThisVersion()
if capabilities != "" {
f, err := os.Open(capabilities)
if err != nil {
return compilerOptions{}, fmt.Errorf("capabilities not opened: %w", err)
}
defer f.Close()
c, err = ast.LoadCapabilitiesJSON(f)
if err != nil {
return compilerOptions{}, fmt.Errorf("capabilities not loaded: %w", err)
}
}
return compilerOptions{
strict: strict,
capabilities: c,
}, nil
func newCompiler(opts CompilerOptions) *ast.Compiler {
return ast.NewCompiler().
WithEnablePrintStatements(true).
WithCapabilities(opts.Capabilities).
WithStrict(opts.Strict)
}

func newCompiler(c compilerOptions) *ast.Compiler {
return ast.NewCompiler().WithEnablePrintStatements(true).WithCapabilities(c.capabilities).WithStrict(c.strict)
// LoadCapabilities loads Rego JSON capabilities given a path. If no path is supplied, the default
// capabilities are returned.
func LoadCapabilities(path string) (*ast.Capabilities, error) {
if path == "" {
return ast.CapabilitiesForThisVersion(), nil
}
return ast.LoadCapabilitiesFile(path)
}

// Load returns an Engine after loading all of the specified policies.
func Load(policyPaths []string, c compilerOptions) (*Engine, error) {
policies, err := loader.NewFileLoader().WithProcessAnnotation(true).Filtered(policyPaths, func(_ string, info os.FileInfo, _ int) bool {
func Load(policyPaths []string, opts CompilerOptions) (*Engine, error) {
var regoVer ast.RegoVersion
switch opts.RegoVersion {
case "v0", "V0":
regoVer = ast.RegoV0
case "v1", "V1":
regoVer = ast.RegoV1
default:
return nil, fmt.Errorf("invalid Rego version: %s", opts.RegoVersion)
}

l := loader.NewFileLoader().WithProcessAnnotation(true).WithRegoVersion(regoVer)
policies, err := l.Filtered(policyPaths, func(_ string, info os.FileInfo, _ int) bool {
return !info.IsDir() && !strings.HasSuffix(info.Name(), bundle.RegoExt)
})

Expand All @@ -80,7 +86,7 @@ func Load(policyPaths []string, c compilerOptions) (*Engine, error) {
}

modules := policies.ParsedModules()
compiler := newCompiler(c)
compiler := newCompiler(opts)
compiler.Compile(modules)
if compiler.Failed() {
return nil, fmt.Errorf("get compiler: %w", compiler.Errors)
Expand Down Expand Up @@ -108,15 +114,11 @@ func Load(policyPaths []string, c compilerOptions) (*Engine, error) {
}

// LoadWithData returns an Engine after loading all of the specified policies and data paths.
func LoadWithData(policyPaths []string, dataPaths []string, capabilities string, strict bool) (*Engine, error) {
compilerOptions, err := newCompilerOptions(strict, capabilities)
if err != nil {
return nil, fmt.Errorf("get compiler options: %w", err)
}

func LoadWithData(policyPaths []string, dataPaths []string, opts CompilerOptions) (*Engine, error) {
engine := &Engine{}
if len(policyPaths) > 0 {
engine, err = Load(policyPaths, compilerOptions)
var err error
engine, err = Load(policyPaths, opts)
if err != nil {
return nil, fmt.Errorf("loading policies: %w", err)
}
Expand Down
117 changes: 70 additions & 47 deletions policy/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ import (
"github.com/open-policy-agent/opa/loader"
)

func testOptions(t *testing.T) CompilerOptions {
t.Helper()
return CompilerOptions{
Capabilities: ast.CapabilitiesForThisVersion(),
RegoVersion: "v0",
}
}

func TestException(t *testing.T) {
ctx := context.Background()

policies := []string{"../examples/exceptions/policy"}
compilerOptions, _ := newCompilerOptions(false, "")
engine, err := Load(policies, compilerOptions)
engine, err := Load(policies, testOptions(t))
if err != nil {
t.Fatalf("loading policies: %v", err)
}
Expand Down Expand Up @@ -59,8 +66,7 @@ func TestTracing(t *testing.T) {
ctx := context.Background()

policies := []string{"../examples/kubernetes/policy"}
compilerOptions, _ := newCompilerOptions(false, "")
engine, err := Load(policies, compilerOptions)
engine, err := Load(policies, testOptions(t))
if err != nil {
t.Fatalf("loading policies: %v", err)
}
Expand Down Expand Up @@ -89,8 +95,7 @@ func TestTracing(t *testing.T) {
ctx := context.Background()

policies := []string{"../examples/kubernetes/policy"}
compilerOptions, _ := newCompilerOptions(false, "")
engine, err := Load(policies, compilerOptions)
engine, err := Load(policies, testOptions(t))
if err != nil {
t.Fatalf("loading policies: %v", err)
}
Expand Down Expand Up @@ -119,8 +124,7 @@ func TestMultifileYaml(t *testing.T) {
ctx := context.Background()

policies := []string{"../examples/kubernetes/policy"}
compilerOptions, _ := newCompilerOptions(false, "")
engine, err := Load(policies, compilerOptions)
engine, err := Load(policies, testOptions(t))
if err != nil {
t.Fatalf("loading policies: %v", err)
}
Expand Down Expand Up @@ -166,8 +170,7 @@ func TestDockerfile(t *testing.T) {
ctx := context.Background()

policies := []string{"../examples/docker/policy"}
compilerOptions, _ := newCompilerOptions(false, "")
engine, err := Load(policies, compilerOptions)
engine, err := Load(policies, testOptions(t))
if err != nil {
t.Fatalf("loading policies: %v", err)
}
Expand Down Expand Up @@ -379,32 +382,10 @@ func TestProblematicIf(t *testing.T) {
}

func TestLoadWithData(t *testing.T) {
tmpDir := t.TempDir()

inaccessibleCapabilitiesPath := filepath.Join(tmpDir, "capabilities")
err := os.WriteFile(inaccessibleCapabilitiesPath, []byte(""), 0o000)
if err != nil {
t.Fatalf("failed to write empty policy file: %v", err)
}

t.Cleanup(func() {
err := os.Chmod(inaccessibleCapabilitiesPath, 0o600)
if err != nil {
t.Fatalf("failed to restore capabilities file permissions: %v", err)
}
})

invalidCapabilitiesPath := filepath.Join(tmpDir, "invalid-capabilities")
err = os.WriteFile(invalidCapabilitiesPath, []byte("invalid json"), 0o600)
if err != nil {
t.Fatalf("failed to write invalid capabilities file: %v", err)
}

testCases := []struct {
desc string
policyPaths []string
dataPaths []string
capabilities string
strict bool
wantPolicies bool
wantDocs bool
Expand Down Expand Up @@ -441,25 +422,16 @@ func TestLoadWithData(t *testing.T) {
dataPaths: []string{"nonexistent/data.yaml"},
wantErr: true,
},
{
desc: "Inaccessible capabilities file",
policyPaths: []string{"../examples/kubernetes/policy"},
dataPaths: []string{"../examples/kubernetes/service.yaml"},
capabilities: inaccessibleCapabilitiesPath,
wantErr: true,
},
{
desc: "Invalid capabilities file",
policyPaths: []string{"../examples/kubernetes/policy"},
dataPaths: []string{"../examples/kubernetes/service.yaml"},
capabilities: invalidCapabilitiesPath,
wantErr: true,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
engine, err := LoadWithData(tc.policyPaths, tc.dataPaths, tc.capabilities, tc.strict)
opts := CompilerOptions{
Strict: tc.strict,
Capabilities: ast.CapabilitiesForThisVersion(),
RegoVersion: "v0",
}
engine, err := LoadWithData(tc.policyPaths, tc.dataPaths, opts)
if tc.wantErr {
if err == nil {
t.Fatal("expected error but got none")
Expand Down Expand Up @@ -502,6 +474,57 @@ func TestLoadWithData(t *testing.T) {
}
}

func TestLoadCapabilities(t *testing.T) {
tmpDir := t.TempDir()

inaccessibleCapabilitiesPath := filepath.Join(tmpDir, "capabilities")
err := os.WriteFile(inaccessibleCapabilitiesPath, []byte(""), 0o000)
if err != nil {
t.Fatalf("failed to write empty policy file: %v", err)
}

t.Cleanup(func() {
err := os.Chmod(inaccessibleCapabilitiesPath, 0o600)
if err != nil {
t.Fatalf("failed to restore capabilities file permissions: %v", err)
}
})

invalidCapabilitiesPath := filepath.Join(tmpDir, "invalid-capabilities")
err = os.WriteFile(invalidCapabilitiesPath, []byte("invalid json"), 0o600)
if err != nil {
t.Fatalf("failed to write invalid capabilities file: %v", err)
}

tests := []struct {
desc string
path string
wantErr bool
}{
{
desc: "Inaccessible capabilities file",
path: inaccessibleCapabilitiesPath,
wantErr: true,
},
{
desc: "Invalid capabilities file",
path: invalidCapabilitiesPath,
wantErr: true,
},
{
desc: "No path does not error",
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
_, err := LoadCapabilities(tc.path)
if gotErr := err != nil; gotErr != tc.wantErr {
t.Errorf("LoadCapabilities(%s) error = %v, want %v", tc.path, gotErr, tc.wantErr)
}
})
}
}

func TestNamespaces(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading