Skip to content

Commit

Permalink
feat: use opa for validate and patch
Browse files Browse the repository at this point in the history
  • Loading branch information
mxab committed Feb 25, 2023
1 parent 7ca51d1 commit 2d5ff7d
Show file tree
Hide file tree
Showing 10 changed files with 481 additions and 0 deletions.
77 changes: 77 additions & 0 deletions admissionctrl/mutator/opa_json_patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package mutator

import (
"context"
"encoding/json"

jsonpatch "github.com/evanphx/json-patch"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/api"
"github.com/mxab/nacp/admissionctrl/opa"
)

type OpaJsonPatchMutator struct {
ruleSets []*opa.OpaRuleSet
logger hclog.Logger
}

func (j *OpaJsonPatchMutator) Mutate(job *api.Job) (out *api.Job, warnings []error, err error) {

ctx := context.TODO()
for _, ruleSet := range j.ruleSets {
result, err := ruleSet.Eval(ctx, job)
if err != nil {
return nil, nil, err
}
patchData, ok := result[0].Bindings["patch"].([]interface{})
patchJSON, err := json.Marshal(patchData)
if err != nil {
return nil, nil, err
}

if ok {
patch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil {
return nil, nil, err
}
j.logger.Debug("Got patch fom ruleset %s, patch: %v", ruleSet.Name(), patchJSON)
jobJson, err := json.Marshal(job)
if err != nil {
return nil, nil, err
}

patched, err := patch.Apply(jobJson)
if err != nil {
return nil, nil, err
}
var patchedJob api.Job
err = json.Unmarshal(patched, &patchedJob)
if err != nil {
return nil, nil, err
}
job = &patchedJob

}

}

return job, nil, nil
}
func (j *OpaJsonPatchMutator) Name() string {
return "jsonpatch"
}

func NewOpaJsonPatchMutator(rules []opa.OpaQueryAndModule, logger hclog.Logger) (*OpaJsonPatchMutator, error) {

ctx := context.TODO()
// read the policy file
ruleSets, err := opa.CreateOpaRuleSet(rules, ctx)
if err != nil {
return nil, err
}
return &OpaJsonPatchMutator{
ruleSets: ruleSets,
logger: logger,
}, nil

}
74 changes: 74 additions & 0 deletions admissionctrl/mutator/opa_json_patch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package mutator

import (
"testing"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/api"
"github.com/mxab/nacp/admissionctrl/opa"
"github.com/mxab/nacp/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestJSONPatcher_Mutate(t *testing.T) {
type args struct {
job *api.Job
}
tests := []struct {
name string
j *OpaJsonPatchMutator
args args
wantOut *api.Job
wantWarnings []error
wantErr bool
}{
{
name: "nothing",
j: newMutator(t, []opa.OpaQueryAndModule{}),

args: args{
job: &api.Job{},
},
wantOut: &api.Job{},
wantWarnings: []error{},
wantErr: false,
},
{
name: "hello world",
j: newMutator(t, []opa.OpaQueryAndModule{
{
Filename: testutil.Filepath(t, "opa/mutators/hello_world_meta.rego"),
Query: "patch = data.hello_world_meta.patch",
},
}),

args: args{
job: &api.Job{},
},
wantOut: &api.Job{
Meta: map[string]string{
"hello": "world",
},
},
wantWarnings: []error{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotOut, gotWarnings, err := tt.j.Mutate(tt.args.job)
require.Equal(t, tt.wantErr, err != nil, "JSONPatcher.Mutate() error = %v, wantErr %v", err, tt.wantErr)
assert.Empty(t, gotWarnings)
assert.Equal(t, tt.wantOut, gotOut)

})
}
}

func newMutator(t *testing.T, rules []opa.OpaQueryAndModule) *OpaJsonPatchMutator {
t.Helper()
m, err := NewOpaJsonPatchMutator(rules, hclog.NewNullLogger())
require.NoError(t, err)
return m
}
56 changes: 56 additions & 0 deletions admissionctrl/opa/opa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package opa

import (
"context"
"os"

"github.com/hashicorp/nomad/api"
"github.com/open-policy-agent/opa/rego"
)

type OpaQueryAndModule struct {
Filename string
Query string
}

type OpaRuleSet struct {
rule OpaQueryAndModule
prepared *rego.PreparedEvalQuery
}

func CreateOpaRuleSet(rules []OpaQueryAndModule, ctx context.Context) ([]*OpaRuleSet, error) {
var ruleSets []*OpaRuleSet

for _, rule := range rules {

module, err := os.ReadFile(rule.Filename)
if err != nil {
return nil, err
}

query, err := rego.New(
rego.Query(rule.Query),
rego.Module(rule.Filename, string(module)),
).PrepareForEval(ctx)

if err != nil {
return nil, err
}
ruleSets = append(ruleSets, &OpaRuleSet{
rule: rule,
prepared: &query,
},
)

}
return ruleSets, nil
}

func (r *OpaRuleSet) Eval(ctx context.Context, job *api.Job) (rego.ResultSet, error) {

results, err := r.prepared.Eval(ctx, rego.EvalInput(job))
return results, err
}
func (r *OpaRuleSet) Name() string {
return r.rule.Filename
}
30 changes: 30 additions & 0 deletions testdata/opa/mutators/hello_world_meta.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package hello_world_meta


patch[operation] {

not input.Meta
operation := {
"op": "add",
"path": "/Meta",
"value": {}
}
}
patch[operation] {

is_null(input.Meta)
operation := {
"op": "add",
"path": "/Meta",
"value": {}
}
}
patch[operation] {

not input.Meta.hello
operation := {
"op": "add",
"path": "/Meta/hello",
"value": "world"
}
}
68 changes: 68 additions & 0 deletions testdata/opa/mutators/hello_world_meta_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package hello_world_meta_test

import data.hello_world_meta.patch

import future.keywords

test_hello_world if {
e := patch with input as {
"ID": "my-job",
"Meta": {},
}
e[{
"op": "add",
"path": "/Meta/hello",
"value": "world"
}]

}

test_hello_world_add_meta if {
e := patch with input as {
"ID": "my-job"
}
count(e) == 2
trace(sprintf("patch: %v", [e]))

e == {
{
"op": "add",
"path": "/Meta",
"value": {}
},
{
"op": "add",
"path": "/Meta/hello",
"value": "world"
}
}
}
test_hello_world_add_meta_if_meta_null if {
e := patch with input as {
"ID": "my-job",
"Meta": null
}
count(e) == 2
trace(sprintf("patch: %v", [e]))

e == {
{
"op": "add",
"path": "/Meta",
"value": {}
},
{
"op": "add",
"path": "/Meta/hello",
"value": "world"
}
}
}
test_hello_world_no_code_if_exists if {
e := patch with input as {
"ID": "my-job",
"Meta": {"hello": "world"}
}
count(e) == 0

}
28 changes: 28 additions & 0 deletions testdata/opa/validators/costcenter_meta.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

package costcenter_meta


import future.keywords.contains
import future.keywords.if

# This definition checks if the costcenter label is not provided. Each rule definition
# contributes to the set of error messages.
errors contains msg if {
# The `not` keyword turns an undefined statement into a true statement. If any
# of the keys are missing, this statement will be true.


not input.Meta.costcenter
trace("Costcenter code is missing")

msg := "Every job must have a costcenter metadata label"
}

# This definition checks if the costcenter label is formatted appropriately. Each rule
# definition contributes to the set of error messages.
errors contains msg if {
value := input.Meta.costcenter

not startswith(value, "cccode-")
msg := sprintf("Costcenter code must start with `cccode-`; found `%v`", [value])
}
28 changes: 28 additions & 0 deletions testdata/opa/validators/costcenter_meta_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package costcenter_meta_test
import data.costcenter_meta.errors

import future.keywords

test_missing_costcenter if {
count(errors) == 1 with input as {
"ID": "my-job",
"Meta": {},
}

}

test_costcenter_prefix_wrong if {
count(errors)==1 with input as {
"ID": "my-job",
"Meta": {"costcenter": "my-costcenter"},
}

}

test_costcenter_correct if {
count(errors) == 0 with input as {
"ID": "my-job",
"Meta": {"costcenter": "cccode-my-costcenter"},
}

}
9 changes: 9 additions & 0 deletions testdata/opa/validators/errors.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dummy

errors[errMsg] {
errMsg := "This is a error message"
}

warnings[warnMsg] {
warnMsg := "This is a warning message"
}
Loading

0 comments on commit 2d5ff7d

Please sign in to comment.