1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00
kyverno/pkg/testrunner/scenario.go
Shivkumar Dudhani 8c1d79ab28
linter suggestions (#655)
* cleanup phase 1

* linter fixes phase 2
2020-01-24 12:05:53 -08:00

440 lines
13 KiB
Go

package testrunner
import (
"bytes"
"encoding/json"
"flag"
"io/ioutil"
"os"
ospath "path"
"reflect"
"testing"
kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1"
client "github.com/nirmata/kyverno/pkg/dclient"
"github.com/nirmata/kyverno/pkg/engine"
"github.com/nirmata/kyverno/pkg/engine/response"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"github.com/golang/glog"
"gopkg.in/yaml.v2"
apiyaml "k8s.io/apimachinery/pkg/util/yaml"
)
type scenarioT struct {
testCases []scaseT
}
//scase defines input and output for a case
type scaseT struct {
Input sInput `yaml:"input"`
Expected sExpected `yaml:"expected"`
}
//sInput defines input for a test scenario
type sInput struct {
Policy string `yaml:"policy"`
Resource string `yaml:"resource"`
LoadResources []string `yaml:"loadresources,omitempty"`
}
type sExpected struct {
Mutation sMutation `yaml:"mutation,omitempty"`
Validation sValidation `yaml:"validation,omitempty"`
Generation sGeneration `yaml:"generation,omitempty"`
}
type sMutation struct {
// path to the patched resource to be compared with
PatchedResource string `yaml:"patchedresource,omitempty"`
// expected response from the policy engine
PolicyResponse response.PolicyResponse `yaml:"policyresponse"`
}
type sValidation struct {
// expected response from the policy engine
PolicyResponse response.PolicyResponse `yaml:"policyresponse"`
}
type sGeneration struct {
// generated resources
GeneratedResources []kyverno.ResourceSpec `yaml:"generatedResources"`
// expected response from the policy engine
PolicyResponse response.PolicyResponse `yaml:"policyresponse"`
}
//getRelativePath expects a path relative to project and builds the complete path
func getRelativePath(path string) string {
gp := os.Getenv("GOPATH")
ap := ospath.Join(gp, projectPath)
return ospath.Join(ap, path)
}
func loadScenario(t *testing.T, path string) (*scenarioT, error) {
fileBytes, err := loadFile(t, path)
if err != nil {
return nil, err
}
var testCases []scaseT
// load test cases separated by '---'
// each test case defines an input & expected result
scenariosBytes := bytes.Split(fileBytes, []byte("---"))
for _, scenarioBytes := range scenariosBytes {
tc := scaseT{}
if err := yaml.Unmarshal([]byte(scenarioBytes), &tc); err != nil {
t.Errorf("failed to decode test case YAML: %v", err)
continue
}
testCases = append(testCases, tc)
}
scenario := scenarioT{
testCases: testCases,
}
return &scenario, nil
}
// loadFile loads file in byte buffer
func loadFile(t *testing.T, path string) ([]byte, error) {
path = getRelativePath(path)
t.Logf("reading file %s", path)
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil, err
}
return ioutil.ReadFile(path)
}
func runScenario(t *testing.T, s *scenarioT) bool {
for _, tc := range s.testCases {
runTestCase(t, tc)
}
return true
}
func runTestCase(t *testing.T, tc scaseT) bool {
policy := loadPolicy(t, tc.Input.Policy)
if policy == nil {
t.Error("Policy not loaded")
t.FailNow()
}
resource := loadPolicyResource(t, tc.Input.Resource)
if resource == nil {
t.Error("Resources not loaded")
t.FailNow()
}
var er response.EngineResponse
er = engine.Mutate(engine.PolicyContext{Policy: *policy, NewResource: *resource})
t.Log("---Mutation---")
validateResource(t, er.PatchedResource, tc.Expected.Mutation.PatchedResource)
validateResponse(t, er.PolicyResponse, tc.Expected.Mutation.PolicyResponse)
// pass the patched resource from mutate to validate
if len(er.PolicyResponse.Rules) > 0 {
resource = &er.PatchedResource
}
er = engine.Validate(engine.PolicyContext{Policy: *policy, NewResource: *resource})
t.Log("---Validation---")
validateResponse(t, er.PolicyResponse, tc.Expected.Validation.PolicyResponse)
// Generation
if resource.GetKind() == "Namespace" {
// generate mock client if resources are to be loaded
// - create mock client
// - load resources
client := getClient(t, tc.Input.LoadResources)
t.Logf("creating NS %s", resource.GetName())
if err := createNamespace(client, resource); err != nil {
t.Error(err)
} else {
policyContext := engine.PolicyContext{
NewResource: *resource,
Policy: *policy,
Client: client,
}
er = engine.Generate(policyContext)
t.Log(("---Generation---"))
validateResponse(t, er.PolicyResponse, tc.Expected.Generation.PolicyResponse)
// Expected generate resource will be in same namesapces as resource
validateGeneratedResources(t, client, *policy, resource.GetName(), tc.Expected.Generation.GeneratedResources)
}
}
return true
}
func createNamespace(client *client.Client, ns *unstructured.Unstructured) error {
_, err := client.CreateResource("Namespace", "", ns, false)
return err
}
func validateGeneratedResources(t *testing.T, client *client.Client, policy kyverno.ClusterPolicy, namespace string, expected []kyverno.ResourceSpec) {
t.Log("--validate if resources are generated---")
// list of expected generated resources
for _, resource := range expected {
if _, err := client.GetResource(resource.Kind, namespace, resource.Name); err != nil {
t.Errorf("generated resource %s/%s/%s not found. %v", resource.Kind, namespace, resource.Name, err)
}
}
}
func validateResource(t *testing.T, responseResource unstructured.Unstructured, expectedResourceFile string) {
resourcePrint := func(obj unstructured.Unstructured, msg string) {
t.Logf("-----%s----", msg)
if data, err := obj.MarshalJSON(); err == nil {
t.Log(string(data))
}
}
if expectedResourceFile == "" {
t.Log("expected resource file not specified, wont compare resources")
return
}
// load expected resource
expectedResource := loadPolicyResource(t, expectedResourceFile)
if expectedResource == nil {
t.Log("failed to get the expected resource")
return
}
resourcePrint(responseResource, "response resource")
resourcePrint(*expectedResource, "expected resource")
// compare the resources
if !reflect.DeepEqual(responseResource, *expectedResource) {
t.Error("failed: response resource returned does not match expected resource")
return
}
t.Log("success: response resource returned matches expected resource")
}
func validateResponse(t *testing.T, er response.PolicyResponse, expected response.PolicyResponse) {
if reflect.DeepEqual(expected, (response.PolicyResponse{})) {
t.Log("no response expected")
return
}
// cant do deepEquals and the stats will be different, or we nil those fields and then do a comparison
// forcing only the fields that are specified to be comprared
// doing a field by fields comparison will allow us to provied more details logs and granular error reporting
// check policy name is same :P
if er.Policy != expected.Policy {
t.Errorf("Policy name: expected %s, received %s", expected.Policy, er.Policy)
}
// compare resource spec
compareResourceSpec(t, er.Resource, expected.Resource)
// //TODO stats
// if er.RulesAppliedCount != expected.RulesAppliedCount {
// t.Log("RulesAppliedCount: error")
// }
// rules
if len(er.Rules) != len(expected.Rules) {
t.Errorf("rule count error, er.Rules=%d, expected.Rules=%d", len(er.Rules), len(expected.Rules))
return
}
if len(er.Rules) == len(expected.Rules) {
// if there are rules being applied then we compare the rule response
// as the rules are applied in the order defined, the comparison of rules will be in order
for index, r := range expected.Rules {
compareRules(t, er.Rules[index], r)
}
}
}
func compareResourceSpec(t *testing.T, resource response.ResourceSpec, expectedResource response.ResourceSpec) {
// kind
if resource.Kind != expectedResource.Kind {
t.Errorf("kind: expected %s, received %s", expectedResource.Kind, resource.Kind)
}
// //TODO apiVersion
// if resource.APIVersion != expectedResource.APIVersion {
// t.Error("error: apiVersion")
// }
// namespace
if resource.Namespace != expectedResource.Namespace {
t.Errorf("namespace: expected %s, received %s", expectedResource.Namespace, resource.Namespace)
}
// name
if resource.Name != expectedResource.Name {
t.Errorf("name: expected %s, received %s", expectedResource.Name, resource.Name)
}
}
func compareRules(t *testing.T, rule response.RuleResponse, expectedRule response.RuleResponse) {
// name
if rule.Name != expectedRule.Name {
t.Errorf("rule name: expected %s, received %+v", expectedRule.Name, rule.Name)
// as the rule names dont match no need to compare the rest of the information
}
// type
if rule.Type != expectedRule.Type {
t.Errorf("rule type: expected %s, received %s", expectedRule.Type, rule.Type)
}
// message
// compare messages if expected rule message is not empty
if expectedRule.Message != "" && rule.Message != expectedRule.Message {
t.Errorf("rule message: expected %s, received %s", expectedRule.Message, rule.Message)
}
// //TODO patches
// if reflect.DeepEqual(rule.Patches, expectedRule.Patches) {
// t.Log("error: patches")
// }
// success
if rule.Success != expectedRule.Success {
t.Errorf("rule success: expected %t, received %t", expectedRule.Success, rule.Success)
}
}
func loadPolicyResource(t *testing.T, file string) *unstructured.Unstructured {
// expect only one resource to be specified in the YAML
resources := loadResource(t, file)
if resources == nil {
t.Log("no resource specified")
return nil
}
if len(resources) > 1 {
t.Logf("more than one resource specified in the file %s", file)
t.Log("considering the first one for policy application")
}
return resources[0]
}
func getClient(t *testing.T, files []string) *client.Client {
var objects []runtime.Object
if files != nil {
glog.V(4).Infof("loading resources: %v", files)
for _, file := range files {
objects = loadObjects(t, file)
}
}
// create mock client
scheme := runtime.NewScheme()
// mock client expects the resource to be as runtime.Object
c, err := client.NewMockClient(scheme, objects...)
if err != nil {
t.Errorf("failed to create client. %v", err)
return nil
}
// get GVR from GVK
gvrs := getGVRForResources(objects)
c.SetDiscovery(client.NewFakeDiscoveryClient(gvrs))
t.Log("created mock client with pre-loaded resources")
return c
}
func getGVRForResources(objects []runtime.Object) []schema.GroupVersionResource {
var gvrs []schema.GroupVersionResource
for _, obj := range objects {
gvk := obj.GetObjectKind().GroupVersionKind()
gv := gvk.GroupVersion()
// maintain a static map for kind -> Resource
gvr := gv.WithResource(getResourceFromKind(gvk.Kind))
gvrs = append(gvrs, gvr)
}
return gvrs
}
func loadResource(t *testing.T, path string) []*unstructured.Unstructured {
var unstrResources []*unstructured.Unstructured
t.Logf("loading resource from %s", path)
data, err := loadFile(t, path)
if err != nil {
return nil
}
rBytes := bytes.Split(data, []byte("---"))
for _, r := range rBytes {
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode(r, nil, nil)
if err != nil {
t.Logf("failed to decode resource: %v", err)
continue
}
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj)
if err != nil {
t.Logf("failed to unmarshall resource. %v", err)
continue
}
unstr := unstructured.Unstructured{Object: data}
t.Logf("loaded resource %s/%s/%s", unstr.GetKind(), unstr.GetNamespace(), unstr.GetName())
unstrResources = append(unstrResources, &unstr)
}
return unstrResources
}
func loadObjects(t *testing.T, path string) []runtime.Object {
var resources []runtime.Object
t.Logf("loading objects from %s", path)
data, err := loadFile(t, path)
if err != nil {
return nil
}
rBytes := bytes.Split(data, []byte("---"))
for _, r := range rBytes {
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, gvk, err := decode(r, nil, nil)
if err != nil {
t.Logf("failed to decode resource: %v", err)
continue
}
t.Log(gvk)
//TODO: add more details
t.Logf("loaded object %s", gvk.Kind)
resources = append(resources, obj)
}
return resources
}
func loadPolicy(t *testing.T, path string) *kyverno.ClusterPolicy {
t.Logf("loading policy from %s", path)
data, err := loadFile(t, path)
if err != nil {
return nil
}
var policies []*kyverno.ClusterPolicy
pBytes := bytes.Split(data, []byte("---"))
for _, p := range pBytes {
policy := kyverno.ClusterPolicy{}
pBytes, err := apiyaml.ToJSON(p)
if err != nil {
glog.Error(err)
continue
}
if err := json.Unmarshal(pBytes, &policy); err != nil {
t.Logf("failed to marshall polic. %v", err)
continue
}
t.Logf("loaded policy %s", policy.Name)
policies = append(policies, &policy)
}
if len(policies) == 0 {
t.Log("no policies loaded")
return nil
}
if len(policies) > 1 {
t.Log("more than one policy defined, considering first for processing")
}
return policies[0]
}
func testScenario(t *testing.T, path string) {
flag.Set("logtostderr", "true")
// flag.Set("v", "8")
scenario, err := loadScenario(t, path)
if err != nil {
t.Error(err)
return
}
runScenario(t, scenario)
}