Skip to content

Commit

Permalink
Write location parser for Mutations
Browse files Browse the repository at this point in the history
The location parser parses the location specification language used
for matching Kubernetes object fields.

Example:

```
p := NewParser("spec.containers[name:*].securityContext")
root, err := p.Parse()
...

Closes #915

Signed-off-by: Oren Shomron <shomron@gmail.com>
  • Loading branch information
shomron committed Nov 7, 2020
1 parent 4516141 commit d2fbc8c
Show file tree
Hide file tree
Showing 7 changed files with 940 additions and 0 deletions.
19 changes: 19 additions & 0 deletions pkg/path/parser/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
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
http://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.
*/

// Package parser provides a parser for a path specification language used
// in expressing Kubernetes object paths.
// See specification: https://docs.google.com/document/d/1MdchNFz9guycX__QMGxpJviPaT_MZs8iXaAFqCvoXYQ/edit#heading=h.ryydvhafooho
package parser
61 changes: 61 additions & 0 deletions pkg/path/parser/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
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
http://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.
*/

package parser

type NodeType string

const (
RootNode NodeType = "Root"
ListNode NodeType = "List"
ObjectNode NodeType = "Object"
)

type Node interface {
Type() NodeType
}

type Root struct {
Nodes []Node
}

func (r Root) Type() NodeType {
return RootNode
}

type Object struct {
Value string
}

func (o Object) Type() NodeType {
return ObjectNode
}

type List struct {
KeyField string
KeyValue *string
Glob bool
}

func (l List) Type() NodeType {
return ListNode
}

func (l List) Value() (string, bool) {
if l.KeyValue == nil {
return "", false
}
return *l.KeyValue, true
}
153 changes: 153 additions & 0 deletions pkg/path/parser/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
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
http://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.
*/

package parser

import (
"errors"
"fmt"

"github.com/open-policy-agent/gatekeeper/pkg/path/token"
)

type Parser struct {
input string
scanner *token.Scanner
curToken token.Token
peekToken token.Token
err error
}

func NewParser(input string) *Parser {
s := token.NewScanner(input)
p := &Parser{
input: input,
scanner: s,
}
p.curToken = p.scanner.Next()
p.peekToken = p.scanner.Next()
return p
}

// next advances to the next token in the stream.
func (p *Parser) next() {
p.curToken = p.peekToken
p.peekToken = p.scanner.Next()
}

// expect returns whether the next token matches our expectation,
// and if so advances to that token.
// Otherwise returns false and doesn't advance.
func (p *Parser) expect(t token.Type) bool {
if p.peekToken.Type == t {
p.next()
return true
}
return false
}

// expectPeek returns whether the next token matches our expectation.
// The current token is not advanced either way.
func (p *Parser) expectPeek(t token.Type) bool {
return p.peekToken.Type == t
}

func (p *Parser) Parse() (*Root, error) {
root := &Root{}
loop:
for p.curToken.Type != token.EOF && p.err == nil {
var node Node
switch p.curToken.Type {
case token.IDENT:
node = p.parseObject()
case token.LBRACKET:
node = p.parseList()
default:
p.setError(fmt.Errorf("unexpected token: expected field name or eof, got: %s", p.peekToken.String()))
}

if p.err != nil {
// Encountered parsing error, abort
return nil, p.err
}

if node != nil {
root.Nodes = append(root.Nodes, node)
}

// Advance past separator if needed and ensure no unexpected tokens follow
switch {
case p.expect(token.SEPARATOR):
if p.expectPeek(token.EOF) {
// block trailing separators
p.setError(errors.New("trailing separators are forbidden"))
return nil, p.err
}
// Skip past the separator
p.next()
case p.expect(token.LBRACKET):
// Allowed but don't advance past the bracket
case p.expect(token.EOF):
break loop
default:
p.setError(fmt.Errorf("expected '.' or eof, got: %s", p.peekToken.String()))
return nil, p.err
}
}
return root, nil
}

// parseList tries to parse the current position as List match node, e.g. [key: val]
// returns nil if it cannot be parsed as a List.
func (p *Parser) parseList() Node {
out := &List{}

// keyField is required
if !p.expect(token.IDENT) {
p.setError(fmt.Errorf("expected keyField in listSpec, got: %s", p.peekToken.String()))
return nil
}

out.KeyField = p.curToken.Literal

if !p.expect(token.COLON) {
p.setError(fmt.Errorf("expected ':' following keyField %s, got: %s", out.KeyField, p.peekToken.String()))
return nil
}

switch {
case p.expect(token.GLOB):
out.Glob = true
case p.expect(token.IDENT):
// Optional
val := p.curToken.Literal
out.KeyValue = &val
}

if !p.expect(token.RBRACKET) {
p.setError(fmt.Errorf("expected ']' following listSpec, got: %s", p.peekToken.String()))
return nil
}
return out
}

func (p *Parser) parseObject() Node {
out := &Object{Value: p.curToken.Literal}
return out
}

func (p *Parser) setError(err error) {
p.err = err
}
Loading

0 comments on commit d2fbc8c

Please sign in to comment.