mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-31 03:45:17 +00:00
initial commit
This commit is contained in:
parent
3db58d76d2
commit
4692058a4a
7 changed files with 540 additions and 0 deletions
13
examples/mutate/patches/resources/endpoints.yaml
Normal file
13
examples/mutate/patches/resources/endpoints.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: test-endpoint
|
||||
labels:
|
||||
label : test
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: 192.168.10.171
|
||||
ports:
|
||||
- name: secure-connection
|
||||
port: 443
|
||||
protocol: TCP
|
15
examples/mutate/patches/testScenarios.yaml
Normal file
15
examples/mutate/patches/testScenarios.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# input
|
||||
policy: policy-endpoints
|
||||
resource: test-endpoint
|
||||
initResources:
|
||||
# expected
|
||||
mutation:
|
||||
mPatchedResource: test-endpoint
|
||||
reason: Success
|
||||
validation:
|
||||
reason: Success
|
||||
---
|
||||
policy: policy-endpoints
|
||||
resource: test-endpoint
|
||||
initResources:
|
||||
mPatchedResource: test-endpoint
|
156
pkg/testutils/testbundle.go
Normal file
156
pkg/testutils/testbundle.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
ospath "path"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/glog"
|
||||
policytypes "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
)
|
||||
|
||||
func NewTestBundle(path string) *testBundle {
|
||||
return &testBundle{
|
||||
path: path,
|
||||
policies: make(map[string]*policytypes.Policy),
|
||||
resources: make(map[string]*resourceInfo),
|
||||
initResources: make(map[string]*resourceInfo),
|
||||
}
|
||||
}
|
||||
|
||||
func (tb *testBundle) load() error {
|
||||
// check if resources folder is defined
|
||||
rpath := ospath.Join(tb.path, resourcesFolder)
|
||||
_, err := os.Stat(rpath)
|
||||
if os.IsNotExist(err) {
|
||||
glog.Warningf("Resources directory not present at %s", tb.path)
|
||||
return fmt.Errorf("Resources directory not present at %s", tb.path)
|
||||
}
|
||||
|
||||
// check if scenario yaml is defined
|
||||
spath := ospath.Join(tb.path, tScenarioFile)
|
||||
_, err = os.Stat(spath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("Scenario file %s not defined at %s", tScenarioFile, tb.path)
|
||||
}
|
||||
tb.scenarios, err = LoadScenarios(spath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// check if there are any files
|
||||
pYAMLs := getYAMLfiles(tb.path)
|
||||
if len(pYAMLs) == 0 {
|
||||
return fmt.Errorf("No policy yaml found at path %s", tb.path)
|
||||
}
|
||||
for _, p := range pYAMLs {
|
||||
// extract policy
|
||||
policy, err := extractPolicy(p)
|
||||
if err != nil {
|
||||
glog.Errorf("unable to extract policy: %s", err)
|
||||
continue
|
||||
}
|
||||
tb.policies[policy.GetName()] = policy
|
||||
}
|
||||
|
||||
// extract resources
|
||||
rYAMLs := getYAMLfiles(rpath)
|
||||
if len(rYAMLs) == 0 {
|
||||
return fmt.Errorf("No resource yaml found at path %s", rpath)
|
||||
}
|
||||
for _, r := range rYAMLs {
|
||||
resources, err := extractResource(r)
|
||||
if err != nil {
|
||||
glog.Errorf("unable to extract resource: %s", err)
|
||||
}
|
||||
tb.mergeResources(resources)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tb *testBundle) mergeResources(rs map[string]*resourceInfo) {
|
||||
for k, v := range rs {
|
||||
if _, ok := tb.resources[k]; ok {
|
||||
glog.Infof("resource already defined %s ", k)
|
||||
continue
|
||||
}
|
||||
tb.resources[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
type testBundle struct {
|
||||
path string
|
||||
policies map[string]*policytypes.Policy
|
||||
resources map[string]*resourceInfo
|
||||
initResources map[string]*resourceInfo
|
||||
scenarios []*tScenario
|
||||
}
|
||||
|
||||
func (tb *testBundle) run(t *testing.T, testingapplyTest IApplyTest) {
|
||||
// run each scenario
|
||||
for _, ts := range tb.scenarios {
|
||||
// get policy
|
||||
p, ok := tb.policies[ts.Policy]
|
||||
if !ok {
|
||||
glog.Warningf("Policy %s not found", ts.Policy)
|
||||
continue
|
||||
}
|
||||
// get resources
|
||||
r, ok := tb.resources[ts.Resource]
|
||||
if !ok {
|
||||
glog.Warningf("Resource %s not found", ts.Resource)
|
||||
continue
|
||||
}
|
||||
// TODO: handle generate
|
||||
mPatchedResource, mResult, vResult, err := testingapplyTest.applyPolicy(p, r, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// check the expected scenario
|
||||
tb.checkMutationResult(t, ts.Mutation, mPatchedResource, mResult)
|
||||
tb.checkValidationResult(t, ts.Validation, vResult)
|
||||
}
|
||||
}
|
||||
|
||||
func (tb *testBundle) checkValidationResult(t *testing.T, expect *tValidation, vResult result.Result) {
|
||||
if expect == nil {
|
||||
glog.Info("No Validation check defined")
|
||||
return
|
||||
}
|
||||
// compare result
|
||||
// compare reason
|
||||
if len(expect.Reason) > 0 && expect.Reason != vResult.GetReason().String() {
|
||||
t.Error("Reason not matching")
|
||||
}
|
||||
// compare message
|
||||
if len(expect.Message) > 0 && expect.Message != vResult.String() {
|
||||
t.Error(("Message not matching"))
|
||||
}
|
||||
}
|
||||
|
||||
func (tb *testBundle) checkMutationResult(t *testing.T, expect *tMutation, pr *resourceInfo, mResult result.Result) {
|
||||
if expect == nil {
|
||||
glog.Info("No Mutation check defined")
|
||||
return
|
||||
}
|
||||
// get expected patched resource
|
||||
pr, ok := tb.resources[expect.MPatchedResource]
|
||||
if !ok {
|
||||
glog.Warningf("Resource %s not found", expect.MPatchedResource)
|
||||
return
|
||||
}
|
||||
// compare patched resources
|
||||
if !checkMutationRPatches(pr, pr) {
|
||||
t.Error("Patched resources not as expected")
|
||||
}
|
||||
// compare result
|
||||
// compare reason
|
||||
if len(expect.Reason) > 0 && expect.Reason != mResult.GetReason().String() {
|
||||
t.Error("Reason not matching")
|
||||
}
|
||||
// compare message
|
||||
if len(expect.Message) > 0 && expect.Message != mResult.String() {
|
||||
t.Error(("Message not matching"))
|
||||
}
|
||||
}
|
66
pkg/testutils/testsuite.go
Normal file
66
pkg/testutils/testsuite.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
//NewTestSuite returns new test suite
|
||||
func NewTestSuite(t *testing.T, path string) *testSuite {
|
||||
return &testSuite{
|
||||
t: t,
|
||||
path: path,
|
||||
tb: []*testBundle{},
|
||||
}
|
||||
}
|
||||
func (ts *testSuite) runTests() {
|
||||
//TODO : make sure the implementation the interface is pointing to is not nil
|
||||
if ts.applyTest == nil {
|
||||
glog.Error("Apply Test set for the test suite")
|
||||
return
|
||||
}
|
||||
// for each test bundle run the test scenario
|
||||
for _, tb := range ts.tb {
|
||||
tb.run(ts.t, ts.applyTest)
|
||||
}
|
||||
}
|
||||
func (ts *testSuite) setApplyTest(applyTest IApplyTest) {
|
||||
ts.applyTest = applyTest
|
||||
}
|
||||
|
||||
type testSuite struct {
|
||||
t *testing.T
|
||||
path string
|
||||
tb []*testBundle
|
||||
applyTest IApplyTest
|
||||
}
|
||||
|
||||
func (ts *testSuite) buildTestSuite() error {
|
||||
// loading test bundles for test suite
|
||||
fmt.Println(ts.path)
|
||||
err := filepath.Walk(ts.path, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() {
|
||||
// check if there are resources dir and policies yaml
|
||||
tb := NewTestBundle(path)
|
||||
if tb != nil {
|
||||
// load resources
|
||||
err := tb.load()
|
||||
if err != nil {
|
||||
// glog.Error(err)
|
||||
return nil
|
||||
}
|
||||
ts.tb = append(ts.tb, tb)
|
||||
// fmt.Println(path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ts.t.Fatal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
54
pkg/testutils/testutils.go
Normal file
54
pkg/testutils/testutils.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
ospath "path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Load policy & resource files
|
||||
// engine pass the (policy, resource)
|
||||
// check the expected response
|
||||
|
||||
const examplesPath string = "examples"
|
||||
const resourcesFolder string = "resources"
|
||||
const tScenarioFile string = "testScenarios.yaml"
|
||||
|
||||
//LoadTestSuite reads the resource, policy and scenario files
|
||||
func LoadTestSuite(t *testing.T, path string) *testSuite {
|
||||
// gp := os.Getenv("GOPATH")
|
||||
// ap := ospath.Join(gp, "src/github.com/nirmata/kyverno")
|
||||
// build test suite
|
||||
// each suite contains test bundles for test sceanrios
|
||||
// ts := NewTestSuite(t, ospath.Join(ap, examplesPath))
|
||||
ts := NewTestSuite(t, path)
|
||||
ts.buildTestSuite()
|
||||
return ts
|
||||
}
|
||||
|
||||
func checkMutationRPatches(er *resourceInfo, pr *resourceInfo) bool {
|
||||
if !er.isSame(*pr) {
|
||||
getResourceYAML(pr.rawResource)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getYAMLfiles(path string) (yamls []string) {
|
||||
fileInfo, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, file := range fileInfo {
|
||||
fmt.Println(filepath.Ext(file.Name()))
|
||||
if file.Name() == tScenarioFile {
|
||||
continue
|
||||
}
|
||||
if filepath.Ext(file.Name()) == ".yml" || filepath.Ext(file.Name()) == ".yaml" {
|
||||
yamls = append(yamls, ospath.Join(path, file.Name()))
|
||||
}
|
||||
}
|
||||
return yamls
|
||||
}
|
20
pkg/testutils/testutils_test.go
Normal file
20
pkg/testutils/testutils_test.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
file := "/Users/shiv/nirmata/code/go/src/github.com/nirmata/kyverno/examples/mutate/patches"
|
||||
ts := LoadTestSuite(t, file)
|
||||
// policy application logic
|
||||
tp := &testPolicy{}
|
||||
ts.setApplyTest(tp)
|
||||
// run the tests for each test bundle
|
||||
ts.runTests()
|
||||
if ts != nil {
|
||||
fmt.Println("Done building the test bundles")
|
||||
}
|
||||
// run the tests against the policy engine
|
||||
}
|
216
pkg/testutils/utils.go
Normal file
216
pkg/testutils/utils.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/golang/glog"
|
||||
policytypes "github.com/nirmata/kyverno/pkg/apis/policy/v1alpha1"
|
||||
client "github.com/nirmata/kyverno/pkg/dclient"
|
||||
"github.com/nirmata/kyverno/pkg/engine"
|
||||
"github.com/nirmata/kyverno/pkg/result"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultYamlSeparator = "---"
|
||||
)
|
||||
|
||||
func loadFile(fileDir string) ([]byte, error) {
|
||||
if _, err := os.Stat(fileDir); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.ReadFile(fileDir)
|
||||
}
|
||||
|
||||
func extractPolicy(fileDir string) (*policytypes.Policy, error) {
|
||||
policy := &policytypes.Policy{}
|
||||
|
||||
file, err := loadFile(fileDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load file: %v", err)
|
||||
}
|
||||
|
||||
policyBytes, err := kyaml.ToJSON(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(policyBytes, policy); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode policy %s, err: %v", policy.Name, err)
|
||||
}
|
||||
|
||||
if policy.TypeMeta.Kind != "Policy" {
|
||||
return nil, fmt.Errorf("failed to parse policy")
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
type resourceInfo struct {
|
||||
rawResource []byte
|
||||
gvk *metav1.GroupVersionKind
|
||||
}
|
||||
|
||||
func (ri resourceInfo) isSame(other resourceInfo) bool {
|
||||
// compare gvk
|
||||
if ri.gvk != other.gvk {
|
||||
return false
|
||||
}
|
||||
// compare rawResource
|
||||
return bytes.Equal(ri.rawResource, other.rawResource)
|
||||
}
|
||||
|
||||
func getResourceYAML(d []byte) {
|
||||
fmt.Println(string(d))
|
||||
// convert json to yaml
|
||||
// print the result for reference
|
||||
// can be used as a dry run the get the expected result
|
||||
}
|
||||
|
||||
func extractResourceRaw(d []byte) (string, *resourceInfo) {
|
||||
decode := scheme.Codecs.UniversalDeserializer().Decode
|
||||
obj, gvk, err := decode([]byte(d), nil, nil)
|
||||
if err != nil {
|
||||
glog.Warningf("Error while decoding YAML object, err: %s\n", err)
|
||||
return "", nil
|
||||
}
|
||||
raw, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
glog.Warningf("Error while marshalling manifest, err: %v\n", err)
|
||||
return "", nil
|
||||
}
|
||||
gvkInfo := &metav1.GroupVersionKind{Group: gvk.Group, Version: gvk.Version, Kind: gvk.Kind}
|
||||
rn := ParseNameFromObject(raw)
|
||||
rns := ParseNamespaceFromObject(raw)
|
||||
if rns != "" {
|
||||
rn = rns + "/" + rn
|
||||
}
|
||||
return rn, &resourceInfo{rawResource: raw, gvk: gvkInfo}
|
||||
}
|
||||
|
||||
func extractResource(resource string) (map[string]*resourceInfo, error) {
|
||||
resources := make(map[string]*resourceInfo)
|
||||
data, err := loadFile(resource)
|
||||
if err != nil {
|
||||
glog.Warningf("Error while loading file: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
dd := bytes.Split(data, []byte(defaultYamlSeparator))
|
||||
for _, d := range dd {
|
||||
rn, r := extractResourceRaw(d)
|
||||
resources[rn] = r
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
//ParseNameFromObject extracts resource name from JSON obj
|
||||
func ParseNameFromObject(bytes []byte) string {
|
||||
var objectJSON map[string]interface{}
|
||||
json.Unmarshal(bytes, &objectJSON)
|
||||
|
||||
meta := objectJSON["metadata"].(map[string]interface{})
|
||||
|
||||
if name, ok := meta["name"].(string); ok {
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParseNamespaceFromObject extracts the namespace from the JSON obj
|
||||
func ParseNamespaceFromObject(bytes []byte) string {
|
||||
var objectJSON map[string]interface{}
|
||||
json.Unmarshal(bytes, &objectJSON)
|
||||
|
||||
meta := objectJSON["metadata"].(map[string]interface{})
|
||||
|
||||
if namespace, ok := meta["namespace"].(string); ok {
|
||||
return namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type IApplyTest interface {
|
||||
applyPolicy(policy *policytypes.Policy, resource *resourceInfo, client *client.Client) (*resourceInfo, result.Result, result.Result, error)
|
||||
}
|
||||
|
||||
type testPolicy struct {
|
||||
}
|
||||
|
||||
func (tp *testPolicy) applyPolicy(policy *policytypes.Policy, resource *resourceInfo, client *client.Client) (*resourceInfo, result.Result, result.Result, error) {
|
||||
// apply policy on the trigger resource
|
||||
// Mutate
|
||||
var vResult result.Result
|
||||
var patchedResource []byte
|
||||
mPatches, mResult := engine.Mutate(*policy, resource.rawResource, *resource.gvk)
|
||||
// TODO: only validate if there are no errors in mutate, why?
|
||||
err := mResult.ToError()
|
||||
if err == nil && len(mPatches) != 0 {
|
||||
patchedResource, err = engine.ApplyPatches(resource.rawResource, mPatches)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// Validate
|
||||
vResult = engine.Validate(*policy, patchedResource, *resource.gvk)
|
||||
}
|
||||
// Generate
|
||||
if client == nil {
|
||||
glog.Warning("Client is required to test generate")
|
||||
}
|
||||
// transform the patched Resource into resource Info
|
||||
_, ri := extractResourceRaw(patchedResource)
|
||||
// return the results
|
||||
return ri, mResult, vResult, nil
|
||||
// TODO: merge the results for mutation and validation
|
||||
}
|
||||
|
||||
type tScenario struct {
|
||||
Policy string `yaml:"policy"`
|
||||
Resource string `yaml:"resource"`
|
||||
InitResources []string `yaml:"initResources,omitempty"`
|
||||
Mutation *tMutation `yaml:"mutation,omitempty"`
|
||||
Validation *tValidation `yaml:"validation,omitempty"`
|
||||
}
|
||||
|
||||
type tValidation struct {
|
||||
Reason string `yaml:"reason,omitempty"`
|
||||
Message string `yaml:"message,omitempty"`
|
||||
Error string `yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
type tMutation struct {
|
||||
MPatchedResource string `yaml:"mPatchedResource,omitempty"`
|
||||
Reason string `yaml:"reason,omitempty"`
|
||||
Message string `yaml:"message,omitempty"`
|
||||
Error string `yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
func LoadScenarios(file string) ([]*tScenario, error) {
|
||||
ts := []*tScenario{}
|
||||
// read the file
|
||||
data, err := loadFile(file)
|
||||
if err != nil {
|
||||
glog.Warningf("Error while loading file: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
dd := bytes.Split(data, []byte(defaultYamlSeparator))
|
||||
for _, d := range dd {
|
||||
s := &tScenario{}
|
||||
err := yaml.Unmarshal([]byte(d), s)
|
||||
if err != nil {
|
||||
glog.Warningf("Error while decoding YAML object, err: %s", err)
|
||||
continue
|
||||
}
|
||||
fmt.Println(s.Policy)
|
||||
fmt.Println(s.Resource)
|
||||
ts = append(ts, s)
|
||||
}
|
||||
return ts, nil
|
||||
}
|
Loading…
Add table
Reference in a new issue