ast: extend capabilities to return min compatible version

This commit adds the ability to determine the minimum compatible OPA
version for a set of capabilities. This can be coupled with the
capabilities generated by the compiler to determine the min. compatible
version of a policy/bundle. The build has been extended to generate the
version index that lets us quickly check the required version for each
builtin/feature/keyword in the capabilities.

Signed-off-by: Torin Sandall <torin@styra.com>
This commit is contained in:
Torin Sandall
2023-10-25 14:41:43 -07:00
committed by Torin Sandall
parent 3e6f747743
commit 16fccc9a76
6 changed files with 1612 additions and 1 deletions
+73
View File
@@ -6,6 +6,8 @@ package ast
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"io"
"os"
@@ -13,10 +15,37 @@ import (
"strings"
caps "github.com/open-policy-agent/opa/capabilities"
"github.com/open-policy-agent/opa/internal/semver"
"github.com/open-policy-agent/opa/internal/wasm/sdk/opa/capabilities"
"github.com/open-policy-agent/opa/util"
)
// VersonIndex contains an index from built-in function name, language feature,
// and future rego keyword to version number. During the build, this is used to
// create an index of the minimum version required for the built-in/feature/kw.
type VersionIndex struct {
Builtins map[string]semver.Version `json:"builtins"`
Features map[string]semver.Version `json:"features"`
Keywords map[string]semver.Version `json:"keywords"`
}
// NOTE(tsandall): this file is generated by internal/cmd/genversionindex/main.go
// and run as part of go:generate. We generate the version index as part of the
// build process because it's relatively expensive to build (it takes ~500ms on
// my machine) and never changes.
//
//go:embed version_index.json
var versionIndexBs []byte
var minVersionIndex = func() VersionIndex {
var vi VersionIndex
err := json.Unmarshal(versionIndexBs, &vi)
if err != nil {
panic(err)
}
return vi
}()
// In the compiler, we used this to check that we're OK working with ref heads.
// If this isn't present, we'll fail. This is to ensure that older versions of
// OPA can work with policies that we're compiling -- if they don't know ref
@@ -130,6 +159,50 @@ func LoadCapabilitiesVersions() ([]string, error) {
return capabilitiesVersions, nil
}
// MinimumCompatibleVersion returns the minimum compatible OPA version based on
// the built-ins, features, and keywords in c.
func (c *Capabilities) MinimumCompatibleVersion() (string, bool) {
var maxVersion semver.Version
// this is the oldest OPA release that includes capabilities
if err := maxVersion.Set("0.17.0"); err != nil {
panic("unreachable")
}
for _, bi := range c.Builtins {
v, ok := minVersionIndex.Builtins[bi.Name]
if !ok {
return "", false
}
if v.Compare(maxVersion) > 0 {
maxVersion = v
}
}
for _, kw := range c.FutureKeywords {
v, ok := minVersionIndex.Keywords[kw]
if !ok {
return "", false
}
if v.Compare(maxVersion) > 0 {
maxVersion = v
}
}
for _, feat := range c.Features {
v, ok := minVersionIndex.Features[feat]
if !ok {
return "", false
}
if v.Compare(maxVersion) > 0 {
maxVersion = v
}
}
return maxVersion.String(), true
}
// addBuiltinSorted inserts a built-in into c in sorted order. An existing built-in with the same name
// will be overwritten.
func (c *Capabilities) addBuiltinSorted(bi *Builtin) {
+45
View File
@@ -157,6 +157,51 @@ func TestCapabilitiesAddBuiltinSorted(t *testing.T) {
}
}
func TestCapabilitiesMinimumCompatibleVersion(t *testing.T) {
tests := []struct {
note string
module string
version string
}{
{
note: "builtins",
module: `
package x
p { array.reverse([1,2,3]) }
`,
version: "0.36.0",
},
{
note: "keywords",
module: `
package x
import future.keywords.every
`,
version: "0.38.0",
},
{
note: "features",
module: `
package x
import future.keywords.if
p.a.b.c.d if { true }
`,
version: "0.46.0",
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
c := MustCompileModules(map[string]string{"test.rego": tc.module})
minVersion, found := c.Required.MinimumCompatibleVersion()
if !found || minVersion != tc.version {
t.Fatal("expected", tc.version, "but got", minVersion)
}
})
}
}
func findBuiltinIndex(c *Capabilities, name string) int {
for i, bi := range c.Builtins {
if bi.Name == name {
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1173,6 +1173,13 @@ func compile(c *ast.Capabilities, b *bundle.Bundle, dbg debug.Debug, enablePrint
return nil, compiler.Errors
}
minVersion, ok := compiler.Required.MinimumCompatibleVersion()
if !ok {
dbg.Printf("could not determine minimum compatible version!")
} else {
dbg.Printf("minimum compatible version: %v", minVersion)
}
return compiler, nil
}
+78
View File
@@ -0,0 +1,78 @@
package main
import (
"encoding/json"
"os"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/semver"
)
func minVersionIndex() ast.VersionIndex {
index := ast.VersionIndex{
Builtins: map[string]semver.Version{},
Features: map[string]semver.Version{},
Keywords: map[string]semver.Version{},
}
versions, err := ast.LoadCapabilitiesVersions()
if err != nil {
panic(err)
}
for _, v := range versions {
var sv semver.Version
if err := sv.Set(v[1:]); err != nil {
panic(err)
}
c, err := ast.LoadCapabilitiesVersion(v)
if err != nil {
panic(err)
}
for _, bi := range c.Builtins {
exist, ok := index.Builtins[bi.Name]
if !ok || exist.Compare(sv) > 0 {
index.Builtins[bi.Name] = sv
}
}
for _, kw := range c.FutureKeywords {
exist, ok := index.Keywords[kw]
if !ok || exist.Compare(sv) > 0 {
index.Keywords[kw] = sv
}
}
for _, feat := range c.Features {
exist, ok := index.Features[feat]
if !ok || exist.Compare(sv) > 0 {
index.Features[feat] = sv
}
}
}
return index
}
func main() {
fd, err := os.Create(os.Args[1])
if err != nil {
panic(err)
}
enc := json.NewEncoder(fd)
enc.SetIndent("", " ")
vi := minVersionIndex()
if err := enc.Encode(vi); err != nil {
panic(err)
}
if err := fd.Close(); err != nil {
panic(err)
}
}
+1 -1
View File
@@ -16,6 +16,6 @@ func main() {
}
}
// Capabilities + built-in metadata file generation:
//go:generate build/gen-run-go.sh internal/cmd/genopacapabilities/main.go capabilities.json
//go:generate build/gen-run-go.sh internal/cmd/genbuiltinmetadata/main.go builtin_metadata.json
//go:generate build/gen-run-go.sh internal/cmd/genversionindex/main.go ast/version_index.json