Skip to content

Commit

Permalink
Merge pull request #2 from trussio/feature/assert-admission-hooks
Browse files Browse the repository at this point in the history
PR 1: Feature/assert admission hooks
  • Loading branch information
spacetj authored Nov 25, 2019
2 parents 7a95568 + 67f1369 commit fdfad4f
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 73 deletions.
16 changes: 12 additions & 4 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package cmd

import (
"fmt"
"os"

"github.com/anchorageio/anchorctl/utils/kubernetes"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -46,6 +47,11 @@ Kinds of tests include:
return fmt.Errorf("Could not parse testfile flag", err.Error())
}

threshold, err := cmd.Flags().GetFloat64("threshold")
if err != nil {
return fmt.Errorf("Could not parse threshold flag", err.Error())
}

switch kind {

case "kubetest":
Expand All @@ -54,9 +60,9 @@ Kinds of tests include:
return fmt.Errorf("Could not parse kubeconfig flag", err.Error())
}

err = kubernetes.Assert(cmd, kubeconfig, testfile)
err = kubernetes.Assert(cmd, threshold, kubeconfig, testfile)
if err != nil {
return fmt.Errorf("KubeTest errored out", err.Error())
os.Exit(1)
}

}
Expand All @@ -77,6 +83,8 @@ func init() {
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
testCmd.Flags().StringP("file", "f", "", "Input file with the tests.")
testCmd.Flags().StringP("kubeconfig", "c", "~/.kube/config", "Path to kubeconfig file")
testCmd.Flags().StringP("kind", "k", "~/.kube/config", "Path to kubeconfig file")
testCmd.Flags().StringP("kubeconfig", "c", "~/.kube/config", "Path to kubeconfig file.")
testCmd.Flags().StringP("kind", "k", "kubetest", "Kind of test, only kubetest is supported at the moment.")
testCmd.Flags().Float64P("threshold", "t", 80, "Percentage of tests to pass, else return failure.")
testCmd.Flags().IntP("verbose", "v", 3, "Verbosity Level, choose between 1 being Fatal - 7 being .")
}
21 changes: 21 additions & 0 deletions samples/fixtures/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: nginx-labels
name: nginx-labels
namespace: applications
spec:
replicas: 1
selector:
matchLabels:
run: nginx-labels
template:
metadata:
labels:
run: nginx-labels
spec:
containers:
- image: nginx
name: nginx-labels
resources: {}
18 changes: 18 additions & 0 deletions samples/fixtures/loadbalancer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: hello-world
namespace: applications
labels:
app: hello-world
spec:
ports:
- name: 8765-8765
port: 8765
protocol: TCP
targetPort: 8765
selector:
app: hello-world
type: LoadBalancer
status:
loadBalancer: {}
15 changes: 12 additions & 3 deletions samples/kube-test.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
object:
kind: Pod
metadata:
# name: nginx-7db9fccd9b-jlpw4
namespace: default
kind: pod
label:
key: run
value: nginx
file: ./something.yaml

tests:
- type: AssertJSONPath
jsonPath: ".spec.nodeName"
value: "docker-desktop"
- type: AssertValidation
action: "CREATE" # Allowed actions: CREATE, UPDATE, DELETE
# filePath: "/Users/cherukat/go/src/github.com/anchorageio/anchorctl/samples/fixtures/loadbalancer.yaml"
filePath: "./samples/fixtures/loadbalancer.yaml"
expectedError: "Internal error occurred: admission webhook \"webhook.openpolicyagent.org\" denied the request: External Loadbalancers cannot be deployed in this cluster"
- type: AssertMutation
action: "CREATE" # Allowed actions: CREATE, UPDATE
filePath: "./samples/fixtures/deploy.yaml"
jsonPath: ".metadata.labels.function"
value: "wutnow"
58 changes: 49 additions & 9 deletions utils/kubernetes/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package kubernetes

import (
"bytes"
"fmt"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/jsonpath"
)

Expand All @@ -11,6 +13,7 @@ func assertJsonpath(cmd *cobra.Command, object interface{}, path, value string)
jp := jsonpath.New("assertJsonpath")
jp.AllowMissingKeys(true)
err := jp.Parse("{" + path + "}")
passed := true

if err != nil {
cmd.PrintErrln("Cannot parse jsonpath. ", err)
Expand All @@ -19,20 +22,57 @@ func assertJsonpath(cmd *cobra.Command, object interface{}, path, value string)

buf := new(bytes.Buffer)

err = jp.Execute(buf, object)
objects := getSlice(cmd, object)

if err != nil {
cmd.PrintErrln("Error executing jsonpath on object. ", err)
return false, err
for _, i := range objects {

err = jp.Execute(buf, i)
if err != nil {
cmd.PrintErrln("Error executing jsonpath on object. ", err)
passed = false
break
}

if buf.String() == value {
cmd.Println("PASSED: " + path + " " + value)
} else {
cmd.Println("FAILED: expected" + value + " got " + buf.String())
passed = false
}

buf.Reset()

}

return passed, err
}

func assertValidation(client *kubernetes.Clientset, action, filepath, expectedError string) bool {
_, _, err := applyAction(client, filepath, action)

if err != nil && err.Error() == expectedError {
fmt.Println("Passed validation")
return true
}

if buf.String() == value {
cmd.Println("PASSED: " + path + " " + value)
return true, nil
fmt.Println("Failed Validation, got no error, expected error = ", expectedError)
return false

}

func assertMutation(client *kubernetes.Clientset, cmd *cobra.Command, action, filepath, jsonpath, value string) (bool, error) {
_, obj, err := applyAction(client, filepath, action)
if err != nil {
return false, fmt.Errorf("Errored out: ", err)
}

cmd.Println("FAILED: expected" + value + " got " + buf.String())
return assertJsonpath(cmd, obj, jsonpath, value)

return false, nil
}

func assertNetworkPolicies(sourceNamespace, sourceLabelKey, sourceLabelValue, destNamespace, destNamespaceKey, destValue, port, ipaddress string){
// TODO: If ip address is provided, check it returns a 200 with the port
// TODO: Else, create pod in sourceNamespace with sourceLabelKey and sourceLabelValue
// TODO: Create pod in destNamespace with destLabelKey and destLabelValue
// TODO: Exec into source pod, telnet destination pod in the provided IP address
}
146 changes: 128 additions & 18 deletions utils/kubernetes/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,27 @@ package kubernetes

import (
"fmt"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"io/ioutil"
v1 "k8s.io/api/core/v1"
"k8s.io/api/extensions/v1beta1"
"log"

//v1 "k8s.io/api/core/v1"
//"k8s.io/api/extensions/v1beta1"
//"log"
"reflect"

//v1 "k8s.io/api/core/v1"
//"k8s.io/api/extensions/v1beta1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
//"log"
"k8s.io/client-go/kubernetes/scheme"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// getKubeClientSet returns a kubernetes client set which can be used to connect to kubernetes cluster
Expand Down Expand Up @@ -35,30 +51,24 @@ func getKubeClient(incluster bool, filepath string) (*kubernetes.Clientset, erro
return clientset, nil
}

func getObject(client *kubernetes.Clientset, metadata *objectMetadata) (interface{}, error) {

switch metadata.Kind {
func getObject(client *kubernetes.Clientset, kubetest *kubeTest) (interface{}, error) {

case "pod":
var listOptions *metav1.ListOptions

// Use Label if exists
if metadata.Label.Key != "" {
pods, err := listPodsByLabel(client, metadata.Namespace, metadata.Label.Key, metadata.Label.Value)
if err != nil {
return nil, fmt.Errorf("Cannot get pod list with key "+ metadata.Label.Key + " and value " + metadata.Label.Value, err)
}
if kubetest.Metadata.Label.Key != "" {
listOptions = getListOptions(kubetest.Metadata.Label.Key, kubetest.Metadata.Label.Value)
}

if len(pods.Items) < 1 {
return nil, fmt.Errorf("No pods with key "+ metadata.Label.Key + " and value " + metadata.Label.Value, err)
}
switch kubetest.Kind {

return pods.Items[0], nil
case "Pod":
if kubetest.Metadata.Name != "" {
return getPod(client, kubetest.Metadata.Name, kubetest.Metadata.Namespace)
}
return listPods(client, kubetest.Metadata.Namespace, listOptions)

return getPod(client, metadata.Name, metadata.Namespace)

case "deployment":
return getDeployment(client, metadata.Name, metadata.Namespace)
case "Deployment":
return getDeployment(client, kubetest.Metadata.Name, kubetest.Metadata.Namespace)

default:
return nil, fmt.Errorf("Cannot detect object type")
Expand All @@ -81,3 +91,103 @@ func decodeTestFile(filePath string) (*kubeTest, error) {

return kubeTest, nil
}

func getListOptions(key, value string) *metav1.ListOptions {
return &metav1.ListOptions{LabelSelector: key + "=" + value}
}

func getSlice(cmd *cobra.Command, t interface{}) []interface{} {
var slicedInterface []interface{}

switch reflect.TypeOf(t).Kind() {
case reflect.Slice:
s := reflect.ValueOf(t)

for i := 0; i < s.Len(); i++ {
slicedInterface = append(slicedInterface, s.Index(i).Interface())
}

default:
slicedInterface = append(slicedInterface, t)
}

return slicedInterface
}

// ApplyFile mimics kubectl apply -f. Takes in a path to a file and applies that object to the cluster and returns the applied object.
func applyAction(client *kubernetes.Clientset, pathToFile, action string) (*kubeMetadata, interface{}, error) {

decode := scheme.Codecs.UniversalDeserializer().Decode

bytes, err := ioutil.ReadFile(pathToFile)
objectMetadata := &kubeMetadata{}
var object interface{}

if err != nil {
log.Fatal(fmt.Sprintf("Error while reading the file. Err was: %s", err))
}

obj, _, err := decode(bytes, nil, nil)

if err != nil {
log.Fatal(fmt.Sprintf("Error while decoding YAML object. Err was: %s", err))
}

err = yaml.Unmarshal(bytes, &objectMetadata)

if err != nil {
log.Fatal(fmt.Sprintf("Error while unmarshalling Object Metadata. Err was: %s", err))
}

switch obj.(type) {
case *appsv1.Deployment:
deploy := obj.(*appsv1.Deployment)
if action == "CREATE" {
object, err = client.AppsV1().Deployments(objectMetadata.Metadata.Namespace).Create(deploy)
} else if action == "UPDATE" {
object, err = client.AppsV1().Deployments(objectMetadata.Metadata.Namespace).Update(deploy)
} else {
err = client.AppsV1().Deployments(objectMetadata.Metadata.Namespace).Delete(objectMetadata.Metadata.Name, &metav1.DeleteOptions{})
}
case *v1.Pod:
pod := obj.(*v1.Pod)
if action == "CREATE" {
object, err = client.CoreV1().Pods(objectMetadata.Metadata.Namespace).Create(pod)
} else if action == "UPDATE" {
object, err = client.CoreV1().Pods(objectMetadata.Metadata.Namespace).Update(pod)
} else {
err = client.CoreV1().Pods(objectMetadata.Metadata.Namespace).Delete(objectMetadata.Metadata.Name, &metav1.DeleteOptions{})
}
case *v1.Service:
service := obj.(*v1.Service)
if action == "CREATE" {
object, err = client.CoreV1().Services(objectMetadata.Metadata.Namespace).Create(service)
} else if action == "UPDATE" {
object, err = client.CoreV1().Services(objectMetadata.Metadata.Namespace).Update(service)
} else {
err = client.CoreV1().Services(objectMetadata.Metadata.Namespace).Delete(objectMetadata.Metadata.Name, &metav1.DeleteOptions{})
}
case *v1beta1.Ingress:
ingress := obj.(*v1beta1.Ingress)
if action == "CREATE" {
object, err = client.ExtensionsV1beta1().Ingresses(objectMetadata.Metadata.Namespace).Create(ingress)
} else if action == "UPDATE" {
object, err = client.ExtensionsV1beta1().Ingresses(objectMetadata.Metadata.Namespace).Update(ingress)
} else {
err = client.ExtensionsV1beta1().Ingresses(objectMetadata.Metadata.Namespace).Delete(objectMetadata.Metadata.Name, &metav1.DeleteOptions{})
}
case *v1beta1.DaemonSet:
ds := obj.(*v1beta1.DaemonSet)
if action == "CREATE" {
object, err = client.ExtensionsV1beta1().DaemonSets(objectMetadata.Metadata.Namespace).Create(ds)
} else if action == "UPDATE" {
object, err = client.ExtensionsV1beta1().DaemonSets(objectMetadata.Metadata.Namespace).Update(ds)
} else {
err = client.ExtensionsV1beta1().DaemonSets(objectMetadata.Metadata.Namespace).Delete(objectMetadata.Metadata.Name, &metav1.DeleteOptions{})
}
default:
object, err = nil, fmt.Errorf("ApplyAction for kind is not implemented")
}

return objectMetadata, object, err
}
Loading

0 comments on commit fdfad4f

Please sign in to comment.