mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
4bfae4e64e
Signed-off-by: Colin Lacy <colinjlacy@gmail.com>
414 lines
10 KiB
Go
414 lines
10 KiB
Go
// Copyright 2021 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/bundle"
|
|
"github.com/open-policy-agent/opa/cmd/internal/env"
|
|
ib "github.com/open-policy-agent/opa/internal/bundle/inspect"
|
|
pr "github.com/open-policy-agent/opa/internal/presentation"
|
|
iStrs "github.com/open-policy-agent/opa/internal/strings"
|
|
"github.com/open-policy-agent/opa/util"
|
|
|
|
"github.com/olekukonko/tablewriter"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const maxTableFieldLen = 50
|
|
const pageWidth = 80
|
|
|
|
type inspectCommandParams struct {
|
|
outputFormat *util.EnumFlag
|
|
listAnnotations bool
|
|
v1Compatible bool
|
|
}
|
|
|
|
func (p *inspectCommandParams) regoVersion() ast.RegoVersion {
|
|
if p.v1Compatible {
|
|
return ast.RegoV1
|
|
}
|
|
return ast.RegoV0
|
|
}
|
|
|
|
func newInspectCommandParams() inspectCommandParams {
|
|
return inspectCommandParams{
|
|
outputFormat: util.NewEnumFlag(evalPrettyOutput, []string{
|
|
evalJSONOutput,
|
|
evalPrettyOutput,
|
|
}),
|
|
listAnnotations: false,
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
|
|
params := newInspectCommandParams()
|
|
|
|
var inspectCommand = &cobra.Command{
|
|
Use: "inspect <path> [<path> [...]]",
|
|
Short: "Inspect OPA bundle(s)",
|
|
Long: `Inspect OPA bundle(s).
|
|
|
|
The 'inspect' command provides a summary of the contents in OPA bundle(s). 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
|
|
* manifest data
|
|
* signature data
|
|
* information about the Wasm module files
|
|
* package- and rule annotations
|
|
|
|
Example:
|
|
|
|
$ ls
|
|
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.
|
|
`,
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if err := validateInspectParams(¶ms, args); err != nil {
|
|
return err
|
|
}
|
|
return env.CmdFlags.CheckEnvironmentVariables(cmd)
|
|
},
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
if err := doInspect(params, args[0], os.Stdout); err != nil {
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
|
|
addOutputFormat(inspectCommand.Flags(), params.outputFormat)
|
|
addListAnnotations(inspectCommand.Flags(), ¶ms.listAnnotations)
|
|
addV1CompatibleFlag(inspectCommand.Flags(), ¶ms.v1Compatible, false)
|
|
RootCommand.AddCommand(inspectCommand)
|
|
}
|
|
|
|
func doInspect(params inspectCommandParams, path string, out io.Writer) error {
|
|
info, err := ib.FileForRegoVersion(params.regoVersion(), path, params.listAnnotations)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch params.outputFormat.String() {
|
|
case evalJSONOutput:
|
|
return pr.JSON(out, info)
|
|
|
|
default:
|
|
if info.Manifest.Revision != "" || len(*info.Manifest.Roots) != 0 || len(info.Manifest.Metadata) != 0 {
|
|
if err := populateManifest(out, info.Manifest); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(info.Namespaces) != 0 {
|
|
if err := populateNamespaces(out, info.Namespaces); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if params.listAnnotations && len(info.Annotations) != 0 {
|
|
if err := populateAnnotations(out, info.Annotations); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func validateInspectParams(p *inspectCommandParams, args []string) error {
|
|
if len(args) != 1 {
|
|
return fmt.Errorf("specify exactly one OPA bundle or path")
|
|
}
|
|
|
|
of := p.outputFormat.String()
|
|
if of == evalJSONOutput || of == evalPrettyOutput {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("invalid output format for inspect command")
|
|
}
|
|
|
|
func populateManifest(out io.Writer, m bundle.Manifest) error {
|
|
t := generateTableWithKeys(out, "field", "value")
|
|
var lines [][]string
|
|
|
|
if m.Revision != "" {
|
|
lines = append(lines, []string{"Revision", truncateTableStr(m.Revision)})
|
|
}
|
|
|
|
if len(*m.Roots) != 0 {
|
|
roots := *m.Roots
|
|
if len(roots) == 1 {
|
|
if roots[0] != "" {
|
|
lines = append(lines, []string{"Roots", truncateFileName(roots[0])})
|
|
}
|
|
} else {
|
|
sort.Strings(roots)
|
|
for _, root := range roots {
|
|
lines = append(lines, []string{"Roots", truncateFileName(root)})
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(m.Metadata) != 0 {
|
|
metadata, err := json.Marshal(m.Metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines = append(lines, []string{"Metadata", truncateTableStr(string(metadata))})
|
|
}
|
|
|
|
t.AppendBulk(lines)
|
|
if t.NumLines() > 0 {
|
|
fmt.Fprintln(out, "MANIFEST:")
|
|
t.Render()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func populateNamespaces(out io.Writer, n map[string][]string) error {
|
|
t := generateTableWithKeys(out, "namespace", "file")
|
|
// only auto-merge the namespace column
|
|
t.SetAutoMergeCells(false)
|
|
t.SetAutoMergeCellsByColumnIndex([]int{0})
|
|
var lines [][]string
|
|
|
|
keys := make([]string, 0, len(n))
|
|
for k := range n {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
for _, file := range n[k] {
|
|
lines = append(lines, []string{k, truncateFileName(file)})
|
|
}
|
|
}
|
|
|
|
t.AppendBulk(lines)
|
|
if t.NumLines() > 0 {
|
|
fmt.Fprintln(out, "NAMESPACES:")
|
|
t.Render()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func populateAnnotations(out io.Writer, refs []*ast.AnnotationsRef) error {
|
|
if len(refs) > 0 {
|
|
fmt.Fprintln(out, "ANNOTATIONS:")
|
|
for _, ref := range refs {
|
|
printTitle(out, ref)
|
|
fmt.Fprintln(out)
|
|
|
|
if a := ref.Annotations; a != nil && len(a.Description) > 0 {
|
|
fmt.Fprintln(out, a.Description)
|
|
fmt.Fprintln(out)
|
|
}
|
|
|
|
if p := ref.GetPackage(); p != nil {
|
|
fmt.Fprintln(out, "Package: ", dropDataPrefix(p.Path))
|
|
}
|
|
if r := ref.GetRule(); r != nil {
|
|
fmt.Fprintln(out, "Rule: ", r.Head.Name)
|
|
}
|
|
if loc := ref.Location; loc != nil {
|
|
fmt.Fprintln(out, "Location:", loc.String())
|
|
}
|
|
if a := ref.Annotations; a != nil {
|
|
if len(a.Scope) > 0 {
|
|
fmt.Fprintln(out, "Scope:", a.Scope)
|
|
}
|
|
if a.Entrypoint {
|
|
fmt.Fprintln(out, "Entrypoint:", a.Entrypoint)
|
|
}
|
|
}
|
|
fmt.Fprintln(out)
|
|
|
|
if a := ref.Annotations; a != nil {
|
|
if len(a.Organizations) > 0 {
|
|
fmt.Fprintln(out, "Organizations:")
|
|
l := make([]listEntry, 0, len(a.Organizations))
|
|
for _, o := range a.Organizations {
|
|
l = append(l, listEntry{"", removeNewLines(o)})
|
|
}
|
|
printList(out, l, "")
|
|
fmt.Fprintln(out)
|
|
}
|
|
|
|
if len(a.Authors) > 0 {
|
|
fmt.Fprintln(out, "Authors:")
|
|
l := make([]listEntry, 0, len(a.Authors))
|
|
for _, a := range a.Authors {
|
|
l = append(l, listEntry{"", removeNewLines(a.String())})
|
|
}
|
|
printList(out, l, "")
|
|
fmt.Fprintln(out)
|
|
}
|
|
|
|
if len(a.Schemas) > 0 {
|
|
// NOTE(johanfylling): The Type Checker will MERGE all applicable schema annotations for a rule
|
|
// into one list. Here, child nodes OVERRIDE parent nodes' schema annotations instead (default annot. behavior).
|
|
// Should the former behavior be replicated here?
|
|
fmt.Fprintln(out, "Schemas:")
|
|
l := make([]listEntry, 0, len(a.Schemas))
|
|
for _, s := range a.Schemas {
|
|
le := listEntry{key: s.Path.String()}
|
|
if len(s.Schema) > 0 {
|
|
le.value = s.Schema.String()
|
|
} else if s.Definition != nil {
|
|
b, _ := json.Marshal(s.Definition)
|
|
le.value = string(b)
|
|
}
|
|
l = append(l, le)
|
|
}
|
|
printList(out, l, ": ")
|
|
fmt.Fprintln(out)
|
|
}
|
|
|
|
if len(a.RelatedResources) > 0 {
|
|
fmt.Fprintln(out, "Related Resources:")
|
|
l := make([]listEntry, 0, len(a.RelatedResources))
|
|
for _, res := range a.RelatedResources {
|
|
l = append(l, listEntry{removeNewLines(res.Ref.String()), res.Description})
|
|
}
|
|
printList(out, l, " ")
|
|
fmt.Fprintln(out)
|
|
}
|
|
if len(a.Custom) > 0 {
|
|
fmt.Fprintln(out, "Custom:")
|
|
l := make([]listEntry, 0, len(a.Custom))
|
|
for k, v := range a.Custom {
|
|
b, _ := json.Marshal(v)
|
|
l = append(l, listEntry{k, string(b)})
|
|
}
|
|
printList(out, l, ": ")
|
|
fmt.Fprintln(out)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type listEntry struct {
|
|
key string
|
|
value string
|
|
}
|
|
|
|
func printList(out io.Writer, list []listEntry, separator string) {
|
|
keyLength := 0
|
|
for _, e := range list {
|
|
l := len(e.key)
|
|
if l > keyLength {
|
|
keyLength = l
|
|
}
|
|
}
|
|
for _, e := range list {
|
|
var line string
|
|
if len(e.value) > 0 {
|
|
line = fmt.Sprintf(" %s%s%s%s",
|
|
e.key,
|
|
separator,
|
|
strings.Repeat(" ", keyLength-len(e.key)),
|
|
e.value)
|
|
} else {
|
|
line = fmt.Sprintf(" %v", e.key)
|
|
}
|
|
fmt.Fprintln(out, truncateStr(line, pageWidth))
|
|
}
|
|
}
|
|
|
|
func printTitle(out io.Writer, ref *ast.AnnotationsRef) {
|
|
var title string
|
|
if a := ref.Annotations; a != nil {
|
|
t := strings.TrimSpace(a.Title)
|
|
if len(t) > 0 {
|
|
title = t
|
|
}
|
|
}
|
|
|
|
if len(title) == 0 {
|
|
title = dropDataPrefix(ref.Path).String()
|
|
}
|
|
|
|
fmt.Fprintf(out, "%s\n", title)
|
|
|
|
var underline []byte
|
|
for i := 0; i < len(title) && i < pageWidth; i++ {
|
|
underline = append(underline, '=')
|
|
}
|
|
fmt.Fprintln(out, string(underline))
|
|
}
|
|
|
|
func generateTableWithKeys(writer io.Writer, keys ...string) *tablewriter.Table {
|
|
table := tablewriter.NewWriter(writer)
|
|
aligns := []int{}
|
|
hdrs := make([]string, 0, len(keys))
|
|
for _, k := range keys {
|
|
hdrs = append(hdrs, strings.Title(k)) //nolint:staticcheck // SA1019, no unicode
|
|
aligns = append(aligns, tablewriter.ALIGN_LEFT)
|
|
}
|
|
table.SetHeader(hdrs)
|
|
table.SetAlignment(tablewriter.ALIGN_CENTER)
|
|
table.SetAutoMergeCells(true)
|
|
table.SetColumnAlignment(aligns)
|
|
table.SetRowLine(false)
|
|
table.SetAutoWrapText(false)
|
|
return table
|
|
}
|
|
|
|
func truncateTableStr(s string) string {
|
|
return truncateStr(s, maxTableFieldLen)
|
|
}
|
|
|
|
func truncateStr(s string, maxLen int) string {
|
|
if len(s) < maxLen {
|
|
return s
|
|
}
|
|
return fmt.Sprintf("%v...", s[:maxLen-3])
|
|
}
|
|
|
|
func removeNewLines(s string) string {
|
|
return strings.ReplaceAll(s, "\n", " ")
|
|
}
|
|
|
|
func truncateFileName(s string) string {
|
|
if len(s) < maxTableFieldLen {
|
|
return s
|
|
}
|
|
|
|
res, _ := iStrs.TruncateFilePaths(maxTableFieldLen, len(s), s)
|
|
return res[s]
|
|
}
|
|
|
|
// dropDataPrefix drops the first component of the passed Ref
|
|
func dropDataPrefix(ref ast.Ref) ast.Ref {
|
|
if len(ref) <= 1 {
|
|
return ast.EmptyRef()
|
|
}
|
|
r := ref[1:].Copy()
|
|
if s, ok := r[0].Value.(ast.String); ok {
|
|
r[0].Value = ast.Var(s)
|
|
}
|
|
return r
|
|
}
|