mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
add ability for opa inspect to inspect a single file outside of any bundle (#6873)
add ability for opa inspect to inspect a single file outside of any bundle Signed-off-by: Tyler Schade <tyler.schade@solo.io>
This commit is contained in:
+17
-8
@@ -57,10 +57,10 @@ func init() {
|
||||
|
||||
var inspectCommand = &cobra.Command{
|
||||
Use: "inspect <path> [<path> [...]]",
|
||||
Short: "Inspect OPA bundle(s)",
|
||||
Long: `Inspect OPA bundle(s).
|
||||
Short: "Inspect OPA bundle(s) or Rego files.",
|
||||
Long: `Inspect OPA bundle(s) or Rego files.
|
||||
|
||||
The 'inspect' command provides a summary of the contents in OPA bundle(s). Bundles are
|
||||
The 'inspect' command provides a summary of the contents in OPA bundle(s) or a single Rego file. Bundles are
|
||||
gzipped tarballs containing policies and data. The 'inspect' command reads bundle(s) and lists
|
||||
the following:
|
||||
|
||||
@@ -77,8 +77,10 @@ Example:
|
||||
bundle.tar.gz
|
||||
$ opa inspect bundle.tar.gz
|
||||
|
||||
You can provide exactly one OPA bundle or path to the 'inspect' command on the command-line. If you provide a path
|
||||
referring to a directory, the 'inspect' command will load that path as a bundle and summarize its structure and contents.
|
||||
You can provide exactly one OPA bundle, path to a bundle directory, or direct path to a Rego file to the 'inspect' command
|
||||
on the command-line. If you provide a path referring to a directory, the 'inspect' command will load that path as a bundle
|
||||
and summarize its structure and contents. If you provide a path referring to a Rego file, the 'inspect' command will load
|
||||
that file and summarize its structure and contents.
|
||||
`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := validateInspectParams(¶ms, args); err != nil {
|
||||
@@ -111,8 +113,7 @@ func doInspect(params inspectCommandParams, path string, out io.Writer) error {
|
||||
return pr.JSON(out, info)
|
||||
|
||||
default:
|
||||
if info.Manifest.Revision != "" || len(*info.Manifest.Roots) != 0 || len(info.Manifest.Metadata) != 0 ||
|
||||
info.Manifest.RegoVersion != nil {
|
||||
if hasManifest(info) {
|
||||
if err := populateManifest(out, info.Manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -134,6 +135,14 @@ func doInspect(params inspectCommandParams, path string, out io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
func hasManifest(info *ib.Info) bool {
|
||||
if info.Manifest == nil {
|
||||
return false
|
||||
}
|
||||
return info.Manifest.Revision != "" || len(*info.Manifest.Roots) != 0 || len(info.Manifest.Metadata) != 0 ||
|
||||
info.Manifest.RegoVersion != nil
|
||||
}
|
||||
|
||||
func validateInspectParams(p *inspectCommandParams, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("specify exactly one OPA bundle or path")
|
||||
@@ -146,7 +155,7 @@ func validateInspectParams(p *inspectCommandParams, args []string) error {
|
||||
return fmt.Errorf("invalid output format for inspect command")
|
||||
}
|
||||
|
||||
func populateManifest(out io.Writer, m bundle.Manifest) error {
|
||||
func populateManifest(out io.Writer, m *bundle.Manifest) error {
|
||||
t := generateTableWithKeys(out, "field", "value")
|
||||
var lines [][]string
|
||||
|
||||
|
||||
+237
-1
@@ -14,7 +14,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
|
||||
"github.com/open-policy-agent/opa/util/test"
|
||||
@@ -1521,3 +1520,240 @@ p {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoInspectSingleFileWithAnnotations(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/a/xxxxxxxxxxxxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyy/foo.rego": `# METADATA
|
||||
# title: pkg-title
|
||||
# description: pkg-descr
|
||||
# organizations:
|
||||
# - pkg-org
|
||||
# related_resources:
|
||||
# - https://pkg
|
||||
# - ref: https://pkg
|
||||
# description: rr-pkg-note
|
||||
# authors:
|
||||
# - pkg-author
|
||||
# schemas:
|
||||
# - input: {"type": "boolean"}
|
||||
# custom:
|
||||
# pkg: pkg-custom
|
||||
package test
|
||||
|
||||
# METADATA
|
||||
# scope: document
|
||||
# title: doc-title
|
||||
# description: doc-descr
|
||||
# organizations:
|
||||
# - doc-org
|
||||
# related_resources:
|
||||
# - https://doc
|
||||
# - ref: https://doc
|
||||
# description: rr-doc-note
|
||||
# authors:
|
||||
# - doc-author
|
||||
# schemas:
|
||||
# - input: {"type": "integer"}
|
||||
# custom:
|
||||
# doc: doc-custom
|
||||
|
||||
# METADATA
|
||||
# title: rule-title
|
||||
# description: rule-title
|
||||
# organizations:
|
||||
# - rule-org
|
||||
# related_resources:
|
||||
# - https://rule
|
||||
# - ref: https://rule
|
||||
# description: rr-rule-note
|
||||
# authors:
|
||||
# - rule-author
|
||||
# schemas:
|
||||
# - input: {"type": "string"}
|
||||
# custom:
|
||||
# rule: rule-custom
|
||||
p = 1`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
fileName := fmt.Sprintf("%s/a/xxxxxxxxxxxxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyy/foo.rego", rootDir)
|
||||
ps := newInspectCommandParams()
|
||||
ps.listAnnotations = true
|
||||
var out bytes.Buffer
|
||||
err := doInspect(ps, fileName, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
shortFileName := truncateFileName(fileName)
|
||||
output := strings.TrimSpace(out.String())
|
||||
expected := strings.TrimSpace(fmt.Sprintf(`
|
||||
NAMESPACES:
|
||||
+-----------+----------------------------------------------------+
|
||||
| NAMESPACE | FILE |
|
||||
+-----------+----------------------------------------------------+
|
||||
| data.test | %[1]s |
|
||||
+-----------+----------------------------------------------------+
|
||||
ANNOTATIONS:
|
||||
pkg-title
|
||||
=========
|
||||
|
||||
pkg-descr
|
||||
|
||||
Package: test
|
||||
Location: %[2]s:16
|
||||
Scope: package
|
||||
|
||||
Organizations:
|
||||
pkg-org
|
||||
|
||||
Authors:
|
||||
pkg-author
|
||||
|
||||
Schemas:
|
||||
input: {"type":"boolean"}
|
||||
|
||||
Related Resources:
|
||||
https://pkg
|
||||
https://pkg rr-pkg-note
|
||||
|
||||
Custom:
|
||||
pkg: "pkg-custom"
|
||||
|
||||
doc-title
|
||||
=========
|
||||
|
||||
doc-descr
|
||||
|
||||
Package: test
|
||||
Rule: p
|
||||
Location: %[2]s:50
|
||||
Scope: document
|
||||
|
||||
Organizations:
|
||||
doc-org
|
||||
|
||||
Authors:
|
||||
doc-author
|
||||
|
||||
Schemas:
|
||||
input: {"type":"integer"}
|
||||
|
||||
Related Resources:
|
||||
https://doc
|
||||
https://doc rr-doc-note
|
||||
|
||||
Custom:
|
||||
doc: "doc-custom"
|
||||
|
||||
rule-title
|
||||
==========
|
||||
|
||||
rule-title
|
||||
|
||||
Package: test
|
||||
Rule: p
|
||||
Location: %[2]s:50
|
||||
Scope: rule
|
||||
|
||||
Organizations:
|
||||
rule-org
|
||||
|
||||
Authors:
|
||||
rule-author
|
||||
|
||||
Schemas:
|
||||
input: {"type":"string"}
|
||||
|
||||
Related Resources:
|
||||
https://rule
|
||||
https://rule rr-rule-note
|
||||
|
||||
Custom:
|
||||
rule: "rule-custom"`, shortFileName, fileName))
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("Unexpected output. Expected:\n\n%q\n\nGot:\n\n%q", expected, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoInspectSingleFile(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/a/xxxxxxxxxxxxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyy/foo.rego": `# METADATA
|
||||
# title: pkg-title
|
||||
# description: pkg-descr
|
||||
# organizations:
|
||||
# - pkg-org
|
||||
# related_resources:
|
||||
# - https://pkg
|
||||
# - ref: https://pkg
|
||||
# description: rr-pkg-note
|
||||
# authors:
|
||||
# - pkg-author
|
||||
# schemas:
|
||||
# - input: {"type": "boolean"}
|
||||
# custom:
|
||||
# pkg: pkg-custom
|
||||
package test
|
||||
|
||||
# METADATA
|
||||
# scope: document
|
||||
# title: doc-title
|
||||
# description: doc-descr
|
||||
# organizations:
|
||||
# - doc-org
|
||||
# related_resources:
|
||||
# - https://doc
|
||||
# - ref: https://doc
|
||||
# description: rr-doc-note
|
||||
# authors:
|
||||
# - doc-author
|
||||
# schemas:
|
||||
# - input: {"type": "integer"}
|
||||
# custom:
|
||||
# doc: doc-custom
|
||||
|
||||
# METADATA
|
||||
# title: rule-title
|
||||
# description: rule-title
|
||||
# organizations:
|
||||
# - rule-org
|
||||
# related_resources:
|
||||
# - https://rule
|
||||
# - ref: https://rule
|
||||
# description: rr-rule-note
|
||||
# authors:
|
||||
# - rule-author
|
||||
# schemas:
|
||||
# - input: {"type": "string"}
|
||||
# custom:
|
||||
# rule: rule-custom
|
||||
p = 1`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
fileName := fmt.Sprintf("%s/a/xxxxxxxxxxxxxxxxxxxxxx/yyyyyyyyyyyyyyyyyyyy/foo.rego", rootDir)
|
||||
ps := newInspectCommandParams()
|
||||
var out bytes.Buffer
|
||||
err := doInspect(ps, fileName, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
shortFileName := truncateFileName(fileName)
|
||||
output := strings.TrimSpace(out.String())
|
||||
expected := strings.TrimSpace(fmt.Sprintf(`
|
||||
NAMESPACES:
|
||||
+-----------+----------------------------------------------------+
|
||||
| NAMESPACE | FILE |
|
||||
+-----------+----------------------------------------------------+
|
||||
| data.test | %s |
|
||||
+-----------+----------------------------------------------------+
|
||||
`, shortFileName))
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("Unexpected output. Expected:\n\n%q\n\nGot:\n\n%q", expected, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
// Info represents information about a bundle.
|
||||
type Info struct {
|
||||
Manifest bundle.Manifest `json:"manifest,omitempty"`
|
||||
Manifest *bundle.Manifest `json:"manifest,omitempty"`
|
||||
Signatures bundle.SignaturesConfig `json:"signatures_config,omitempty"`
|
||||
WasmModules []map[string]interface{} `json:"wasm_modules,omitempty"`
|
||||
Namespaces map[string][]string `json:"namespaces,omitempty"`
|
||||
@@ -35,6 +35,14 @@ func File(path string, includeAnnotations bool) (*Info, error) {
|
||||
}
|
||||
|
||||
func FileForRegoVersion(regoVersion ast.RegoVersion, path string, includeAnnotations bool) (*Info, error) {
|
||||
if strings.HasSuffix(path, bundle.RegoExt) {
|
||||
return fileInfoForRegoVersion(regoVersion, path, includeAnnotations)
|
||||
}
|
||||
|
||||
return bundleOrDirInfoForRegoVersion(regoVersion, path, includeAnnotations)
|
||||
}
|
||||
|
||||
func bundleOrDirInfoForRegoVersion(regoVersion ast.RegoVersion, path string, includeAnnotations bool) (*Info, error) {
|
||||
b, err := loader.NewFileLoader().
|
||||
WithRegoVersion(regoVersion).
|
||||
WithSkipBundleVerification(true).
|
||||
@@ -52,7 +60,7 @@ func FileForRegoVersion(regoVersion ast.RegoVersion, path string, includeAnnotat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bi := &Info{Manifest: b.Manifest}
|
||||
bi := &Info{Manifest: &b.Manifest}
|
||||
|
||||
namespaces := make(map[string][]string, len(b.Modules))
|
||||
modules := make([]*ast.Module, 0, len(b.Modules))
|
||||
@@ -199,3 +207,55 @@ func (bi *Info) getBundleDataWasmAndSignatures(name string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileInfoForRegoVersion(regoVersion ast.RegoVersion, path string, includeAnnotations bool) (*Info, error) {
|
||||
res, err := loader.NewFileLoader().
|
||||
WithRegoVersion(regoVersion).
|
||||
WithSkipBundleVerification(true).
|
||||
WithProcessAnnotation(true). // Always process annotations, for enriching namespace listing
|
||||
WithJSONOptions(&json.Options{
|
||||
MarshalOptions: json.MarshalOptions{
|
||||
IncludeLocation: json.NodeToggle{
|
||||
// Annotation location data is only included if includeAnnotations is set
|
||||
AnnotationsRef: includeAnnotations,
|
||||
},
|
||||
},
|
||||
}).
|
||||
All([]string{path})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bi := &Info{
|
||||
Namespaces: make(map[string][]string, len(res.Modules)),
|
||||
}
|
||||
|
||||
moduleMap := make(map[string]*ast.Module, len(res.Modules))
|
||||
|
||||
for _, m := range res.Modules {
|
||||
bi.Namespaces[m.Parsed.Package.Path.String()] = append(
|
||||
bi.Namespaces[m.Parsed.Package.Path.String()],
|
||||
filepath.Clean(m.Name),
|
||||
)
|
||||
moduleMap[m.Name] = m.Parsed
|
||||
}
|
||||
|
||||
if includeAnnotations {
|
||||
as, errs := ast.BuildAnnotationSet(util.Values(moduleMap))
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
bi.Annotations = as.Flatten()
|
||||
}
|
||||
|
||||
c := ast.NewCompiler().
|
||||
WithAllowUndefinedFunctionCalls(true)
|
||||
c.Compile(moduleMap)
|
||||
if c.Failed() {
|
||||
return bi, c.Errors
|
||||
}
|
||||
|
||||
bi.Required = c.Required
|
||||
|
||||
return bi, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
// Values returns a slice of values from any map. Copied from golang.org/x/exp/maps.
|
||||
func Values[M ~map[K]V, K comparable, V any](m M) []V {
|
||||
r := make([]V, 0, len(m))
|
||||
for _, v := range m {
|
||||
r = append(r, v)
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValues(t *testing.T) {
|
||||
testMap := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
values := Values(testMap)
|
||||
if len(values) != 3 {
|
||||
t.Errorf("Expected 3 values, got %d", len(values))
|
||||
}
|
||||
slices.Sort(values)
|
||||
if values[0] != 1 || values[1] != 2 || values[2] != 3 {
|
||||
t.Errorf("Expected [1, 2, 3], got %v", values)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user