From d71e3bb6c22fff6674030b8d30858a3965dcb2cb Mon Sep 17 00:00:00 2001 From: IoannisMatzaris <81440191+IoannisMatzaris@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:57:48 +0200 Subject: [PATCH] cmd/capabilities: expose capabilities through CLI (#4588) There is a new command argument "capabilities". With this, it is possible to print the current capabilities version, show all capabilities versions & print any capabilities version, without the need of a file. Moreover, for the other commands which use the --capabilities flag, it is possible to give only the version number, without specifying a file. However, there are no breaking changes for those who use the capabilities file as an input for the flag. Unit tests were also written, in order to test the new argument and the changes made in ast. Fixes: #4236 Signed-off-by: IoannisMatzaris --- ast/capabilities.go | 52 +++++++++++++++- ast/capabilities_test.go | 39 ++++++++++++ cmd/build.go | 2 +- cmd/capabilities.go | 124 +++++++++++++++++++++++++++++++++++++++ cmd/capabilities_test.go | 76 ++++++++++++++++++++++++ cmd/flags.go | 28 +++++---- docs/content/cli.md | 73 +++++++++++++++++++++-- 7 files changed, 376 insertions(+), 18 deletions(-) create mode 100644 cmd/capabilities.go create mode 100644 cmd/capabilities_test.go diff --git a/ast/capabilities.go b/ast/capabilities.go index a6b162edf7..02fc3c277b 100644 --- a/ast/capabilities.go +++ b/ast/capabilities.go @@ -5,14 +5,19 @@ package ast import ( + "bytes" + "fmt" "io" + "os" "sort" + "strings" + caps "github.com/open-policy-agent/opa/capabilities" "github.com/open-policy-agent/opa/internal/wasm/sdk/opa/capabilities" "github.com/open-policy-agent/opa/util" ) -// Capabilities defines a structure containing data that describes the capablilities +// Capabilities defines a structure containing data that describes the capabilities // or features supported by a particular version of OPA. type Capabilities struct { Builtins []*Builtin `json:"builtins"` @@ -63,3 +68,48 @@ func LoadCapabilitiesJSON(r io.Reader) (*Capabilities, error) { var c Capabilities return &c, d.Decode(&c) } + +// LoadCapabilitiesVersion loads a JSON serialized capabilities structure from the specific version. +func LoadCapabilitiesVersion(version string) (*Capabilities, error) { + cvs, err := LoadCapabilitiesVersions() + if err != nil { + return nil, err + } + + for _, cv := range cvs { + if cv == version { + cont, err := caps.FS.ReadFile(cv + ".json") + if err != nil { + return nil, err + } + + return LoadCapabilitiesJSON(bytes.NewReader(cont)) + } + + } + return nil, fmt.Errorf("no capabilities version found %v", version) +} + +// LoadCapabilitiesFile loads a JSON serialized capabilities structure from a file. +func LoadCapabilitiesFile(file string) (*Capabilities, error) { + fd, err := os.Open(file) + if err != nil { + return nil, err + } + defer fd.Close() + return LoadCapabilitiesJSON(fd) +} + +// LoadCapabilitiesVersions loads all capabilities versions +func LoadCapabilitiesVersions() ([]string, error) { + ents, err := caps.FS.ReadDir(".") + if err != nil { + return nil, err + } + + var capabilitiesVersions []string + for _, ent := range ents { + capabilitiesVersions = append(capabilitiesVersions, strings.Replace(ent.Name(), ".json", "", 1)) + } + return capabilitiesVersions, nil +} diff --git a/ast/capabilities_test.go b/ast/capabilities_test.go index 0d4d8377da..d6db51f1a6 100644 --- a/ast/capabilities_test.go +++ b/ast/capabilities_test.go @@ -1,7 +1,10 @@ package ast import ( + "path" "testing" + + "github.com/open-policy-agent/opa/util/test" ) func TestParserCatchesIllegalCapabilities(t *testing.T) { @@ -83,3 +86,39 @@ func TestParserCapabilitiesWithWildcardOptInAndOlderOPA(t *testing.T) { t.Fatal("unexpected error:", err) } } + +func TestLoadCapabilitiesVersion(t *testing.T) { + + capabilitiesVersions, err := LoadCapabilitiesVersions() + if err != nil { + t.Fatal("expected success", err) + } + + if len(capabilitiesVersions) == 0 { + t.Fatal("expected a non-empty array of capabilities versions") + } + for _, cv := range capabilitiesVersions { + if _, err := LoadCapabilitiesVersion(cv); err != nil { + t.Fatal("expected success", err) + } + } +} + +func TestLoadCapabilitiesFile(t *testing.T) { + + files := map[string]string{ + "test-capabilities.json": ` + { + "builtins": [] + } + `, + } + + test.WithTempFS(files, func(root string) { + _, err := LoadCapabilitiesFile(path.Join(root, "test-capabilities.json")) + if err != nil { + t.Fatal("expected success", err) + } + }) + +} diff --git a/cmd/build.go b/cmd/build.go index 4e1fe5ff4e..b4bd96a6b2 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -201,7 +201,7 @@ The OPA repository contains a set of capabilities files for each OPA release. Fo the following command builds a directory of policies ('./policies') and validates them against OPA v0.22.0: - opa build ./policies --capabilities $OPA_SRC/capabilities/v0.22.0.json + opa build ./policies --capabilities v0.22.0 `, PreRunE: func(Cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/cmd/capabilities.go b/cmd/capabilities.go new file mode 100644 index 0000000000..8bd001d9de --- /dev/null +++ b/cmd/capabilities.go @@ -0,0 +1,124 @@ +// Copyright 2022 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" + "strings" + + "github.com/open-policy-agent/opa/ast" + "github.com/spf13/cobra" +) + +type capabilitiesParams struct { + showCurrent bool + version string + file string +} + +func init() { + + capabilitiesParams := capabilitiesParams{} + + var capabilitiesCommand = &cobra.Command{ + Use: "capabilities", + Short: "Print the capabilities of OPA", + Long: `Show capabilities for OPA. + +The 'capabilities' command prints the OPA capabilities, prior to and including the version of OPA used. + +Print a list of all existing capabilities version names + + $ opa capabilities + v0.17.0 + v0.17.1 + ... + v0.37.1 + v0.37.2 + v0.38.0 + ... + +Print the capabilities of the current version + + $ opa capabilities --current + { + "builtins": [...], + "future_keywords": [...], + "wasm_abi_versions": [...] + } + +Print the capabilities of a specific version + + $ opa capabilities --version v0.32.1 + { + "builtins": [...], + "future_keywords": null, + "wasm_abi_versions": [...] + } + +Print the capabilities of a capabilities file + + $ opa capabilities --file ./capabilities/v0.32.1.json + { + "builtins": [...], + "future_keywords": null, + "wasm_abi_versions": [...] + } + +`, + RunE: func(*cobra.Command, []string) error { + cs, err := doCapabilities(capabilitiesParams) + if err != nil { + return err + } + fmt.Println(cs) + return nil + }, + } + capabilitiesCommand.Flags().BoolVar(&capabilitiesParams.showCurrent, "current", false, "print current capabilities") + capabilitiesCommand.Flags().StringVar(&capabilitiesParams.version, "version", "", "print capabilities of a specific version") + capabilitiesCommand.Flags().StringVar(&capabilitiesParams.file, "file", "", "print current capabilities") + + RootCommand.AddCommand(capabilitiesCommand) +} + +func doCapabilities(params capabilitiesParams) (string, error) { + var ( + c *ast.Capabilities + err error + ) + + if len(params.version) > 0 { + c, err = ast.LoadCapabilitiesVersion(params.version) + } else if len(params.file) > 0 { + c, err = ast.LoadCapabilitiesFile(params.file) + } else if params.showCurrent { + c = ast.CapabilitiesForThisVersion() + } else { + return showVersions() + } + + if err != nil { + return "", err + } + + bs, err := json.MarshalIndent(c, "", " ") + if err != nil { + return "", err + } + return string(bs), nil + +} + +func showVersions() (string, error) { + cvs, err := ast.LoadCapabilitiesVersions() + if err != nil { + return "", err + } + + t := strings.Join(cvs, "\n") + return t, nil +} diff --git a/cmd/capabilities_test.go b/cmd/capabilities_test.go new file mode 100644 index 0000000000..6bfcdd75f1 --- /dev/null +++ b/cmd/capabilities_test.go @@ -0,0 +1,76 @@ +// Copyright 2022 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 ( + "path" + "testing" + + "github.com/open-policy-agent/opa/util/test" +) + +func TestCapabilitiesNoArgs(t *testing.T) { + t.Run("test with no arguments", func(t *testing.T) { + _, err := doCapabilities(capabilitiesParams{}) + if err != nil { + t.Fatal("expected success", err) + } + }) +} + +func TestCapabilitiesVersion(t *testing.T) { + t.Run("test with version", func(t *testing.T) { + params := capabilitiesParams{ + version: "v0.39.0", + } + _, err := doCapabilities(params) + if err != nil { + t.Fatal("expected success", err) + } + }) +} + +func TestCapabilitiesFile(t *testing.T) { + t.Run("test with file", func(t *testing.T) { + files := map[string]string{ + "test-capabilities.json": ` + { + "builtins": [ + { + "name": "plus", + "infix": "+", + "decl": { + "type": "function", + "args": [ + { + "type": "number" + }, + { + "type": "number" + } + ], + "result": { + "type": "number" + } + } + } + ] + } + `, + } + + test.WithTempFS(files, func(root string) { + params := capabilitiesParams{ + file: path.Join(root, "test-capabilities.json"), + } + _, err := doCapabilities(params) + + if err != nil { + t.Fatal("expected success", err) + } + }) + + }) +} diff --git a/cmd/flags.go b/cmd/flags.go index 330b317c74..47e5a12f05 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -6,7 +6,6 @@ package cmd import ( "fmt" - "os" "github.com/spf13/pflag" @@ -127,7 +126,7 @@ func addBundleVerificationExcludeFilesFlag(fs *pflag.FlagSet, excludeNames *[]st } func addCapabilitiesFlag(fs *pflag.FlagSet, f *capabilitiesFlag) { - fs.VarP(f, "capabilities", "", "set capabilities.json file path") + fs.VarP(f, "capabilities", "", "set capabilities version or capabilities.json file path") } func addPartialFlag(fs *pflag.FlagSet, partial *bool, value bool) { @@ -170,8 +169,8 @@ func setExplainFlag(fs *pflag.FlagSet, explain *util.EnumFlag) { } type capabilitiesFlag struct { - C *ast.Capabilities - path string + C *ast.Capabilities + pathOrVersion string } func newcapabilitiesFlag() *capabilitiesFlag { @@ -187,18 +186,23 @@ func (f *capabilitiesFlag) Type() string { } func (f *capabilitiesFlag) String() string { - return f.path + return f.pathOrVersion } func (f *capabilitiesFlag) Set(s string) error { - f.path = s - fd, err := os.Open(s) - if err != nil { - return err + f.pathOrVersion = s + var errPath, errVersion error + + f.C, errPath = ast.LoadCapabilitiesFile(s) + if errPath != nil { + f.C, errVersion = ast.LoadCapabilitiesVersion(s) } - defer fd.Close() - f.C, err = ast.LoadCapabilitiesJSON(fd) - return err + + if errVersion != nil && errPath != nil { + return fmt.Errorf("no such file or capabilities version found: %v", s) + } + return nil + } type stringptrFlag struct { diff --git a/docs/content/cli.md b/docs/content/cli.md index c40ec36f11..d772d81211 100755 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -206,7 +206,7 @@ The OPA repository contains a set of capabilities files for each OPA release. Fo the following command builds a directory of policies ('./policies') and validates them against OPA v0.22.0: - opa build ./policies --capabilities $OPA_SRC/capabilities/v0.22.0.json + opa build ./policies --capabilities v0.22.0 ``` @@ -217,7 +217,7 @@ opa build [ [...]] [flags] ``` -b, --bundle load paths as bundle files or root directories - --capabilities string set capabilities.json file path + --capabilities string set capabilities version or capabilities.json file path --claims-file string set path of JSON file containing optional claims (see: https://www.openpolicyagent.org/docs/latest/management-bundles/#signature-format) --debug enable debug output -e, --entrypoint string set slash separated entrypoint path @@ -238,6 +238,71 @@ opa build [ [...]] [flags] ____ +## opa capabilities + +Print the capabilities of OPA + +### Synopsis + +Show capabilities for OPA. + +The 'capabilities' command prints the OPA capabilities, prior to and including the version of OPA used. + +Print a list of all existing capabilities version names + + $ opa capabilities + v0.17.0 + v0.17.1 + ... + v0.37.1 + v0.37.2 + v0.38.0 + ... + +Print the capabilities of the current version + + $ opa capabilities --current + { + "builtins": [...], + "future_keywords": [...], + "wasm_abi_versions": [...] + } + +Print the capabilities of a specific version + + $ opa capabilities --version v0.32.1 + { + "builtins": [...], + "future_keywords": null, + "wasm_abi_versions": [...] + } + +Print the capabilities of a capabilities file + + $ opa capabilities --file ./capabilities/v0.32.1.json + { + "builtins": [...], + "future_keywords": null, + "wasm_abi_versions": [...] + } + + + +``` +opa capabilities [flags] +``` + +### Options + +``` + --current print current capabilities + --file string print current capabilities + -h, --help help for capabilities + --version string print capabilities of a specific version +``` + +____ + ## opa check Check Rego source files @@ -258,7 +323,7 @@ opa check [path [...]] [flags] ``` -b, --bundle load paths as bundle files or root directories - --capabilities string set capabilities.json file path + --capabilities string set capabilities version or capabilities.json file path -f, --format {pretty,json} set output format (default pretty) -h, --help help for check --ignore strings set file and directory names to ignore during loading (e.g., '.*' excludes hidden files) @@ -435,7 +500,7 @@ opa eval [flags] ``` -b, --bundle string set bundle file(s) or directory path(s). This flag can be repeated. - --capabilities string set capabilities.json file path + --capabilities string set capabilities version or capabilities.json file path --count int number of times to repeat each benchmark (default 1) --coverage report coverage -d, --data string set policy or data file(s). This flag can be repeated.