-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is an experimental feature, subject to change. Fixes: #6876 Signed-off-by: Johan Fylling <johan.dev@fylling.se>
- Loading branch information
1 parent
b0f417f
commit 3ac5104
Showing
18 changed files
with
4,406 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
# OPA Debug API | ||
|
||
This directory contains the OPA Debug API. | ||
The Debug API facilitates programmatic debugging of Rego policies, on top of which 3rd parties can build tools for debugging. | ||
|
||
This API takes inspiration from the [Debug Adapter Protocol (DAP)](https://microsoft.github.io/debug-adapter-protocol/), | ||
and follows the conventions established therein for managing threads, breakpoints, and variable scopes. | ||
|
||
> **Note**: The Debug API is experimental and subject to change. | ||
## Creating a Debug Session | ||
|
||
```go | ||
debugger := debug.NewDebugger() | ||
|
||
ctx := context.Background() | ||
evalProps := debug.EvalProperties{ | ||
Query: "data.example.allow = x", | ||
InputPath: "/path/to/input.json", | ||
LaunchProperties: LaunchProperties{ | ||
DataPaths: []string{"/path/to/data.json", "/path/to/policy.rego"}, | ||
}, | ||
} | ||
session, err := s.debugger.LaunchEval(ctx, evalProps) | ||
if err != nil { | ||
// handle error | ||
} | ||
|
||
// The session is launched in a paused state. | ||
// Before resuming the session, here is the opportunity to set breakpoints | ||
|
||
// Resume execution of all threads associated with the session | ||
err = session.ResumeAll() | ||
if err != nil { | ||
// handle error | ||
} | ||
``` | ||
|
||
## Managing Breakpoints | ||
|
||
Breakpoints can be added, removed, and enumerated. | ||
|
||
Breakpoints are added to file-and-row locations in a module, and are triggered when the policy evaluation reaches that location. | ||
Breakpoints can be added at any time during policy evaluation. | ||
|
||
```go | ||
// Add a breakpoint | ||
br, err := session.AddBreakpoint(location.Location{ | ||
File: "/path/to/policy.rego", | ||
Row: 10, | ||
}) | ||
if err != nil { | ||
// handle error | ||
} | ||
|
||
// ... | ||
|
||
// Remove the breakpoint | ||
_, err = session.RemoveBreakpoint(br.ID) | ||
if err != nil { | ||
// handle error | ||
} | ||
``` | ||
|
||
## Stepping Through Policy Evaluation | ||
|
||
When evaluation execution is paused, either immidiately after launching a session or when a breakpoint is hit, the session can be stepped through. | ||
|
||
### Step Over | ||
|
||
`StepOver()` executes the next expression in the current scope and then stops on the next expression in the same scope, | ||
not stopping on expressions in sub-scopes; e.g. execution of referenced rule, called function, comprehension, or every expression. | ||
|
||
```go | ||
threads, err := session.Threads() | ||
if err != nil { | ||
// handle error | ||
} | ||
|
||
if err := session.StepOver(threads[0].ID); err != nil { | ||
// handle error | ||
} | ||
``` | ||
|
||
#### Example 1 | ||
|
||
``` | ||
allow if { | ||
x := f(input) >-+ | ||
x == 1 | | ||
} | | ||
| | ||
f(x) := y if { <-+ | ||
y := x + 1 | ||
} | ||
``` | ||
|
||
### Example 2 | ||
|
||
``` | ||
allow if { | ||
every x in l { >-+ | ||
x < 10 <-+ | ||
} | ||
input.x == 1 | ||
``` | ||
|
||
### Step In | ||
|
||
`StepIn()` executes the next expression in the current scope and then stops on the next expression in the same scope or sub-scope; | ||
stepping into any referenced rule, called function, comprehension, or every expression. | ||
|
||
```go | ||
if err := session.StepIn(threads[0].ID); err != nil { | ||
// handle error | ||
} | ||
``` | ||
|
||
### Example 1 | ||
|
||
``` | ||
allow if { | ||
x := f(input) >-+ | ||
x == 1 | | ||
} | | ||
| | ||
f(x) := y if { <-+ | ||
y := x + 1 | ||
} | ||
``` | ||
|
||
### Example 2 | ||
|
||
``` | ||
allow if { | ||
every x in l { >-+ | ||
x < 10 <-+ | ||
} | ||
input.x == 1 | ||
} | ||
``` | ||
|
||
### Step Out | ||
|
||
`StepOut()` steps out of the current scope (rule, function, comprehension, every expression) and stops on the next expression in the parent scope. | ||
|
||
```go | ||
if err := session.StepOut(threads[0].ID); err != nil { | ||
// handle error | ||
} | ||
``` | ||
|
||
#### Example 1 | ||
|
||
``` | ||
allow if { | ||
x := f(input) <-+ | ||
x == 1 | | ||
} | | ||
| | ||
f(x) := y if { | | ||
y := x + 1 >-+ | ||
} | ||
``` | ||
|
||
### Example 2 | ||
|
||
``` | ||
allow if { | ||
every x in l { | ||
x < 10 >-+ | ||
} | | ||
input.x == 1 <-+ | ||
} | ||
``` | ||
|
||
## Fetching Variable Values | ||
|
||
The current values of local and global variables are organized into scopes: | ||
|
||
* `Local`: contains variables defined in the current rule, function, comprehension, or every expression. | ||
* `Virtual Cache`: contains the state of the global Virtual Cache, where calculated return values for rules and functions are stored. | ||
* `Input`: contains the input document. | ||
* `Data`: contains the data document. | ||
* `Result Set`: contains the result set of the current query. This scope is only available on the final expression of the query evaluation. | ||
|
||
```go | ||
scopes, err := session.Scopes(thread.ID) | ||
if err != nil { | ||
// handle error | ||
} | ||
|
||
var localScope debug.Scope | ||
for _, scope := range scopes { | ||
if scope.Name == "Local" { | ||
localScope = scope | ||
break | ||
} | ||
} | ||
|
||
variables, err := session.Variables(localScope.VariablesReference()) | ||
if err != nil { | ||
// handle error | ||
} | ||
|
||
// Enumerate and process variables | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
// Copyright 2024 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 debug | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/open-policy-agent/opa/ast/location" | ||
) | ||
|
||
type BreakpointID int | ||
|
||
type Breakpoint interface { | ||
ID() BreakpointID | ||
Location() location.Location | ||
} | ||
|
||
type breakpoint struct { | ||
id BreakpointID | ||
location location.Location | ||
} | ||
|
||
func (b breakpoint) ID() BreakpointID { | ||
return b.id | ||
} | ||
|
||
func (b breakpoint) Location() location.Location { | ||
return b.location | ||
} | ||
|
||
func (b breakpoint) String() string { | ||
return fmt.Sprintf("<%d> %s:%d", b.id, b.location.File, b.location.Row) | ||
} | ||
|
||
type breakpointList []Breakpoint | ||
|
||
func (b breakpointList) String() string { | ||
if b == nil { | ||
return "[]" | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
buf.WriteString("[") | ||
for i, bp := range b { | ||
if i > 0 { | ||
buf.WriteString(", ") | ||
} | ||
_, _ = fmt.Fprint(buf, bp) | ||
} | ||
buf.WriteString("]") | ||
return buf.String() | ||
} | ||
|
||
type breakpointCollection struct { | ||
breakpoints map[string]breakpointList | ||
idCounter BreakpointID | ||
mtx sync.Mutex | ||
} | ||
|
||
func newBreakpointCollection() *breakpointCollection { | ||
return &breakpointCollection{ | ||
breakpoints: map[string]breakpointList{}, | ||
} | ||
} | ||
|
||
func (bc *breakpointCollection) newID() BreakpointID { | ||
bc.idCounter++ | ||
return bc.idCounter | ||
} | ||
|
||
func (bc *breakpointCollection) add(location location.Location) Breakpoint { | ||
bc.mtx.Lock() | ||
defer bc.mtx.Unlock() | ||
|
||
bp := breakpoint{ | ||
id: bc.newID(), | ||
location: location, | ||
} | ||
bps := bc.breakpoints[bp.location.File] | ||
bps = append(bps, bp) | ||
bc.breakpoints[bp.location.File] = bps | ||
return bp | ||
} | ||
|
||
func (bc *breakpointCollection) all() breakpointList { | ||
bc.mtx.Lock() | ||
defer bc.mtx.Unlock() | ||
|
||
var bps breakpointList | ||
for _, list := range bc.breakpoints { | ||
bps = append(bps, list...) | ||
} | ||
return bps | ||
} | ||
|
||
func (bc *breakpointCollection) allForFilePath(path string) breakpointList { | ||
bc.mtx.Lock() | ||
defer bc.mtx.Unlock() | ||
|
||
return bc.breakpoints[path] | ||
} | ||
|
||
func (bc *breakpointCollection) remove(id BreakpointID) Breakpoint { | ||
bc.mtx.Lock() | ||
defer bc.mtx.Unlock() | ||
|
||
var removed Breakpoint | ||
for path, bps := range bc.breakpoints { | ||
var newBps breakpointList | ||
for _, bp := range bps { | ||
if bp.ID() != id { | ||
newBps = append(newBps, bp) | ||
} else { | ||
removed = bp | ||
} | ||
} | ||
bc.breakpoints[path] = newBps | ||
} | ||
|
||
return removed | ||
} | ||
|
||
func (bc *breakpointCollection) clear() { | ||
bc.mtx.Lock() | ||
defer bc.mtx.Unlock() | ||
|
||
bc.breakpoints = map[string]breakpointList{} | ||
} | ||
|
||
func (bc *breakpointCollection) String() string { | ||
if bc == nil { | ||
return "[]" | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
buf.WriteString("[") | ||
for _, bps := range bc.breakpoints { | ||
for i, bp := range bps { | ||
if i > 0 { | ||
buf.WriteString(", ") | ||
} | ||
_, _ = fmt.Fprint(buf, bp) | ||
} | ||
} | ||
buf.WriteString("]") | ||
return buf.String() | ||
} |
Oops, something went wrong.