mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
5a33432bd4
While likely not important for small bundles, using `opa build` to build large bundles would previously allocate much more memory than was needed, as the bundle would first be written to an intermediate in-memory buffer before getting written to disk. This fixes that by deferring the creation of the output file to the first write, then writing to that directly. Signed-off-by: Anders Eknert <anders.eknert@apple.com>
431 lines
15 KiB
Go
431 lines
15 KiB
Go
// 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 (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/open-policy-agent/opa/cmd/internal/env"
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/bundle"
|
|
"github.com/open-policy-agent/opa/v1/compile"
|
|
"github.com/open-policy-agent/opa/v1/keys"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
const defaultPublicKeyID = "default"
|
|
|
|
type (
|
|
buildParams struct {
|
|
capabilities *capabilitiesFlag
|
|
target *util.EnumFlag
|
|
planFormat *util.EnumFlag
|
|
bundleMode bool
|
|
pruneUnused bool
|
|
optimizationLevel int
|
|
entrypoints repeatedStringFlag
|
|
outputFile string
|
|
revision stringptrFlag
|
|
ignore []string
|
|
debug bool
|
|
algorithm string
|
|
key string
|
|
scope string
|
|
pubKey string
|
|
pubKeyID string
|
|
claimsFile string
|
|
excludeVerifyFiles []string
|
|
plugin string
|
|
ns string
|
|
v0Compatible bool
|
|
v1Compatible bool
|
|
followSymlinks bool
|
|
wasmIncludePrint bool
|
|
stderr io.Writer
|
|
}
|
|
// deferredFileWriter is a wrapper around [*os.File] that defers the creation of the file until
|
|
// the first write, which allows us to pass a file writer without using an intermediate buffer,
|
|
// and without creating any file in case the build fails.
|
|
deferredFileWriter struct {
|
|
*os.File
|
|
path string
|
|
}
|
|
)
|
|
|
|
func (w *deferredFileWriter) Write(p []byte) (n int, err error) {
|
|
if w.File == nil {
|
|
if w.File, err = os.Create(w.path); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return w.File.Write(p)
|
|
}
|
|
|
|
func newBuildParams() buildParams {
|
|
return buildParams{
|
|
capabilities: newCapabilitiesFlag(),
|
|
target: util.NewEnumFlag(compile.TargetRego, compile.Targets),
|
|
planFormat: util.NewEnumFlag(compile.PlanFormatJSON, compile.PlanFormats),
|
|
stderr: os.Stderr,
|
|
}
|
|
}
|
|
|
|
func (p *buildParams) regoVersion() ast.RegoVersion {
|
|
if p.v0Compatible {
|
|
// v0 takes precedence over v1
|
|
return ast.RegoV0
|
|
}
|
|
if p.v1Compatible {
|
|
return ast.RegoV1
|
|
}
|
|
return ast.DefaultRegoVersion
|
|
}
|
|
|
|
func initBuild(root *cobra.Command, brand string) {
|
|
executable := root.Name()
|
|
|
|
buildParams := newBuildParams()
|
|
|
|
buildCommand := &cobra.Command{
|
|
Use: "build <path> [<path> [...]]",
|
|
Short: `Build an ` + brand + ` bundle`,
|
|
Long: `Build an ` + brand + ` bundle.
|
|
|
|
The 'build' command packages ` + brand + ` policy and data files into bundles. Bundles are
|
|
gzipped tarballs containing policies and data. Paths referring to directories are
|
|
loaded recursively.
|
|
|
|
$ ls
|
|
example.rego
|
|
|
|
$ ` + executable + ` build -b .
|
|
|
|
You can load bundles into ` + brand + ` on the command-line:
|
|
|
|
$ ls
|
|
bundle.tar.gz example.rego
|
|
|
|
$ ` + executable + ` run bundle.tar.gz
|
|
|
|
You can also configure ` + brand + ` to download bundles from remote HTTP endpoints:
|
|
|
|
$ ` + executable + ` run --server \
|
|
--set bundles.example.resource=bundle.tar.gz \
|
|
--set services.example.url=http://localhost:8080
|
|
|
|
Inside another terminal in the same directory, serve the bundle via HTTP:
|
|
|
|
$ python3 -m http.server --bind localhost 8080
|
|
|
|
For more information on bundles see https://www.openpolicyagent.org/docs/latest/management-bundles/.
|
|
|
|
Common Flags
|
|
------------
|
|
|
|
When -b is specified the 'build' command assumes paths refer to existing bundle files
|
|
or directories following the bundle structure. If multiple bundles are provided, their
|
|
contents are merged. If there are any merge conflicts (e.g., due to conflicting bundle
|
|
roots), the command fails. When loading an existing bundle file, the .manifest from
|
|
the input bundle will be included in the output bundle. Flags that set .manifest fields
|
|
(such as --revision) override input bundle .manifest fields.
|
|
|
|
The -O flag controls the optimization level. By default, optimization is disabled (-O=0).
|
|
When optimization is enabled the 'build' command generates a bundle that is semantically
|
|
equivalent to the input files however the structure of the files in the bundle may have
|
|
been changed by rewriting, inlining, pruning, etc. Higher optimization levels may result
|
|
in longer build times. The --partial-namespace flag can used in conjunction with the -O flag
|
|
to specify the namespace for the partially evaluated files in the optimized bundle.
|
|
|
|
The 'build' command supports targets (specified by -t):
|
|
|
|
rego The default target emits a bundle containing a set of policy and data files
|
|
that are semantically equivalent to the input files. If optimizations are
|
|
disabled the output may simply contain a copy of the input policy and data
|
|
files. If optimization is enabled at least one entrypoint must be supplied,
|
|
either via the -e option, or via entrypoint metadata annotations.
|
|
|
|
wasm The wasm target emits a bundle containing a WebAssembly module compiled from
|
|
the input files for each specified entrypoint. The bundle may contain the
|
|
original policy or data files.
|
|
|
|
plan The plan target emits a bundle containing a plan, i.e., an intermediate
|
|
representation compiled from the input files for each specified entrypoint.
|
|
This is for further processing, ` + brand + ` cannot evaluate a "plan bundle" like it
|
|
can evaluate a wasm or rego bundle.
|
|
|
|
The -e flag tells the 'build' command which documents (entrypoints) will be queried by
|
|
the software asking for policy decisions, so that it can focus optimization efforts and
|
|
ensure that document is not eliminated by the optimizer.
|
|
Note: Unless the --prune-unused flag is used, any rule transitively referring to a
|
|
package or rule declared as an entrypoint will also be enumerated as an entrypoint.
|
|
|
|
Signing
|
|
-------
|
|
|
|
The 'build' command can be used to verify the signature of a signed bundle and
|
|
also to generate a signature for the output bundle the command creates.
|
|
|
|
If the directory path(s) provided to the 'build' command contain a ".signatures.json" file,
|
|
it will attempt to verify the signatures included in that file. The bundle files
|
|
or directory path(s) to verify must be specified using --bundle.
|
|
|
|
For more information on the bundle signing and verification, see
|
|
https://www.openpolicyagent.org/docs/latest/management-bundles/#signing.
|
|
|
|
Example:
|
|
|
|
$ ` + executable + ` build --verification-key /path/to/public_key.pem --signing-key /path/to/private_key.pem --bundle foo
|
|
|
|
Where foo has the following structure:
|
|
|
|
foo/
|
|
|
|
|
+-- bar/
|
|
| |
|
|
| +-- data.json
|
|
|
|
|
+-- policy.rego
|
|
|
|
|
+-- .manifest
|
|
|
|
|
+-- .signatures.json
|
|
|
|
|
|
The 'build' command will verify the signatures using the public key provided by the --verification-key flag.
|
|
The default signing algorithm is RS256 and the --signing-alg flag can be used to specify
|
|
a different one. The --verification-key-id and --scope flags can be used to specify the name for the key
|
|
provided using the --verification-key flag and scope to use for bundle signature verification respectively.
|
|
|
|
If the verification succeeds, the 'build' command will write out an updated ".signatures.json" file
|
|
to the output bundle. It will use the key specified by the --signing-key flag to sign
|
|
the token in the ".signatures.json" file.
|
|
|
|
To include additional claims in the payload use the --claims-file flag to provide a JSON file
|
|
containing optional claims.
|
|
|
|
For more information on the format of the ".signatures.json" file
|
|
see https://www.openpolicyagent.org/docs/latest/management-bundles/#signature-format.
|
|
|
|
Capabilities
|
|
------------
|
|
|
|
The 'build' command can validate policies against a configurable set of ` + brand + ` capabilities.
|
|
The capabilities define the built-in functions and other language features that policies
|
|
may depend on. For example, the following capabilities file only permits the policy to
|
|
depend on the "plus" built-in function ('+'):
|
|
|
|
{
|
|
"builtins": [
|
|
{
|
|
"name": "plus",
|
|
"infix": "+",
|
|
"decl": {
|
|
"type": "function",
|
|
"args": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
{
|
|
"type": "number"
|
|
}
|
|
],
|
|
"result": {
|
|
"type": "number"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
Capabilities can be used to validate policies against a specific version of ` + brand + `.
|
|
The ` + brand + ` repository contains a set of capabilities files for each ` + brand + ` release. For example,
|
|
the following command builds a directory of policies ('./policies') and validates them
|
|
against ` + brand + ` v0.22.0:
|
|
|
|
` + executable + ` build ./policies --capabilities v0.22.0
|
|
`,
|
|
PreRunE: func(Cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("expected at least one path")
|
|
}
|
|
return env.CmdFlags.CheckEnvironmentVariables(Cmd)
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
|
|
if err := dobuild(buildParams, args); err != nil {
|
|
fmt.Println("error:", err)
|
|
return err
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
buildCommand.Flags().VarP(buildParams.target, "target", "t", "set the output bundle target type")
|
|
buildCommand.Flags().Var(buildParams.planFormat, "format", "set the plan output format (only applies when --target=plan)")
|
|
buildCommand.Flags().BoolVar(&buildParams.pruneUnused, "prune-unused", false, "exclude dependents of entrypoints")
|
|
buildCommand.Flags().BoolVar(&buildParams.debug, "debug", false, "enable debug output")
|
|
buildCommand.Flags().IntVarP(&buildParams.optimizationLevel, "optimize", "O", 0, "set optimization level")
|
|
buildCommand.Flags().VarP(&buildParams.entrypoints, "entrypoint", "e", "set slash separated entrypoint path")
|
|
buildCommand.Flags().VarP(&buildParams.revision, "revision", "r", "set output bundle revision")
|
|
buildCommand.Flags().StringVarP(&buildParams.outputFile, "output", "o", "bundle.tar.gz", "set the output filename")
|
|
buildCommand.Flags().StringVar(&buildParams.ns, "partial-namespace", "partial", "set the namespace to use for partially evaluated files in an optimized bundle")
|
|
buildCommand.Flags().BoolVar(&buildParams.followSymlinks, "follow-symlinks", false, "follow symlinks in the input set of paths when building the bundle")
|
|
buildCommand.Flags().BoolVar(&buildParams.wasmIncludePrint, "wasm-include-print", false, "enable print statements inside of WebAssembly modules compiled by the compiler")
|
|
|
|
addBundleModeFlag(buildCommand.Flags(), &buildParams.bundleMode, false)
|
|
addIgnoreFlag(buildCommand.Flags(), &buildParams.ignore)
|
|
addCapabilitiesFlag(buildCommand.Flags(), buildParams.capabilities)
|
|
|
|
// bundle verification config
|
|
addVerificationKeyFlag(buildCommand.Flags(), &buildParams.pubKey)
|
|
addVerificationKeyIDFlag(buildCommand.Flags(), &buildParams.pubKeyID, defaultPublicKeyID)
|
|
addSigningAlgFlag(buildCommand.Flags(), &buildParams.algorithm, defaultTokenSigningAlg)
|
|
addBundleVerificationScopeFlag(buildCommand.Flags(), &buildParams.scope)
|
|
addBundleVerificationExcludeFilesFlag(buildCommand.Flags(), &buildParams.excludeVerifyFiles)
|
|
|
|
// bundle signing config
|
|
addSigningKeyFlag(buildCommand.Flags(), &buildParams.key)
|
|
addSigningPluginFlag(buildCommand.Flags(), &buildParams.plugin)
|
|
addClaimsFileFlag(buildCommand.Flags(), &buildParams.claimsFile)
|
|
|
|
addV0CompatibleFlag(buildCommand.Flags(), &buildParams.v0Compatible, false)
|
|
addV1CompatibleFlag(buildCommand.Flags(), &buildParams.v1Compatible, false)
|
|
|
|
root.AddCommand(buildCommand)
|
|
}
|
|
|
|
func dobuild(params buildParams, args []string) error {
|
|
// generate the bundle verification and signing config
|
|
bvc, err := buildVerificationConfig(params.pubKey, params.pubKeyID, params.algorithm, params.scope, params.excludeVerifyFiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bsc, err := buildSigningConfig(params.key, params.algorithm, params.claimsFile, params.plugin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if manifest files are found in the input directories and the -b flag is not set, this is likely a mistake.
|
|
if !params.bundleMode {
|
|
if bvc != nil || bsc != nil {
|
|
return errors.New("enable bundle mode (ie. --bundle) to verify or sign bundle files or directories")
|
|
}
|
|
|
|
for _, arg := range args {
|
|
stat, err := os.Stat(arg)
|
|
if err != nil || !stat.IsDir() {
|
|
continue
|
|
}
|
|
|
|
for _, name := range []string{bundle.ManifestExt, bundle.ManifestProtoExt} {
|
|
if _, err := os.Stat(filepath.Join(arg, name)); err == nil {
|
|
fmt.Fprintf(params.stderr,
|
|
"Warning: %s file found in %q but -b flag not specified. Manifest will be ignored.\n",
|
|
name, arg,
|
|
)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
out := &deferredFileWriter{path: params.outputFile}
|
|
defer out.Close()
|
|
|
|
compiler := compile.New().
|
|
WithCapabilities(util.Or(params.capabilities.C, capabilitiesForParamsVersion(params))).
|
|
WithTarget(params.target.String()).
|
|
WithPlanFormat(params.planFormat.String()).
|
|
WithAsBundle(params.bundleMode).
|
|
WithPruneUnused(params.pruneUnused).
|
|
WithOptimizationLevel(params.optimizationLevel).
|
|
WithOutput(out).
|
|
WithEntrypoints(params.entrypoints.v...).
|
|
WithRegoAnnotationEntrypoints(true).
|
|
WithPaths(args...).
|
|
WithFilter(buildCommandLoaderFilter(params.bundleMode, params.ignore)).
|
|
WithBundleLazyLoadingMode(bundle.HasExtension()).
|
|
WithBundleVerificationConfig(bvc).
|
|
WithBundleSigningConfig(bsc).
|
|
WithPartialNamespace(params.ns).
|
|
WithFollowSymlinks(params.followSymlinks)
|
|
|
|
compiler = compiler.WithRegoVersion(params.regoVersion())
|
|
|
|
if params.revision.isSet {
|
|
compiler = compiler.WithRevision(*params.revision.v)
|
|
}
|
|
|
|
if params.debug {
|
|
compiler = compiler.WithDebug(params.stderr)
|
|
}
|
|
|
|
if params.claimsFile == "" {
|
|
compiler = compiler.WithBundleVerificationKeyID(params.pubKeyID)
|
|
}
|
|
|
|
if params.target.String() == compile.TargetPlan {
|
|
compiler = compiler.WithEnablePrintStatements(true)
|
|
}
|
|
|
|
if params.target.String() == compile.TargetWasm {
|
|
compiler = compiler.WithEnablePrintStatements(params.wasmIncludePrint)
|
|
}
|
|
|
|
return compiler.Build(context.Background())
|
|
}
|
|
|
|
func buildCommandLoaderFilter(bundleMode bool, ignore []string) func(string, os.FileInfo, int) bool {
|
|
return func(abspath string, info os.FileInfo, depth int) bool {
|
|
if !bundleMode {
|
|
if !info.IsDir() && strings.HasSuffix(abspath, ".tar.gz") {
|
|
return true
|
|
}
|
|
}
|
|
return ignored(ignore).Apply(abspath, info, depth)
|
|
}
|
|
}
|
|
|
|
func buildVerificationConfig(pubKey, pubKeyID, alg, scope string, excludeFiles []string) (*bundle.VerificationConfig, error) {
|
|
if pubKey == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
keyConfig, err := keys.NewKeyConfig(pubKey, alg, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
confMap := map[string]*keys.Config{pubKeyID: keyConfig}
|
|
|
|
return bundle.NewVerificationConfig(confMap, pubKeyID, scope, excludeFiles), nil
|
|
}
|
|
|
|
func buildSigningConfig(key, alg, claimsFile, plugin string) (*bundle.SigningConfig, error) {
|
|
if key == "" {
|
|
if plugin != "" || claimsFile != "" {
|
|
return nil, errSigningConfigIncomplete
|
|
}
|
|
return nil, nil
|
|
}
|
|
return bundle.NewSigningConfig(key, alg, claimsFile).WithPlugin(plugin), nil
|
|
}
|
|
|
|
func capabilitiesForParamsVersion(params buildParams) func() *ast.Capabilities {
|
|
return func() *ast.Capabilities {
|
|
return ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(params.regoVersion()))
|
|
}
|
|
}
|