Skip to content

Commit

Permalink
Add built-in function to get runtime info
Browse files Browse the repository at this point in the history
These changes add support for accessing runtime information inside of
policies. In some cases, policies need to access environment variables
or configuration that OPA was booted with. These changes add a built-in
function that allows policies to gain access to this information. The
built-in function itself is relatively trivial. Most of the required
changes were plumbing the runtime information from the entrypoint down
into the evaluation engine. The alternative would have been to introduce
a global variable containing this information however that would be have
been harder to reason about in library integrations.

Fixes open-policy-agent#420

Signed-off-by: Torin Sandall <torinsandall@gmail.com>
  • Loading branch information
tsandall committed Oct 16, 2018
1 parent ca74ca1 commit cf73499
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 25 deletions.
17 changes: 17 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ var DefaultBuiltins = [...]*Builtin{
// Rego
RegoParseModule,

// OPA
OPARuntime,

// Tracing
Trace,

Expand Down Expand Up @@ -1263,6 +1266,20 @@ var RegoParseModule = &Builtin{
),
}

/**
* OPA
*/

// OPARuntime returns an object containing OPA runtime information such as the
// configuration that OPA was booted with.
var OPARuntime = &Builtin{
Name: "opa.runtime",
Decl: types.NewFunction(
nil,
types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)),
),
}

/**
* Trace
*/
Expand Down
8 changes: 7 additions & 1 deletion cmd/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/open-policy-agent/opa/ast"
pr "github.com/open-policy-agent/opa/internal/presentation"
"github.com/open-policy-agent/opa/internal/runtime"
"github.com/open-policy-agent/opa/loader"
"github.com/open-policy-agent/opa/metrics"
"github.com/open-policy-agent/opa/profiler"
Expand Down Expand Up @@ -195,7 +196,12 @@ func eval(args []string, params evalCommandParams) (err error) {
query = args[0]
}

regoArgs := []func(*rego.Rego){rego.Query(query)}
info, err := runtime.Term(runtime.Params{})
if err != nil {
return err
}

regoArgs := []func(*rego.Rego){rego.Query(query), rego.Runtime(info)}

if len(params.imports.v) > 0 {
regoArgs = append(regoArgs, rego.Imports(params.imports.v))
Expand Down
11 changes: 10 additions & 1 deletion cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"os"
"time"

"github.com/open-policy-agent/opa/internal/runtime"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/cover"
"github.com/open-policy-agent/opa/tester"
Expand Down Expand Up @@ -110,6 +112,12 @@ func opaTest(args []string) int {
return 1
}

info, err := runtime.Term(runtime.Params{})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}

var cov *cover.Cover
var coverTracer topdown.Tracer

Expand All @@ -122,7 +130,8 @@ func opaTest(args []string) int {
SetCompiler(compiler).
SetStore(store).
EnableTracing(testParams.verbose).
SetCoverageTracer(coverTracer)
SetCoverageTracer(coverTracer).
SetRuntime(info)

ch, err := runner.Run(ctx, modules)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions docs/book/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ evaluation query will always return the same value.
| ------- |--------|-------------|
| <span class="opa-keep-it-together">``rego.parse_module(filename, string, output)``</span> | 2 | ``rego.parse_module`` parses the input ``string`` as a Rego module and returns the AST as a JSON object ``output``. |

### OPA
| Built-in | Inputs | Description |
| ------- |--------|-------------|
| <span class="opa-keep-it-together">``opa.runtime(output)``</span> | 0 | ``opa.runtime`` returns a JSON object ``output`` that describes the runtime environment where OPA is deployed. **Caution**: Policies that depend on the output of ``opa.runtime`` may return different answers depending on how OPA was started. If possible, prefer using an explicit `input` or `data` value instead of `opa.runtime`. The ``output`` of ``opa.runtime`` will include a ``"config"`` key if OPA was started with a configuration file. The ``output`` of ``opa.runtime`` will include a ``"env"`` key containing the environment variables that the OPA process was started with. |

### Debugging
| Built-in | Inputs | Description |
| ------- |--------|-------------|
Expand Down
61 changes: 61 additions & 0 deletions internal/runtime/runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 runtime contains utilities to return runtime information on the OPA instance.
package runtime

import (
"io/ioutil"
"os"
"strings"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/util"
)

// Params controls the types of runtime information to return.
type Params struct {
ConfigFile string
}

// Term returns the runtime information as an ast.Term object.
func Term(params Params) (*ast.Term, error) {

obj := ast.NewObject()

if params.ConfigFile != "" {

bs, err := ioutil.ReadFile(params.ConfigFile)
if err != nil {
return nil, err
}

var x interface{}
if err := util.Unmarshal(bs, &x); err != nil {
return nil, err
}

v, err := ast.InterfaceToValue(x)
if err != nil {
return nil, err
}

obj.Insert(ast.StringTerm("config"), ast.NewTerm(v))
}

env := ast.NewObject()

for _, s := range os.Environ() {
parts := strings.SplitN(s, "=", 2)
if len(parts) == 1 {
env.Insert(ast.StringTerm(parts[0]), ast.NullTerm())
} else if len(parts) > 1 {
env.Insert(ast.StringTerm(parts[0]), ast.StringTerm(parts[1]))
}
}

obj.Insert(ast.StringTerm("env"), ast.NewTerm(env))

return ast.NewTerm(obj), nil
}
14 changes: 12 additions & 2 deletions rego/rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ type Rego struct {
capture map[*ast.Expr]ast.Var // map exprs to generated capture vars
termVarID int
dump io.Writer
runtime *ast.Term
}

// Dump returns an argument that sets the writer to dump debugging information to.
Expand Down Expand Up @@ -301,6 +302,14 @@ func Tracer(t topdown.Tracer) func(r *Rego) {
}
}

// Runtime returns an argument that sets the runtime data to provide to the
// evaluation engine.
func Runtime(term *ast.Term) func(r *Rego) {
return func(r *Rego) {
r.runtime = term
}
}

// New returns a new Rego object.
func New(options ...func(*Rego)) *Rego {

Expand Down Expand Up @@ -638,7 +647,8 @@ func (r *Rego) eval(ctx context.Context, qc ast.QueryCompiler, compiled ast.Body
WithStore(r.store).
WithTransaction(txn).
WithMetrics(r.metrics).
WithInstrumentation(r.instrumentation)
WithInstrumentation(r.instrumentation).
WithRuntime(r.runtime)

if r.tracer != nil {
q = q.WithTracer(r.tracer)
Expand Down Expand Up @@ -775,7 +785,7 @@ func (r *Rego) partial(ctx context.Context, compiled ast.Body, txn storage.Trans
WithMetrics(r.metrics).
WithInstrumentation(r.instrumentation).
WithUnknowns(unknowns).
WithPartialNamespace(partialNamespace)
WithRuntime(r.runtime)

if r.tracer != nil {
q = q.WithTracer(r.tracer)
Expand Down
13 changes: 11 additions & 2 deletions repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ import (

// REPL represents an instance of the interactive shell.
type REPL struct {
output io.Writer
store storage.Store
output io.Writer
store storage.Store
runtime *ast.Term

modules map[string]*ast.Module
currentModuleID string
Expand Down Expand Up @@ -295,6 +296,12 @@ func (r *REPL) DisableUndefinedOutput(yes bool) *REPL {
return r
}

// WithRuntime sets the runtime data to provide to the evaluation engine.
func (r *REPL) WithRuntime(term *ast.Term) *REPL {
r.runtime = term
return r
}

func (r *REPL) complete(line string) []string {
c := []string{}
set := map[string]struct{}{}
Expand Down Expand Up @@ -796,6 +803,7 @@ func (r *REPL) evalBody(ctx context.Context, compiler *ast.Compiler, input ast.V
rego.Metrics(r.metrics),
rego.Tracer(buf),
rego.Instrument(r.instrument),
rego.Runtime(r.runtime),
)

rs, err := eval.Eval(ctx)
Expand Down Expand Up @@ -843,6 +851,7 @@ func (r *REPL) evalPartial(ctx context.Context, compiler *ast.Compiler, input as
rego.Tracer(buf),
rego.Instrument(r.instrument),
rego.ParsedUnknowns(r.unknowns),
rego.Runtime(r.runtime),
)

pq, err := eval.Partial(ctx)
Expand Down
20 changes: 14 additions & 6 deletions runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/sirupsen/logrus"
"gopkg.in/fsnotify.v1"
"io"
"io/ioutil"
"os"
"sync"
"time"

"github.com/pkg/errors"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/runtime"
"github.com/open-policy-agent/opa/loader"
"github.com/open-policy-agent/opa/plugins"
"github.com/open-policy-agent/opa/plugins/bundle"
Expand All @@ -32,7 +30,9 @@ import (
"github.com/open-policy-agent/opa/storage/inmem"
"github.com/open-policy-agent/opa/util"
"github.com/open-policy-agent/opa/version"
"sync"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gopkg.in/fsnotify.v1"
)

var (
Expand Down Expand Up @@ -135,6 +135,7 @@ type Runtime struct {
Store storage.Store
Manager *plugins.Manager

info *ast.Term // runtime information provided to evaluation engine
decisionLogger func(context.Context, *server.Info)
}

Expand Down Expand Up @@ -190,10 +191,16 @@ func NewRuntime(ctx context.Context, params Params) (*Runtime, error) {
}
}

info, err := runtime.Term(runtime.Params{ConfigFile: params.ConfigFile})
if err != nil {
return nil, err
}

rt := &Runtime{
Store: store,
Manager: m,
Params: params,
info: info,
decisionLogger: decisionLogger,
}

Expand Down Expand Up @@ -227,6 +234,7 @@ func (rt *Runtime) StartServer(ctx context.Context) {
WithDiagnosticsBuffer(rt.Params.DiagnosticsBuffer).
WithDecisionIDFactory(rt.Params.DecisionIDFactory).
WithDecisionLogger(rt.decisionLogger).
WithRuntime(rt.info).
Init(ctx)

if err != nil {
Expand Down Expand Up @@ -270,7 +278,7 @@ func (rt *Runtime) StartREPL(ctx context.Context) {
defer rt.Manager.Stop(ctx)

banner := rt.getBanner()
repl := repl.New(rt.Store, rt.Params.HistoryPath, rt.Params.Output, rt.Params.OutputFormat, rt.Params.ErrorLimit, banner)
repl := repl.New(rt.Store, rt.Params.HistoryPath, rt.Params.Output, rt.Params.OutputFormat, rt.Params.ErrorLimit, banner).WithRuntime(rt.info)

if rt.Params.Watch {
if err := rt.startWatcher(ctx, rt.Params.Paths, onReloadPrinter(rt.Params.Output)); err != nil {
Expand Down
19 changes: 17 additions & 2 deletions server/authorizer/authorizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,29 @@ type Basic struct {
inner http.Handler
compiler func() *ast.Compiler
store storage.Store
runtime *ast.Term
}

// Runtime returns an argument that sets the runtime on the authorizer.
func Runtime(term *ast.Term) func(*Basic) {
return func(b *Basic) {
b.runtime = term
}
}

// NewBasic returns a new Basic object.
func NewBasic(inner http.Handler, compiler func() *ast.Compiler, store storage.Store) http.Handler {
return &Basic{
func NewBasic(inner http.Handler, compiler func() *ast.Compiler, store storage.Store, opts ...func(*Basic)) http.Handler {
b := &Basic{
inner: inner,
compiler: compiler,
store: store,
}

for _, opt := range opts {
opt(b)
}

return b
}

func (h *Basic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand All @@ -52,6 +66,7 @@ func (h *Basic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rego.Compiler(h.compiler()),
rego.Store(h.store),
rego.Input(input),
rego.Runtime(h.runtime),
)

rs, err := rego.Eval(r.Context())
Expand Down
Loading

0 comments on commit cf73499

Please sign in to comment.