diff --git a/policy/compiler.go b/policy/compiler.go new file mode 100644 index 00000000..5d9c90d7 --- /dev/null +++ b/policy/compiler.go @@ -0,0 +1,170 @@ +package policy + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common" + "github.com/google/cel-go/common/ast" + "github.com/google/cel-go/common/operators" + "github.com/google/cel-go/common/types" +) + +type compiler struct { + env *cel.Env + info *ast.SourceInfo + src *Source +} + +type compiledRule struct { + variables []*compiledVariable + matches []*compiledMatch +} + +type compiledVariable struct { + name string + expr *cel.Ast +} + +type compiledMatch struct { + cond *cel.Ast + output *cel.Ast + nestedRule *compiledRule +} + +func compile(env *cel.Env, p *policy) (*cel.Ast, *cel.Issues) { + c := &compiler{ + env: env, + info: p.info, + src: p.source, + } + errs := common.NewErrors(c.src) + iss := cel.NewIssuesWithSourceInfo(errs, c.info) + rule, ruleIss := c.compileRule(p.rule, c.env, iss) + iss = iss.Append(ruleIss) + if iss.Err() != nil { + return nil, iss + } + ruleRoot, _ := env.Compile("true") + opt := cel.NewStaticOptimizer(&ruleComposer{rule: rule}) + ruleExprAST, iss := opt.Optimize(env, ruleRoot) + return ruleExprAST, iss.Append(iss) +} + +func (c *compiler) compileRule(r *rule, ruleEnv *cel.Env, iss *cel.Issues) (*compiledRule, *cel.Issues) { + var err error + compiledVars := make([]*compiledVariable, len(r.variables)) + for i, v := range r.variables { + exprSrc := c.relSource(v.expression) + varAST, exprIss := ruleEnv.CompileSource(exprSrc) + if exprIss.Err() == nil { + ruleEnv, err = ruleEnv.Extend(cel.Variable(v.name.value, varAST.OutputType())) + if err != nil { + iss.ReportErrorAtID(v.expression.id, "invalid variable declaration") + } + compiledVars[i] = &compiledVariable{ + name: v.name.value, + expr: varAST, + } + } + iss = iss.Append(exprIss) + } + compiledMatches := []*compiledMatch{} + for _, m := range r.matches { + condSrc := c.relSource(m.condition) + condAST, condIss := ruleEnv.CompileSource(condSrc) + iss = iss.Append(condIss) + if m.output != nil && m.rule != nil { + iss.ReportErrorAtID(m.condition.id, "either output or rule may be set but not both") + continue + } + if m.output != nil { + outSrc := c.relSource(*m.output) + outAST, outIss := ruleEnv.CompileSource(outSrc) + iss = iss.Append(outIss) + compiledMatches = append(compiledMatches, &compiledMatch{ + cond: condAST, + output: outAST, + }) + continue + } + if m.rule != nil { + nestedRule, ruleIss := c.compileRule(m.rule, ruleEnv, iss) + iss = iss.Append(ruleIss) + compiledMatches = append(compiledMatches, &compiledMatch{ + cond: condAST, + nestedRule: nestedRule, + }) + } + } + return &compiledRule{ + variables: compiledVars, + matches: compiledMatches, + }, iss +} + +func (c *compiler) relSource(pstr policyString) *RelativeSource { + line := 0 + col := 1 + if offset, found := c.info.GetOffsetRange(pstr.id); found { + if loc, found := c.src.OffsetLocation(offset.Start); found { + line = loc.Line() + col = loc.Column() + } + } + return c.src.Relative(pstr.value, line, col) +} + +type ruleComposer struct { + rule *compiledRule +} + +func (opt *ruleComposer) Optimize(ctx *cel.OptimizerContext, a *ast.AST) *ast.AST { + ruleExpr := optimizeRule(ctx, opt.rule) + ctx.UpdateExpr(a.Expr(), ruleExpr) + return ctx.NewAST(ruleExpr) +} + +func optimizeRule(ctx *cel.OptimizerContext, r *compiledRule) ast.Expr { + matchExpr := ctx.NewCall("optional.none") + matches := r.matches + for i := len(matches) - 1; i >= 0; i-- { + m := matches[i] + cond := ctx.CopyASTAndMetadata(m.cond.NativeRep()) + triviallyTrue := cond.Kind() == ast.LiteralKind && cond.AsLiteral() == types.True + if m.output != nil { + out := ctx.CopyASTAndMetadata(m.output.NativeRep()) + if triviallyTrue { + matchExpr = out + continue + } + matchExpr = ctx.NewCall( + operators.Conditional, + cond, + ctx.NewCall("optional.of", out), + matchExpr) + continue + } + nestedRule := optimizeRule(ctx, m.nestedRule) + if triviallyTrue { + matchExpr = nestedRule + continue + } + matchExpr = ctx.NewCall( + operators.Conditional, + cond, + nestedRule, + matchExpr) + } + + vars := r.variables + for i := len(vars) - 1; i >= 0; i-- { + v := vars[i] + varAST := ctx.CopyASTAndMetadata(v.expr.NativeRep()) + // Build up the bindings in reverse order, starting from root, all the way up to the outermost + // binding: + // currExpr = cel.bind(outerVar, outerExpr, currExpr) + inlined, bindMacro := ctx.NewBindMacro(matchExpr.ID(), v.name, varAST, matchExpr) + ctx.SetMacroCall(inlined.ID(), bindMacro) + matchExpr = inlined + } + return matchExpr +} diff --git a/policy/compiler_test.go b/policy/compiler_test.go new file mode 100644 index 00000000..63498e73 --- /dev/null +++ b/policy/compiler_test.go @@ -0,0 +1,32 @@ +package policy + +import ( + "testing" + + "github.com/google/cel-go/cel" +) + +func TestCompile(t *testing.T) { + srcFile := readPolicy(t, "testdata/required_labels.yaml") + p, iss := parse(srcFile) + if iss.Err() != nil { + t.Fatalf("parse() failed: %v", iss.Err()) + } + if p.name.value != "required_labels" { + t.Errorf("policy name is %v, wanted 'required_labels'", p.name) + } + env, err := cel.NewEnv( + cel.OptionalTypes(), + cel.EnableMacroCallTracking(), + cel.ExtendedValidations(), + cel.Variable("rule.labels", cel.MapType(cel.StringType, cel.StringType)), + cel.Variable("resource.labels", cel.MapType(cel.StringType, cel.StringType)), + ) + if err != nil { + t.Fatalf("cel.NewEnv() failed: %v", err) + } + _, iss = compile(env, p) + if iss.Err() != nil { + t.Errorf("compile() failed: %v", iss.Err()) + } +} diff --git a/policy/go.mod b/policy/go.mod new file mode 100644 index 00000000..e1f47612 --- /dev/null +++ b/policy/go.mod @@ -0,0 +1,20 @@ +module github.com/google/cel-go/policy + +go 1.20 + +require ( + github.com/google/cel-go v0.20.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) + +replace github.com/google/cel-go => ../. diff --git a/policy/go.sum b/policy/go.sum new file mode 100644 index 00000000..1f23773b --- /dev/null +++ b/policy/go.sum @@ -0,0 +1,28 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 h1:eSaPbMR4T7WfH9FvABk36NBMacoTUKdWCvV0dx+KfOg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/policy/parser.go b/policy/parser.go new file mode 100644 index 00000000..9b781b7b --- /dev/null +++ b/policy/parser.go @@ -0,0 +1,361 @@ +package policy + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common" + "github.com/google/cel-go/common/ast" +) + +type semanticType int + +const ( + Unspecified semanticType = iota + FirstMatch + LastMatch + LogicalAnd + LogicalOr + Accumulate +) + +type policy struct { + name policyString + rule *rule + semantic semanticType + info *ast.SourceInfo + source *Source +} + +type rule struct { + id *policyString + description *policyString + variables []*variable + matches []*match +} + +type variable struct { + name policyString + expression policyString +} + +type match struct { + condition policyString + output *policyString + rule *rule +} + +type policyString struct { + id int64 + value string +} + +func parse(src *Source) (*policy, *cel.Issues) { + info := ast.NewSourceInfo(src) + errs := common.NewErrors(src) + iss := cel.NewIssuesWithSourceInfo(errs, info) + p := newParser(info, src, iss) + policy := p.parseYaml(src) + if iss.Err() != nil { + return nil, iss + } + policy.source = src + policy.info = p.info + return policy, nil +} + +func (p *parser) parseYaml(src *Source) *policy { + // Parse yaml representation from the source to an object model. + var docNode yaml.Node + err := sourceToYaml(src, &docNode) + if err != nil { + p.iss.ReportErrorAtID(0, err.Error()) + return nil + } + p.collectMetadata(1, &docNode) + return p.parse(docNode.Content[0]) +} + +func sourceToYaml(src *Source, docNode *yaml.Node) error { + err := yaml.Unmarshal([]byte(src.Content()), docNode) + if err != nil { + return err + } + if docNode.Kind != yaml.DocumentNode { + return fmt.Errorf("got yaml node of kind %v, wanted mapping node", docNode.Kind) + } + return nil +} + +func newParser(info *ast.SourceInfo, src *Source, iss *cel.Issues) *parser { + return &parser{ + info: info, + src: src, + iss: iss, + } +} + +type parser struct { + id int64 + info *ast.SourceInfo + src *Source + iss *cel.Issues +} + +func (p *parser) nextID() int64 { + p.id++ + return p.id +} + +func (p *parser) parse(node *yaml.Node) *policy { + pol := &policy{} + id := p.nextID() + p.collectMetadata(id, node) + if p.assertYamlType(id, node, yamlMap) == nil { + return pol + } + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + id := p.nextID() + p.collectMetadata(id, key) + fieldName := key.Value + val := node.Content[i+1] + if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle { + val.Line++ + val.Column = key.Column + 2 + } + switch fieldName { + case "name": + pol.name = p.parseString(val) + case "rule": + rule := p.parseRule(val) + pol.rule = rule + default: + p.reportErrorAtID(id, "unexpected field name: %s", fieldName) + } + } + return pol +} + +func (p *parser) parseRule(node *yaml.Node) *rule { + r := &rule{} + id := p.nextID() + p.collectMetadata(id, node) + if p.assertYamlType(id, node, yamlMap) == nil { + return r + } + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + id := p.nextID() + p.collectMetadata(id, key) + fieldName := key.Value + val := node.Content[i+1] + if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle { + val.Line++ + val.Column = key.Column + 2 + } + switch fieldName { + case "id": + ruleID := p.parseString(val) + r.id = &ruleID + case "description": + desc := p.parseString(val) + r.description = &desc + case "variables": + r.variables = p.parseVariables(val) + case "match": + r.matches = p.parseMatches(val) + default: + p.reportErrorAtID(id, "unexpected field name: %s", fieldName) + } + } + return r +} + +func (p *parser) parseVariables(node *yaml.Node) []*variable { + vars := []*variable{} + id := p.nextID() + p.collectMetadata(id, node) + if p.assertYamlType(id, node, yamlList) == nil { + return vars + } + for _, val := range node.Content { + vars = append(vars, p.parseVariable(val)) + } + return vars +} + +func (p *parser) parseVariable(node *yaml.Node) *variable { + v := &variable{} + id := p.nextID() + p.collectMetadata(id, node) + if p.assertYamlType(id, node, yamlMap) == nil { + return v + } + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + id := p.nextID() + p.collectMetadata(id, key) + fieldName := key.Value + val := node.Content[i+1] + if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle { + val.Line++ + val.Column = key.Column + 2 + } + switch fieldName { + case "name": + v.name = p.parseString(val) + case "expression": + v.expression = p.parseString(val) + default: + p.reportErrorAtID(id, "unexpected field name: %s", fieldName) + } + } + return v +} + +func (p *parser) parseMatches(node *yaml.Node) []*match { + matches := []*match{} + id := p.nextID() + p.collectMetadata(id, node) + if p.assertYamlType(id, node, yamlList) == nil { + return matches + } + for _, val := range node.Content { + matches = append(matches, p.parseMatch(val)) + } + return matches +} + +func (p *parser) parseMatch(node *yaml.Node) *match { + m := &match{} + id := p.nextID() + p.collectMetadata(id, node) + if p.assertYamlType(id, node, yamlMap) == nil { + return m + } + m.condition = policyString{id: id, value: "true"} + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + id := p.nextID() + p.collectMetadata(id, key) + fieldName := key.Value + val := node.Content[i+1] + if val.Style == yaml.FoldedStyle || val.Style == yaml.LiteralStyle { + val.Line++ + val.Column = key.Column + 2 + } + switch fieldName { + case "condition": + m.condition = p.parseString(val) + case "output": + outputExpr := p.parseString(val) + m.output = &outputExpr + case "rule": + m.rule = p.parseRule(val) + default: + p.reportErrorAtID(id, "unexpected field name: %s", fieldName) + } + } + return m +} + +func (p *parser) parseString(node *yaml.Node) policyString { + id := p.nextID() + p.collectMetadata(id, node) + nodeType := p.assertYamlType(id, node, yamlString, yamlText) + if nodeType == nil { + return policyString{id: id, value: "*error*"} + } + if *nodeType == yamlText { + return policyString{id: id, value: node.Value} + } + if node.Style == yaml.FoldedStyle || node.Style == yaml.LiteralStyle { + col := node.Column + line := node.Line + txt, found := p.src.Snippet(line) + indent := "" + for len(indent) < col-1 { + indent += " " + } + var raw strings.Builder + for found && strings.HasPrefix(txt, indent) { + line++ + raw.WriteString(txt) + txt, found = p.src.Snippet(line) + if found && strings.HasPrefix(txt, indent) { + raw.WriteString("\n") + } + } + offset := p.info.OffsetRanges()[p.id] + offsetStart := offset.Start - (int32(node.Column) - 1) + p.info.SetOffsetRange(p.id, ast.OffsetRange{Start: offsetStart, Stop: offsetStart}) + return policyString{id: id, value: raw.String()} + } + return policyString{id: id, value: node.Value} +} + +func (p *parser) collectMetadata(id int64, node *yaml.Node) { + line := node.Line + col := int32(node.Column) + switch node.Style { + case yaml.DoubleQuotedStyle, yaml.SingleQuotedStyle: + col++ + } + offsetStart := int32(0) + if line > 1 { + offsetStart = p.info.LineOffsets()[line-2] + } + p.info.SetOffsetRange(id, ast.OffsetRange{Start: offsetStart + col - 1, Stop: offsetStart + col - 1}) +} + +func (p *parser) assertYamlType(id int64, node *yaml.Node, nodeTypes ...yamlNodeType) *yamlNodeType { + nt, found := yamlTypes[node.LongTag()] + if !found { + p.reportErrorAtID(id, "unsupported map key type: %v", node.LongTag()) + return nil + } + for _, nodeType := range nodeTypes { + if nt == nodeType { + return &nt + } + } + p.reportErrorAtID(id, "got yaml node type %v, wanted type(s) %v", node.LongTag(), nodeTypes) + return nil +} + +func (p *parser) reportErrorAtID(id int64, format string, args ...interface{}) { + p.iss.ReportErrorAtID(id, format, args...) +} + +type yamlNodeType int + +const ( + yamlText yamlNodeType = iota + 1 + yamlBool + yamlNull + yamlString + yamlInt + yamlDouble + yamlList + yamlMap + yamlTimestamp +) + +var ( + // yamlTypes map of the long tag names supported by the Go YAML v3 library. + yamlTypes = map[string]yamlNodeType{ + "!txt": yamlText, + "tag:yaml.org,2002:bool": yamlBool, + "tag:yaml.org,2002:null": yamlNull, + "tag:yaml.org,2002:str": yamlString, + "tag:yaml.org,2002:int": yamlInt, + "tag:yaml.org,2002:float": yamlDouble, + "tag:yaml.org,2002:seq": yamlList, + "tag:yaml.org,2002:map": yamlMap, + "tag:yaml.org,2002:timestamp": yamlTimestamp, + } +) diff --git a/policy/parser_test.go b/policy/parser_test.go new file mode 100644 index 00000000..65932aab --- /dev/null +++ b/policy/parser_test.go @@ -0,0 +1,26 @@ +package policy + +import ( + "os" + "testing" +) + +func TestParse(t *testing.T) { + srcFile := readPolicy(t, "testdata/required_labels.yaml") + p, iss := parse(srcFile) + if iss.Err() != nil { + t.Fatalf("parse() failed: %v", iss.Err()) + } + if p.name.value != "required_labels" { + t.Errorf("policy name is %v, wanted 'required_labels'", p.name) + } +} + +func readPolicy(t *testing.T, fileName string) *Source { + t.Helper() + tmplBytes, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("os.ReadFile(%s) failed: %v", fileName, err) + } + return ByteSource(tmplBytes, fileName) +} diff --git a/policy/source.go b/policy/source.go new file mode 100644 index 00000000..5e0e7506 --- /dev/null +++ b/policy/source.go @@ -0,0 +1,85 @@ +package policy + +// Copyright 2020 Google LLC +// +// 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 +// +// https://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. + +import ( + "github.com/google/cel-go/common" +) + +// ByteSource converts a byte sequence and location description to a model.Source. +func ByteSource(contents []byte, location string) *Source { + return StringSource(string(contents), location) +} + +// StringSource converts a string and location description to a model.Source. +func StringSource(contents, location string) *Source { + return &Source{ + Source: common.NewStringSource(contents, location), + } +} + +// Source represents the contents of a single source file. +type Source struct { + common.Source +} + +// Relative produces a RelativeSource object for the content provided at the absolute location +// within the parent Source as indicated by the line and column. +func (src *Source) Relative(content string, line, col int) *RelativeSource { + return &RelativeSource{ + Source: src.Source, + localSrc: common.NewStringSource(content, src.Description()), + absLoc: common.NewLocation(line, col), + } +} + +// RelativeSource represents an embedded source element within a larger source. +type RelativeSource struct { + common.Source + localSrc common.Source + absLoc common.Location +} + +// AbsoluteLocation returns the location within the parent Source where the RelativeSource starts. +func (rel *RelativeSource) AbsoluteLocation() common.Location { + return rel.absLoc +} + +// Content returns the embedded source snippet. +func (rel *RelativeSource) Content() string { + return rel.localSrc.Content() +} + +// OffsetLocation returns the absolute location given the relative offset, if found. +func (rel *RelativeSource) OffsetLocation(offset int32) (common.Location, bool) { + absOffset, found := rel.Source.LocationOffset(rel.absLoc) + if !found { + return common.NoLocation, false + } + return rel.Source.OffsetLocation(absOffset + offset) +} + +// NewLocation creates an absolute common.Location based on a local line, column +// position from a relative source. +func (rel *RelativeSource) NewLocation(line, col int) common.Location { + localLoc := common.NewLocation(line, col) + relOffset, found := rel.localSrc.LocationOffset(localLoc) + if !found { + return common.NoLocation + } + offset, _ := rel.Source.LocationOffset(rel.absLoc) + absLoc, _ := rel.Source.OffsetLocation(offset + relOffset) + return absLoc +} diff --git a/policy/testdata/required_labels.yaml b/policy/testdata/required_labels.yaml new file mode 100644 index 00000000..5497030a --- /dev/null +++ b/policy/testdata/required_labels.yaml @@ -0,0 +1,32 @@ +# Copyright 2019 Google LLC +# +# 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 +# +# https://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. + +name: "required_labels" +rule: + variables: + - name: want + expression: rule.labels + - name: missing + expression: want.filter(l, !(l in resource.labels)) + - name: invalid + expression: > + resource.labels.filter(l, + l in want && want[l] != resource.labels[l]) + match: + - condition: missing.size() > 0 + output: | + "missing one or more required labels" + - condition: invalid.size() > 0 + output: | + "invalid values provided on one or more labels" \ No newline at end of file