-
Notifications
You must be signed in to change notification settings - Fork 297
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add parse_config and parse_config_file builtins (#726)
* Add parse_config(parser, config) builtin This builtin can be used to parse configuration snippets in-line in Rego policies, making it easier to write unit tests in the same configuration language that will be tested. Signed-off-by: James Alseth <james@jalseth.me> * Add parse_config_file(path) builtin Enables users to parse arbitrary config files from the Rego policies. Signed-off-by: James Alseth <james@jalseth.me>
- Loading branch information
Showing
8 changed files
with
344 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package builtins | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"path/filepath" | ||
|
||
"github.com/open-policy-agent/conftest/parser" | ||
"github.com/open-policy-agent/opa/ast" | ||
"github.com/open-policy-agent/opa/ast/location" | ||
"github.com/open-policy-agent/opa/rego" | ||
"github.com/open-policy-agent/opa/types" | ||
) | ||
|
||
func init() { | ||
registerParseConfig() | ||
registerParseConfigFile() | ||
} | ||
|
||
func registerParseConfig() { | ||
decl := rego.Function{ | ||
Name: "parse_config", | ||
Decl: types.NewFunction( | ||
types.Args(types.S, types.S), // parser name, configuration | ||
types.NewObject(nil, types.NewDynamicProperty(types.S, types.NewAny())), // map[string]interface{} aka JSON | ||
), | ||
} | ||
rego.RegisterBuiltin2(&decl, parseConfig) | ||
} | ||
|
||
func registerParseConfigFile() { | ||
decl := rego.Function{ | ||
Name: "parse_config_file", | ||
Decl: types.NewFunction( | ||
types.Args(types.S), // path to configuration file | ||
types.NewObject(nil, types.NewDynamicProperty(types.S, types.NewAny())), // map[string]interface{} aka JSON | ||
), | ||
} | ||
rego.RegisterBuiltin1(&decl, parseConfigFile) | ||
} | ||
|
||
// parseConfig takes a parser name and configuration as strings and returns the | ||
// parsed configuration as a Rego object. This can be used to parse all of the | ||
// configuration formats conftest supports in-line in Rego policies. | ||
func parseConfig(bctx rego.BuiltinContext, op1, op2 *ast.Term) (*ast.Term, error) { | ||
args, err := decodeArgs([]*ast.Term{op1, op2}) | ||
if err != nil { | ||
return nil, fmt.Errorf("decode args: %w", err) | ||
} | ||
parserName, ok := args[0].(string) | ||
if !ok { | ||
return nil, fmt.Errorf("parser name %v [%T] is not expected type string", args[0], args[0]) | ||
} | ||
config, ok := args[1].(string) | ||
if !ok { | ||
return nil, fmt.Errorf("config %v [%T] is not expected type string", args[1], args[1]) | ||
} | ||
parser, err := parser.New(parserName) | ||
if err != nil { | ||
return nil, fmt.Errorf("create config parser: %w", err) | ||
} | ||
|
||
return toAST(bctx, parser, []byte(config)) | ||
} | ||
|
||
// parseConfigFile takes a config file path, parses the config file, and | ||
// returns the parsed configuration as a Rego object. | ||
func parseConfigFile(bctx rego.BuiltinContext, op1 *ast.Term) (*ast.Term, error) { | ||
args, err := decodeArgs([]*ast.Term{op1}) | ||
if err != nil { | ||
return nil, fmt.Errorf("decode args: %w", err) | ||
} | ||
file, ok := args[0].(string) | ||
if !ok { | ||
return nil, fmt.Errorf("file %v [%T] is not expected type string", args[0], args[0]) | ||
} | ||
filePath := filepath.Join(filepath.Dir(bctx.Location.File), file) | ||
parser, err := parser.NewFromPath(filePath) | ||
if err != nil { | ||
return nil, fmt.Errorf("create config parser: %w", err) | ||
} | ||
contents, err := ioutil.ReadFile(filePath) | ||
if err != nil { | ||
return nil, fmt.Errorf("read config file %s: %w", filePath, err) | ||
} | ||
|
||
return toAST(bctx, parser, contents) | ||
} | ||
|
||
func decodeArgs(args []*ast.Term) ([]interface{}, error) { | ||
decoded := make([]interface{}, len(args)) | ||
for i, arg := range args { | ||
iface, err := ast.ValueToInterface(arg.Value, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("ast.ValueToInterface: %w", err) | ||
} | ||
decoded[i] = iface | ||
} | ||
|
||
return decoded, nil | ||
} | ||
|
||
func toAST(bctx rego.BuiltinContext, parser parser.Parser, contents []byte) (*ast.Term, error) { | ||
var cfg map[string]interface{} | ||
if err := parser.Unmarshal(contents, &cfg); err != nil { | ||
return nil, fmt.Errorf("unmarshal config: %w", err) | ||
} | ||
val, err := ast.InterfaceToValue(cfg) | ||
if err != nil { | ||
return nil, fmt.Errorf("convert config to ast.Value: %w", err) | ||
} | ||
var loc *location.Location | ||
if bctx.Location != nil { | ||
loc = bctx.Location | ||
} else { | ||
loc = &ast.Location{ | ||
File: "-", // stdin | ||
Text: contents, | ||
} | ||
} | ||
|
||
return &ast.Term{Value: val, Location: loc}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package builtins | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/open-policy-agent/conftest/parser" | ||
"github.com/open-policy-agent/opa/ast" | ||
"github.com/open-policy-agent/opa/rego" | ||
) | ||
|
||
func TestParseConfig(t *testing.T) { | ||
testCases := []struct { | ||
desc string | ||
parser string | ||
config string | ||
wantErrMsg string | ||
}{ | ||
{ | ||
desc: "No parser supplied", | ||
wantErrMsg: "create config parser", | ||
}, | ||
{ | ||
desc: "Invalid parser supplied", | ||
parser: "no-such-parser", | ||
wantErrMsg: "create config parser", | ||
}, | ||
{ | ||
desc: "Invalid YAML", | ||
parser: parser.YAML, | ||
config: "```NOTVALID!", | ||
wantErrMsg: "unmarshal config", | ||
}, | ||
{ | ||
desc: "Empty YAML", | ||
parser: parser.YAML, | ||
}, | ||
{ | ||
desc: "Valid YAML", | ||
parser: parser.YAML, | ||
config: `some_field: some_value | ||
another_field: | ||
- arr1 | ||
- arr2`, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.desc, func(t *testing.T) { | ||
pv, err := ast.InterfaceToValue(tc.parser) | ||
if err != nil { | ||
t.Fatalf("Could not convert parser %q to ast.Value: %v", tc.parser, err) | ||
} | ||
cv, err := ast.InterfaceToValue(tc.config) | ||
if err != nil { | ||
t.Fatalf("Could not convert config %q to ast.Value: %v", tc.config, err) | ||
} | ||
|
||
bctx := rego.BuiltinContext{Context: context.Background()} | ||
_, err = parseConfig(bctx, ast.NewTerm(pv), ast.NewTerm(cv)) | ||
if err == nil && tc.wantErrMsg == "" { | ||
return | ||
} | ||
if err != nil && tc.wantErrMsg == "" { | ||
t.Errorf("Error was returned when no error was expected: %v", err) | ||
return | ||
} | ||
if !strings.Contains(err.Error(), tc.wantErrMsg) { | ||
t.Errorf("Error %q does not contain expected string %q", err.Error(), tc.wantErrMsg) | ||
return | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
resource "azurerm_managed_disk" "sample" { | ||
encryption_settings { | ||
enabled = false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,19 @@ | ||
resource "aws_security_group_rule" "my-rule" { | ||
type = "ingress" | ||
cidr_blocks = ["0.0.0.0/0"] | ||
type = "ingress" | ||
cidr_blocks = ["0.0.0.0/0"] | ||
} | ||
|
||
resource "aws_alb_listener" "my-alb-listener"{ | ||
port = "80" | ||
protocol = "HTTP" | ||
resource "aws_alb_listener" "my-alb-listener" { | ||
port = "80" | ||
protocol = "HTTP" | ||
} | ||
|
||
resource "aws_db_security_group" "my-group" { | ||
|
||
} | ||
|
||
resource "azurerm_managed_disk" "source" { | ||
encryption_settings { | ||
enabled = false | ||
} | ||
encryption_settings { | ||
enabled = false | ||
} | ||
} |
Oops, something went wrong.