From a400fbab78dc2e7b702ce49dbbc528d8f2beb452 Mon Sep 17 00:00:00 2001 From: Torin Sandall Date: Mon, 20 Aug 2018 16:12:06 -0700 Subject: [PATCH] Add deps subcommand These changes add a new subcommand that analyzes policies and prints base and virtual document dependencies. Signed-off-by: Torin Sandall --- cmd/deps.go | 113 ++++++++++++++++++++++++++ internal/presentation/presentation.go | 75 +++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 cmd/deps.go diff --git a/cmd/deps.go b/cmd/deps.go new file mode 100644 index 0000000000..61abe787a4 --- /dev/null +++ b/cmd/deps.go @@ -0,0 +1,113 @@ +// Copyright 2018 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 ( + "fmt" + "os" + + "github.com/open-policy-agent/opa/dependencies" + "github.com/open-policy-agent/opa/internal/presentation" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/loader" + "github.com/open-policy-agent/opa/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type depsCommandParams struct { + dataPaths repeatedStringFlag + format *util.EnumFlag + ignore []string +} + +const ( + depsFormatPretty = "pretty" + depsFormatJSON = "json" +) + +func init() { + + var params depsCommandParams + + params.format = util.NewEnumFlag(depsFormatPretty, []string{ + depsFormatPretty, depsFormatJSON, + }) + + depsCommand := &cobra.Command{ + Use: "deps ", + Short: "Analyze Rego query dependencies", + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("specify exactly one query argument") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if err := deps(args, params); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + }, + } + + depsCommand.Flags().VarP(params.format, "format", "f", "set output format") + depsCommand.Flags().VarP(¶ms.dataPaths, "data", "d", "set data file(s) or directory path(s)") + setIgnore(depsCommand.Flags(), ¶ms.ignore) + + RootCommand.AddCommand(depsCommand) +} + +func deps(args []string, params depsCommandParams) error { + + query, err := ast.ParseBody(args[0]) + if err != nil { + return err + } + + f := loaderFilter{ + Ignore: params.ignore, + } + + result, err := loader.Filtered(params.dataPaths.v, f.Apply) + if err != nil { + return err + } + + modules := map[string]*ast.Module{} + for _, m := range result.Modules { + modules[m.Name] = m.Parsed + } + + compiler := ast.NewCompiler() + compiler.Compile(modules) + + if compiler.Failed() { + return compiler.Errors + } + + brs, err := dependencies.Base(compiler, query) + if err != nil { + return err + } + + vrs, err := dependencies.Virtual(compiler, query) + if err != nil { + return err + } + + output := presentation.DepAnalysisOutput{ + Base: brs, + Virtual: vrs, + } + + switch params.format.String() { + case depsFormatJSON: + return presentation.JSON(os.Stdout, output) + default: + return output.Pretty(os.Stdout) + } +} diff --git a/internal/presentation/presentation.go b/internal/presentation/presentation.go index db82993af3..d7ea3af68c 100644 --- a/internal/presentation/presentation.go +++ b/internal/presentation/presentation.go @@ -25,6 +25,81 @@ import ( "github.com/open-policy-agent/opa/topdown" ) +// DepAnalysisOutput contains the result of dependency analysis to be presented. +type DepAnalysisOutput struct { + Base []ast.Ref `json:"base,omitempty"` + Virtual []ast.Ref `json:"virtual,omitempty"` +} + +// JSON outputs o to w as JSON. +func (o DepAnalysisOutput) JSON(w io.Writer) error { + o.sort() + return JSON(w, o) +} + +// Pretty outputs o to w in a human-readable format. +func (o DepAnalysisOutput) Pretty(w io.Writer) error { + + var headers []string + var rows [][]string + + // Fill two columns if results have base and virtual docs. Else fill one column. + if len(o.Base) > 0 && len(o.Virtual) > 0 { + maxLen := len(o.Base) + if len(o.Virtual) > maxLen { + maxLen = len(o.Virtual) + } + headers = []string{"Base Documents", "Virtual Documents"} + rows = make([][]string, maxLen) + for i := range rows { + rows[i] = make([]string, 2) + if i < len(o.Base) { + rows[i][0] = o.Base[i].String() + } + if i < len(o.Virtual) { + rows[i][1] = o.Virtual[i].String() + } + } + } else if len(o.Base) > 0 { + headers = []string{"Base Documents"} + rows = make([][]string, len(o.Base)) + for i := range rows { + rows[i] = []string{o.Base[i].String()} + } + } else if len(o.Virtual) > 0 { + headers = []string{"Virtual Documents"} + rows = make([][]string, len(o.Base)) + for i := range rows { + rows[i] = []string{o.Virtual[i].String()} + } + } + + if len(rows) == 0 { + return nil + } + + table := tablewriter.NewWriter(w) + table.SetHeader(headers) + table.SetAutoWrapText(false) + for i := range rows { + table.Append(rows[i]) + } + + table.Render() + + return nil +} + +func (o DepAnalysisOutput) sort() { + sort.Slice(o.Base, func(i, j int) bool { + return o.Base[i].Compare(o.Base[j]) < 0 + }) + + sort.Slice(o.Virtual, func(i, j int) bool { + return o.Virtual[i].Compare(o.Virtual[j]) < 0 + }) +} + // Output contains the result of evaluation to be presented. type Output struct { Error error `json:"error,omitempty"`