Skip to content

Commit dd0239a

Browse files
authored
feat: Add textproto support (#828)
Support parsing and testing the textual representation of Protocol Buffers (textproto). Signed-off-by: James Alseth <james@jalseth.me>
1 parent 3cc4518 commit dd0239a

File tree

14 files changed

+369
-10
lines changed

14 files changed

+369
-10
lines changed

acceptance.bats

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,14 @@ EOF"
489489
[[ "$output" =~ "built-in error" ]]
490490
}
491491

492+
@test "TextProto policy returns expected results" {
493+
run ./conftest test --proto-file-dirs=examples/textproto/protos -p examples/textproto/policy examples/textproto/
494+
[ "$status" -eq 1 ]
495+
[[ "$output" =~ "2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions" ]]
496+
}
497+
498+
@test "TextProto policy fails when the proto messages could not be resolved" {
499+
run ./conftest test -p examples/textproto/policy examples/textproto/
500+
[ "$status" -eq 1 ]
501+
[[ "$output" =~ "look up message type" ]]
502+
}

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ As of today Conftest supports:
7878
* Jsonnet
7979
* Property files (.properties)
8080
* SPDX
81+
* TextProto (Protocol Buffers)
8182
* TOML
8283
* VCL
8384
* XML

examples/textproto/fail.textproto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# proto-message: conftest.testing.TestMessage
2+
3+
name: "fail"
4+
number: 9000

examples/textproto/pass.textproto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# proto-message: conftest.testing.TestMessage
2+
3+
name: "pass"
4+
number: 9001
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package main
2+
3+
deny[{"msg": msg}] {
4+
input.number <= 9000
5+
msg := sprintf("%s: Power level must be over 9000", [input.name])
6+
}

examples/textproto/protos/test.proto

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
syntax = "proto3";
2+
3+
package conftest.testing;
4+
5+
message TestMessage {
6+
string name = 1;
7+
int32 number = 2;
8+
bool truthy = 3;
9+
}

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ require (
88
github.com/CycloneDX/cyclonedx-go v0.4.0
99
github.com/KeisukeYamashita/go-vcl v0.4.0
1010
github.com/basgys/goxml2json v1.1.0
11+
github.com/bufbuild/protocompile v0.5.1
1112
github.com/cpuguy83/dockercfg v0.3.1
1213
github.com/ghodss/yaml v1.0.0
1314
github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665
1415
github.com/go-ini/ini v1.67.0
16+
github.com/google/go-cmp v0.5.9
1517
github.com/google/go-jsonnet v0.18.0
1618
github.com/hashicorp/go-getter v1.7.1
1719
github.com/hashicorp/hcl v1.0.0
@@ -29,6 +31,8 @@ require (
2931
github.com/spf13/viper v1.10.0
3032
github.com/subosito/gotenv v1.2.0
3133
github.com/tmccombs/hcl2json v0.3.1
34+
golang.org/x/exp v0.0.0-20230519143937-03e91628a987
35+
google.golang.org/protobuf v1.30.0
3236
muzzammil.xyz/jsonc v1.0.0
3337
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3
3438
oras.land/oras-go/v2 v2.0.2
@@ -58,7 +62,6 @@ require (
5862
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
5963
github.com/golang/protobuf v1.5.3 // indirect
6064
github.com/google/flatbuffers v22.9.29+incompatible // indirect
61-
github.com/google/go-cmp v0.5.9 // indirect
6265
github.com/google/s2a-go v0.1.3 // indirect
6366
github.com/google/uuid v1.3.0 // indirect
6467
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
@@ -85,7 +88,6 @@ require (
8588
github.com/spf13/cast v1.4.1 // indirect
8689
github.com/spf13/jwalterweatherman v1.1.0 // indirect
8790
github.com/spf13/pflag v1.0.5 // indirect
88-
github.com/stretchr/testify v1.8.2 // indirect
8991
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
9092
github.com/ulikunitz/xz v0.5.11 // indirect
9193
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
@@ -104,7 +106,6 @@ require (
104106
google.golang.org/appengine v1.6.7 // indirect
105107
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
106108
google.golang.org/grpc v1.55.0 // indirect
107-
google.golang.org/protobuf v1.30.0 // indirect
108109
gopkg.in/ini.v1 v1.66.2 // indirect
109110
gopkg.in/yaml.v2 v2.4.0 // indirect
110111
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkN
231231
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
232232
github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
233233
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
234+
github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg=
235+
github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40=
234236
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
235237
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
236238
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -615,7 +617,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
615617
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
616618
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
617619
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
618-
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
619620
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
620621
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
621622
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
@@ -692,6 +693,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
692693
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
693694
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
694695
golang.org/x/exp v0.0.0-20210126221216-84987778548c/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
696+
golang.org/x/exp v0.0.0-20230519143937-03e91628a987 h1:3xJIFvzUFbu4ls0BTBYcgbCGhA63eAOEMxIHugyXJqA=
697+
golang.org/x/exp v0.0.0-20230519143937-03e91628a987/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
695698
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
696699
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
697700
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

internal/commands/test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func NewTestCommand(ctx context.Context) *cobra.Command {
9696
"output",
9797
"parser",
9898
"policy",
99+
"proto-file-dirs",
99100
"capabilities",
100101
"trace",
101102
"strict",
@@ -181,5 +182,7 @@ func NewTestCommand(ctx context.Context) *cobra.Command {
181182
cmd.Flags().StringSliceP("namespace", "n", []string{"main"}, "Test policies in a specific namespace")
182183
cmd.Flags().StringSliceP("data", "d", []string{}, "A list of paths from which data for the rego policies will be recursively loaded")
183184

185+
cmd.Flags().StringSlice("proto-file-dirs", []string{}, "A list of directories containing Protocol Buffer definitions")
186+
184187
return &cmd
185188
}

internal/commands/verify.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,18 @@ func NewVerifyCommand(ctx context.Context) *cobra.Command {
6868
Short: "Verify Rego unit tests",
6969
Long: verifyDesc,
7070
PreRunE: func(cmd *cobra.Command, args []string) error {
71-
flagNames := []string{"data", "no-color", "output", "policy", "trace", "report", "quiet", "junit-hide-message", "capabilities"}
71+
flagNames := []string{
72+
"data",
73+
"no-color",
74+
"output",
75+
"policy",
76+
"trace",
77+
"report",
78+
"quiet",
79+
"junit-hide-message",
80+
"capabilities",
81+
"proto-file-dirs",
82+
}
7283
for _, name := range flagNames {
7384
if err := viper.BindPFlag(name, cmd.Flags().Lookup(name)); err != nil {
7485
return fmt.Errorf("bind flag: %w", err)
@@ -133,5 +144,7 @@ func NewVerifyCommand(ctx context.Context) *cobra.Command {
133144
cmd.Flags().StringSliceP("data", "d", []string{}, "A list of paths from which data for the rego policies will be recursively loaded")
134145
cmd.Flags().StringSliceP("policy", "p", []string{"policy"}, "Path to the Rego policy files directory")
135146

147+
cmd.Flags().StringSlice("proto-file-dirs", []string{}, "A list of directories containing Protocol Buffer definitions")
148+
136149
return &cmd
137150
}

parser/parser.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import (
44
"bufio"
55
"fmt"
66
"io"
7+
"io/fs"
78
"os"
89
"path/filepath"
910
"sort"
1011
"strings"
1112

1213
"github.com/open-policy-agent/conftest/parser/cyclonedx"
14+
"github.com/spf13/viper"
15+
"golang.org/x/exp/slices"
1316

1417
"github.com/open-policy-agent/conftest/parser/cue"
1518
"github.com/open-policy-agent/conftest/parser/docker"
@@ -25,6 +28,7 @@ import (
2528
"github.com/open-policy-agent/conftest/parser/jsonnet"
2629
"github.com/open-policy-agent/conftest/parser/properties"
2730
"github.com/open-policy-agent/conftest/parser/spdx"
31+
"github.com/open-policy-agent/conftest/parser/textproto"
2832
"github.com/open-policy-agent/conftest/parser/toml"
2933
"github.com/open-policy-agent/conftest/parser/vcl"
3034
"github.com/open-policy-agent/conftest/parser/xml"
@@ -48,6 +52,7 @@ const (
4852
JSONNET = "jsonnet"
4953
PROPERTIES = "properties"
5054
SPDX = "spdx"
55+
TEXTPROTO = "textproto"
5156
TOML = "toml"
5257
VCL = "vcl"
5358
XML = "xml"
@@ -102,11 +107,46 @@ func New(parser string) (Parser, error) {
102107
return &cyclonedx.Parser{}, nil
103108
case DOTENV:
104109
return &dotenv.Parser{}, nil
110+
case TEXTPROTO:
111+
parser := &textproto.Parser{}
112+
if dirs := viper.GetStringSlice("proto-file-dirs"); len(dirs) > 0 {
113+
files, err := findFilesWithExt(dirs, ".proto")
114+
if err != nil {
115+
return nil, fmt.Errorf("find proto files: %w", err)
116+
}
117+
if err := parser.LoadProtoFiles(files); err != nil {
118+
return nil, fmt.Errorf("load protos: %w", err)
119+
}
120+
}
121+
122+
return parser, nil
105123
default:
106124
return nil, fmt.Errorf("unknown parser: %v", parser)
107125
}
108126
}
109127

128+
func findFilesWithExt(dirs []string, ext string) ([]string, error) {
129+
var files []string
130+
for _, dir := range dirs {
131+
err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
132+
if err != nil {
133+
return err
134+
}
135+
if info.IsDir() {
136+
return nil
137+
}
138+
if strings.HasSuffix(info.Name(), ext) {
139+
files = append(files, path)
140+
}
141+
return nil
142+
})
143+
if err != nil {
144+
return nil, fmt.Errorf("walk dir %q: %w", dir, err)
145+
}
146+
}
147+
return files, nil
148+
}
149+
110150
// NewFromPath returns a file parser based on the file type
111151
// that exists at the given path.
112152
func NewFromPath(path string) (Parser, error) {
@@ -152,6 +192,10 @@ func NewFromPath(path string) (Parser, error) {
152192
return New(DOTENV)
153193
}
154194

195+
if slices.Contains(textproto.TextProtoFileExtensions, fileExtension) {
196+
return New(TEXTPROTO)
197+
}
198+
155199
parser, err := New(fileExtension)
156200
if err != nil {
157201
return nil, fmt.Errorf("new: %w", err)
@@ -175,6 +219,7 @@ func Parsers() []string {
175219
JSONNET,
176220
PROPERTIES,
177221
SPDX,
222+
TEXTPROTO,
178223
TOML,
179224
VCL,
180225
XML,
@@ -188,11 +233,8 @@ func Parsers() []string {
188233
// FileSupported returns true if the file at the given path is
189234
// a file that can be parsed.
190235
func FileSupported(path string) bool {
191-
if _, err := NewFromPath(path); err != nil {
192-
return false
193-
}
194-
195-
return true
236+
_, err := NewFromPath(path)
237+
return err == nil
196238
}
197239

198240
// ParseConfigurations parses and returns the configurations from the given

parser/parser_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/open-policy-agent/conftest/parser/ignore"
1111
"github.com/open-policy-agent/conftest/parser/json"
1212
"github.com/open-policy-agent/conftest/parser/jsonc"
13+
"github.com/open-policy-agent/conftest/parser/textproto"
1314
"github.com/open-policy-agent/conftest/parser/yaml"
1415
)
1516

@@ -119,6 +120,11 @@ func TestNewFromPath(t *testing.T) {
119120
&dotenv.Parser{},
120121
false,
121122
},
123+
{
124+
"foo.textproto",
125+
&textproto.Parser{},
126+
false,
127+
},
122128
}
123129

124130
for _, testCase := range testCases {

0 commit comments

Comments
 (0)