Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
type inspectCommandParams struct {
outputFormat *util.EnumFlag
listAnnotations bool
dataPaths repeatedStringFlag
v0Compatible bool
v1Compatible bool
}
Expand All @@ -56,6 +57,7 @@ func newInspectCommandParams() inspectCommandParams {
return inspectCommandParams{
outputFormat: formats.Flag(formats.Pretty, formats.JSON),
listAnnotations: false,
dataPaths: newrepeatedStringFlag([]string{}),
}
}

Expand All @@ -66,12 +68,12 @@ func initInspect(root *cobra.Command, brand string) {

inspectCommand := &cobra.Command{
Use: "inspect <path> [<path> [...]]",
Short: `Inspect ` + brand + ` bundle(s)`,
Long: `Inspect ` + brand + ` bundle(s).
Short: `Inspect ` + brand + ` bundle(s), Rego files, or data files`,
Long: `Inspect ` + brand + ` bundle(s), Rego files, or data files.

The 'inspect' command provides a summary of the contents in ` + brand + ` 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:
The 'inspect' command provides a summary of the contents in ` + brand + ` bundle(s), a single Rego file,
or data files. Bundles are gzipped tarballs containing policies and data. The 'inspect' command reads
bundle(s) and lists the following:

* packages that are contributed by .rego files
* data locations defined by the data.json and data.yaml files
Expand All @@ -80,16 +82,24 @@ the following:
* information about the Wasm module files
* package- and rule annotations

Example:
Examples:

# Inspect a bundle
$ ls
bundle.tar.gz
$ ` + executable + ` inspect bundle.tar.gz

# Inspect data files
$ ` + executable + ` inspect --data data.json
$ ` + executable + ` inspect --data config.yaml

You can provide exactly one ` + brand + ` bundle, 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
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.

Alternatively, you can use the --data flag to inspect individual JSON or YAML data files. This flag can
be repeated to inspect multiple data files.
`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := validateInspectParams(&params, args); err != nil {
Expand All @@ -101,7 +111,11 @@ will load that file and summarize its structure and contents.
cmd.SilenceErrors = true
cmd.SilenceUsage = true

if err := doInspect(params, args[0], os.Stdout); err != nil {
path := ""
if len(args) > 0 {
path = args[0]
}
if err := doInspect(params, path, os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
return err
}
Expand All @@ -111,13 +125,21 @@ will load that file and summarize its structure and contents.

addOutputFormat(inspectCommand.Flags(), params.outputFormat)
addListAnnotations(inspectCommand.Flags(), &params.listAnnotations)
addDataFlag(inspectCommand.Flags(), &params.dataPaths)
addV0CompatibleFlag(inspectCommand.Flags(), &params.v0Compatible, false)
addV1CompatibleFlag(inspectCommand.Flags(), &params.v1Compatible, false)
root.AddCommand(inspectCommand)
}

func doInspect(params inspectCommandParams, path string, out io.Writer) error {
info, err := ib.FileForRegoVersion(params.regoVersion(), path, params.listAnnotations)
var info *ib.Info
var err error

if params.dataPaths.isFlagSet() {
info, err = ib.DataFileInfo(params.dataPaths.v)
} else {
info, err = ib.FileForRegoVersion(params.regoVersion(), path, params.listAnnotations)
}
if err != nil {
return err
}
Expand Down Expand Up @@ -168,7 +190,11 @@ func hasManifest(info *ib.Info) bool {
}

func validateInspectParams(p *inspectCommandParams, args []string) error {
if len(args) != 1 {
if p.dataPaths.isFlagSet() {
if len(args) > 0 {
return errors.New("specify either a bundle/path argument or --data flag, not both")
}
} else if len(args) != 1 {
return errors.New("specify exactly one OPA bundle or path")
}

Expand Down
122 changes: 122 additions & 0 deletions cmd/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2263,3 +2263,125 @@ NAMESPACES:
}
})
}

func TestDoInspectDataFile(t *testing.T) {
files := map[string]string{
"/data.json": `{"users": {"alice": {"role": "admin"}}}`,
}

test.WithTempFS(files, func(rootDir string) {
fileName := filepath.Join(rootDir, "data.json")
ps := newInspectCommandParams()
ps.dataPaths = newrepeatedStringFlag([]string{})
if err := ps.dataPaths.Set(fileName); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
err := doInspect(ps, "", &out)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}

output := out.String()
if !strings.Contains(output, "NAMESPACES:") {
t.Fatalf("Expected NAMESPACES section in output, got:\n%v", output)
}
if !strings.Contains(output, "data") {
t.Fatalf("Expected 'data' namespace in output, got:\n%v", output)
}
if !strings.Contains(output, "data.json") {
t.Fatalf("Expected 'data.json' file in output, got:\n%v", output)
}
})
}

func TestDoInspectDataFileYAML(t *testing.T) {
files := map[string]string{
"/config.yaml": "users:\n alice:\n role: admin\n",
}

test.WithTempFS(files, func(rootDir string) {
fileName := filepath.Join(rootDir, "config.yaml")
ps := newInspectCommandParams()
ps.dataPaths = newrepeatedStringFlag([]string{})
if err := ps.dataPaths.Set(fileName); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
err := doInspect(ps, "", &out)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}

output := out.String()
if !strings.Contains(output, "NAMESPACES:") {
t.Fatalf("Expected NAMESPACES section in output, got:\n%v", output)
}
if !strings.Contains(output, "config.yaml") {
t.Fatalf("Expected 'config.yaml' file in output, got:\n%v", output)
}
})
}

func TestDoInspectDataFileInvalidJSON(t *testing.T) {
files := map[string]string{
"/bad.json": `{"users": invalid}`,
}

test.WithTempFS(files, func(rootDir string) {
fileName := filepath.Join(rootDir, "bad.json")
ps := newInspectCommandParams()
ps.dataPaths = newrepeatedStringFlag([]string{})
if err := ps.dataPaths.Set(fileName); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
err := doInspect(ps, "", &out)
if err == nil {
t.Fatal("Expected error for invalid JSON")
}
})
}

func TestDoInspectDataFileNonDataFile(t *testing.T) {
files := map[string]string{
"/policy.rego": `package test`,
}

test.WithTempFS(files, func(rootDir string) {
fileName := filepath.Join(rootDir, "policy.rego")
ps := newInspectCommandParams()
ps.dataPaths = newrepeatedStringFlag([]string{})
if err := ps.dataPaths.Set(fileName); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
err := doInspect(ps, "", &out)
if err == nil {
t.Fatal("Expected error for non-data file")
}
if !strings.Contains(err.Error(), "not a JSON or YAML data file") {
t.Fatalf("Expected 'not a JSON or YAML' error, got: %v", err)
}
})
}

func TestValidateInspectParamsDataAndArgs(t *testing.T) {
ps := newInspectCommandParams()
ps.dataPaths = newrepeatedStringFlag([]string{})
if err := ps.dataPaths.Set("data.json"); err != nil {
t.Fatal(err)
}

err := validateInspectParams(&ps, []string{"bundle.tar.gz"})
if err == nil {
t.Fatal("Expected error when both --data and positional args are given")
}
if !strings.Contains(err.Error(), "specify either") {
t.Fatalf("Unexpected error: %v", err)
}
}
49 changes: 49 additions & 0 deletions internal/bundle/inspect/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/loader"
"github.com/open-policy-agent/opa/v1/util"
"sigs.k8s.io/yaml"
)

// Info represents information about a bundle.
Expand All @@ -42,6 +43,54 @@ func FileForRegoVersion(regoVersion ast.RegoVersion, path string, includeAnnotat
return bundleOrDirInfoForRegoVersion(regoVersion, path, includeAnnotations)
}

// DataFileInfo returns an Info struct describing the given data files.
// It accepts JSON (.json) and YAML (.yaml, .yml) files.
func DataFileInfo(paths []string) (*Info, error) {
bi := &Info{
Namespaces: make(map[string][]string),
}

for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("error accessing path %s: %w", path, err)
}

if info.IsDir() {
return nil, fmt.Errorf("path %s is a directory, use positional argument for directories", path)
}

ext := strings.ToLower(filepath.Ext(path))
if ext != ".json" && ext != ".yaml" && ext != ".yml" {
return nil, fmt.Errorf("file %s is not a JSON or YAML data file", path)
}

// Read the file to validate it can be parsed
bs, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading file %s: %w", path, err)
}

if ext == ".yaml" || ext == ".yml" {
if _, err := yaml.YAMLToJSON(bs); err != nil {
return nil, fmt.Errorf("error parsing YAML file %s: %w", path, err)
}
} else {
var x any
if err := util.UnmarshalJSON(bs, &x); err != nil {
return nil, fmt.Errorf("error parsing JSON file %s: %w", path, err)
}
}

bi.Namespaces[ast.DefaultRootDocument.String()] = append(
bi.Namespaces[ast.DefaultRootDocument.String()],
filepath.Clean(path),
)
}

return bi, nil
}

func bundleOrDirInfoForRegoVersion(regoVersion ast.RegoVersion, path string, includeAnnotations bool) (*Info, error) {
b, err := loader.NewFileLoader().
WithRegoVersion(regoVersion).
Expand Down
Loading