diff --git a/kubernetes/e2e_test/test_json/apiservice-fail.json b/kubernetes/e2e_test/test_json/apiservice-fail.json new file mode 100644 index 0000000000..dc8babacf7 --- /dev/null +++ b/kubernetes/e2e_test/test_json/apiservice-fail.json @@ -0,0 +1,31 @@ +{ + "apiVersion": "apiregistration.k8s.io/v1", + "kind": "APIService", + "metadata": { + "creationTimestamp": "2019-09-30T03:42:56Z", + "labels": { + "kube-aggregator.kubernetes.io/automanaged": "onstart" + }, + "name": "v1.", + "resourceVersion": "4", + "selfLink": "/apis/apiregistration.k8s.io/v1/apiservices/v1.", + "uid": "ed59c555-e1bb-44e2-af20-1bf51117a4b3" + }, + "spec": { + "groupPriorityMinimum": 18000, + "service": null, + "version": "v1", + "versionPriority": 1 + }, + "status": { + "conditions": [ + { + "lastTransitionTime": "2019-09-30T03:42:56Z", + "message": "Local APIServices are always available", + "reason": "Local", + "status": "True", + "type": "Available" + } + ] + } +} diff --git a/kubernetes/e2e_test/test_json/apiservice.json b/kubernetes/e2e_test/test_json/apiservice.json new file mode 100644 index 0000000000..a6cc708599 --- /dev/null +++ b/kubernetes/e2e_test/test_json/apiservice.json @@ -0,0 +1,31 @@ +{ + "apiVersion": "apiregistration.k8s.io/v1", + "kind": "APIService", + "metadata": { + "creationTimestamp": "2019-09-30T03:42:56Z", + "labels": { + "kube-aggregator.kubernetes.io/automanaged": "onstart" + }, + "name": "v1.", + "resourceVersion": "4", + "selfLink": "/apis/apiregistration.k8s.io/v1/apiservices/v1.", + "uid": "ed59c555-e1bb-44e2-af20-1bf51117a4b3" + }, + "spec": { + "groupPriorityMinimum": 18000, + "service": "DummyNotNull", + "version": "v1", + "versionPriority": 1 + }, + "status": { + "conditions": [ + { + "lastTransitionTime": "2019-09-30T03:42:56Z", + "message": "Local APIServices are always available", + "reason": "Local", + "status": "True", + "type": "Available" + } + ] + } +} diff --git a/kubernetes/e2e_test/test_json/bad-data.json b/kubernetes/e2e_test/test_json/bad-data.json new file mode 100644 index 0000000000..994e08af68 --- /dev/null +++ b/kubernetes/e2e_test/test_json/bad-data.json @@ -0,0 +1,5 @@ +{ + "apiVersion": "v1bad", + "kind": "Pod", + "TestInfo": 12345 +} diff --git a/kubernetes/e2e_test/test_json/deployment.json b/kubernetes/e2e_test/test_json/deployment.json new file mode 100644 index 0000000000..bb76e3ccbc --- /dev/null +++ b/kubernetes/e2e_test/test_json/deployment.json @@ -0,0 +1,88 @@ +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "1" + }, + "creationTimestamp": "2019-10-01T04:51:46Z", + "generation": 1, + "labels": { + "run": "curl3" + }, + "name": "curl3", + "namespace": "default", + "resourceVersion": "469654", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/curl3", + "uid": "6e9bb86b-4e2a-4567-9519-393a3d7245da" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 1, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "run": "curl3" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "run": "curl3" + } + }, + "spec": { + "containers": [ + { + "image": "radial/busyboxplus:curl", + "imagePullPolicy": "IfNotPresent", + "name": "curl3", + "resources": {}, + "stdin": true, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "tty": true + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "conditions": [ + { + "lastTransitionTime": "2019-10-01T04:51:46Z", + "lastUpdateTime": "2019-10-01T04:51:47Z", + "message": "ReplicaSet \"curl3-748c7587cf\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2019-10-14T21:08:27Z", + "lastUpdateTime": "2019-10-14T21:08:27Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1, + "updatedReplicas": 1 + } +} diff --git a/kubernetes/e2e_test/test_json/namespace.json b/kubernetes/e2e_test/test_json/namespace.json new file mode 100644 index 0000000000..f74d9b2d44 --- /dev/null +++ b/kubernetes/e2e_test/test_json/namespace.json @@ -0,0 +1,19 @@ +{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "creationTimestamp": "2019-09-30T03:43:00Z", + "name": "default", + "resourceVersion": "148", + "selfLink": "/api/v1/namespaces/default", + "uid": "96dcb214-c21d-4858-a7d4-b90d09cf9174" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} diff --git a/kubernetes/e2e_test/test_json/node.json b/kubernetes/e2e_test/test_json/node.json new file mode 100644 index 0000000000..4f9a4d37de --- /dev/null +++ b/kubernetes/e2e_test/test_json/node.json @@ -0,0 +1,244 @@ +{ + "apiVersion": "v1", + "kind": "Node", + "metadata": { + "annotations": { + "kubeadm.alpha.kubernetes.io/cri-socket": "/var/run/dockershim.sock", + "node.alpha.kubernetes.io/ttl": "0", + "volumes.kubernetes.io/controller-managed-attach-detach": "true" + }, + "creationTimestamp": "2019-09-30T03:42:56Z", + "labels": { + "beta.kubernetes.io/arch": "amd64", + "beta.kubernetes.io/os": "linux", + "kubernetes.io/arch": "amd64", + "kubernetes.io/hostname": "minikube", + "kubernetes.io/os": "linux", + "node-role.kubernetes.io/master": "" + }, + "name": "minikube", + "resourceVersion": "581498", + "selfLink": "/api/v1/nodes/minikube", + "uid": "d5067f63-91a8-444b-8171-43df8486e50a" + }, + "spec": {}, + "status": { + "addresses": [ + { + "address": "192.168.64.3", + "type": "InternalIP" + }, + { + "address": "minikube", + "type": "Hostname" + } + ], + "allocatable": { + "cpu": "2", + "ephemeral-storage": "15625027559", + "hugepages-2Mi": "0", + "memory": "1887148Ki", + "pods": "110" + }, + "capacity": { + "cpu": "2", + "ephemeral-storage": "16954240Ki", + "hugepages-2Mi": "0", + "memory": "1989548Ki", + "pods": "110" + }, + "conditions": [ + { + "lastHeartbeatTime": "2019-10-18T10:51:37Z", + "lastTransitionTime": "2019-09-30T03:42:53Z", + "message": "kubelet has sufficient memory available", + "reason": "KubeletHasSufficientMemory", + "status": "False", + "type": "MemoryPressure" + }, + { + "lastHeartbeatTime": "2019-10-18T10:51:37Z", + "lastTransitionTime": "2019-09-30T03:42:53Z", + "message": "kubelet has no disk pressure", + "reason": "KubeletHasNoDiskPressure", + "status": "False", + "type": "DiskPressure" + }, + { + "lastHeartbeatTime": "2019-10-18T10:51:37Z", + "lastTransitionTime": "2019-09-30T03:42:53Z", + "message": "kubelet has sufficient PID available", + "reason": "KubeletHasSufficientPID", + "status": "False", + "type": "PIDPressure" + }, + { + "lastHeartbeatTime": "2019-10-18T10:51:37Z", + "lastTransitionTime": "2019-09-30T03:42:53Z", + "message": "kubelet is posting ready status", + "reason": "KubeletReady", + "status": "True", + "type": "Ready" + } + ], + "daemonEndpoints": { + "kubeletEndpoint": { + "Port": 10250 + } + }, + "images": [ + { + "names": [ + "k8s.gcr.io/etcd@sha256:12c2c5e5731c3bcd56e6f1c05c0f9198b6f06793fa7fca2fb43aab9622dc4afa", + "k8s.gcr.io/etcd:3.3.15-0" + ], + "sizeBytes": 246640776 + }, + { + "names": [ + "k8s.gcr.io/kube-apiserver@sha256:f4168527c91289da2708f62ae729fdde5fb484167dd05ffbb7ab666f60de96cd", + "k8s.gcr.io/kube-apiserver:v1.16.0" + ], + "sizeBytes": 217066846 + }, + { + "names": [ + "k8s.gcr.io/kube-controller-manager@sha256:c156a05ee9d40e3ca2ebf9337f38a10558c1fc6c9124006f128a82e6c38cdf3e", + "k8s.gcr.io/kube-controller-manager:v1.16.0" + ], + "sizeBytes": 163310046 + }, + { + "names": [ + "debian@sha256:e25b64a9cf82c72080074d6b1bba7329cdd752d51574971fd37731ed164f3345", + "debian:buster" + ], + "sizeBytes": 114048240 + }, + { + "names": [ + "nginx@sha256:e3456c851a152494c3e4ff5fcc26f240206abac0c9d794affb40e0714846c451", + "nginx:1.7.9" + ], + "sizeBytes": 91664166 + }, + { + "names": [ + "k8s.gcr.io/kube-scheduler@sha256:094023ab9cd02059eb0295d234ff9ea321e0e22e4813986d7f1a1ac4dc1990d0", + "k8s.gcr.io/kube-scheduler:v1.16.0" + ], + "sizeBytes": 87265822 + }, + { + "names": [ + "k8s.gcr.io/kube-proxy@sha256:e7f0f8e320cfeeaafdc9c0cb8e23f51e542fa1d955ae39c8131a0531ba72c794", + "k8s.gcr.io/kube-proxy:v1.16.0" + ], + "sizeBytes": 86056924 + }, + { + "names": [ + "kubernetesui/dashboard:v2.0.0-beta4" + ], + "sizeBytes": 84034786 + }, + { + "names": [ + "k8s.gcr.io/kube-addon-manager:v9.0.2" + ], + "sizeBytes": 83076028 + }, + { + "names": [ + "gcr.io/k8s-minikube/storage-provisioner:v1.8.1" + ], + "sizeBytes": 80815640 + }, + { + "names": [ + "haproxytech/kubernetes-ingress@sha256:d21b0c6ada1672a4a5f7e1a971552f209830d7b235ac77220ccbbdf544de08a0", + "haproxytech/kubernetes-ingress:latest" + ], + "sizeBytes": 69319484 + }, + { + "names": [ + "haproxytech/kubernetes-ingress@sha256:288bfa5270fb79c25c2252e70d097c08508a49f3a601da56405cfc621d4ac7d6" + ], + "sizeBytes": 69314720 + }, + { + "names": [ + "k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13" + ], + "sizeBytes": 51157394 + }, + { + "names": [ + "k8s.gcr.io/coredns@sha256:12eb885b8685b1b13a04ecf5c23bc809c2e57917252fd7b0be9e9c00644e8ee5", + "k8s.gcr.io/coredns:1.6.2" + ], + "sizeBytes": 44100963 + }, + { + "names": [ + "k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13" + ], + "sizeBytes": 42852039 + }, + { + "names": [ + "k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13" + ], + "sizeBytes": 41372492 + }, + { + "names": [ + "kubernetesui/metrics-scraper@sha256:35fcae4fd9232a541a8cb08f2853117ba7231750b75c2cb3b6a58a2aaa57f878", + "kubernetesui/metrics-scraper:v1.0.1" + ], + "sizeBytes": 40101504 + }, + { + "names": [ + "gcr.io/google_containers/defaultbackend@sha256:ee3aa1187023d0197e3277833f19d9ef7df26cee805fef32663e06c7412239f9", + "gcr.io/google_containers/defaultbackend:1.0" + ], + "sizeBytes": 7513643 + }, + { + "names": [ + "jordi/ab@sha256:e73ba5f38be047448267bcfbab191bbea88017150578ef2dd3b0942a0e0d6a56", + "jordi/ab:latest" + ], + "sizeBytes": 5164534 + }, + { + "names": [ + "radial/busyboxplus@sha256:a68c05ab1112fd90ad7b14985a48520e9d26dbbe00cb9c09aa79fdc0ef46b372", + "radial/busyboxplus:curl" + ], + "sizeBytes": 4233788 + }, + { + "names": [ + "k8s.gcr.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea", + "k8s.gcr.io/pause:3.1" + ], + "sizeBytes": 742472 + } + ], + "nodeInfo": { + "architecture": "amd64", + "bootID": "9dc58950-881f-4fa6-a3c6-8ee693ca52f3", + "containerRuntimeVersion": "docker://18.9.9", + "kernelVersion": "4.15.0", + "kubeProxyVersion": "v1.16.0", + "kubeletVersion": "v1.16.0", + "machineID": "c2944d3f6dfd40be90b490e9d012f9d0", + "operatingSystem": "linux", + "osImage": "Buildroot 2018.05.3", + "systemUUID": "288511E9-0000-0000-A257-ACDE48001122" + } + } +} diff --git a/kubernetes/e2e_test/test_json/pod-list.json b/kubernetes/e2e_test/test_json/pod-list.json new file mode 100644 index 0000000000..938833fd6d --- /dev/null +++ b/kubernetes/e2e_test/test_json/pod-list.json @@ -0,0 +1,293 @@ +{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2019-10-14T21:08:25Z", + "generateName": "curl3-748c7587cf-", + "labels": { + "pod-template-hash": "748c7587cf", + "run": "curl3" + }, + "name": "curl3-748c7587cf-stfgk", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "curl3-748c7587cf", + "uid": "1eb825e8-8983-492c-949e-946fe31612ce" + } + ], + "resourceVersion": "469651", + "selfLink": "/api/v1/namespaces/default/pods/curl3-748c7587cf-stfgk", + "uid": "8c30ae29-3fe6-4f5f-ace8-45493724fea5" + }, + "spec": { + "containers": [ + { + "image": "radial/busyboxplus:curl", + "imagePullPolicy": "IfNotPresent", + "name": "curl3", + "resources": {}, + "stdin": true, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "tty": true, + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-ljqzq", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-ljqzq", + "secret": { + "defaultMode": 420, + "secretName": "default-token-ljqzq" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-14T21:08:25Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-14T21:08:27Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-14T21:08:27Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-14T21:08:25Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://089966a149a1e5209570bfd4d7bc3d7f9142a1f5ef69adaf12fe2cd30254e3c4", + "image": "radial/busyboxplus:curl", + "imageID": "docker-pullable://radial/busyboxplus@sha256:a68c05ab1112fd90ad7b14985a48520e9d26dbbe00cb9c09aa79fdc0ef46b372", + "lastState": {}, + "name": "curl3", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2019-10-14T21:08:26Z" + } + } + } + ], + "hostIP": "192.168.64.3", + "phase": "Running", + "podIP": "172.17.0.6", + "podIPs": [ + { + "ip": "172.17.0.6" + } + ], + "qosClass": "BestEffort", + "startTime": "2019-10-14T21:08:25Z" + } + }, + { + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2019-10-01T04:54:32Z", + "generateName": "deb-74cc89dbd7-", + "labels": { + "pod-template-hash": "74cc89dbd7", + "run": "deb" + }, + "name": "deb-74cc89dbd7-t2gmt", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "deb-74cc89dbd7", + "uid": "dd70c211-e319-4204-8407-098901a0cbef" + } + ], + "resourceVersion": "135945", + "selfLink": "/api/v1/namespaces/default/pods/deb-74cc89dbd7-t2gmt", + "uid": "91a1c2f6-4289-4667-b9a8-aa16a684e6cc" + }, + "spec": { + "containers": [ + { + "args": [ + "/bin/bash" + ], + "image": "debian:buster", + "imagePullPolicy": "IfNotPresent", + "name": "deb", + "resources": {}, + "stdin": true, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "tty": true, + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-ljqzq", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-ljqzq", + "secret": { + "defaultMode": 420, + "secretName": "default-token-ljqzq" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-01T04:54:32Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-11T16:09:09Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-11T16:09:09Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-01T04:54:32Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://800a8e8ec657765cf5845a5d5fa9876486899e0cef63796ab81f1490bbd4973a", + "image": "debian:buster", + "imageID": "docker-pullable://debian@sha256:e25b64a9cf82c72080074d6b1bba7329cdd752d51574971fd37731ed164f3345", + "lastState": { + "terminated": { + "containerID": "docker://8eb1eae9ff12697878708debb428d4a691e2b9f36bd87521ba74b5a9b2f5f65c", + "exitCode": 0, + "finishedAt": "2019-10-03T14:49:29Z", + "reason": "Completed", + "startedAt": "2019-10-02T04:19:27Z" + } + }, + "name": "deb", + "ready": true, + "restartCount": 2, + "started": true, + "state": { + "running": { + "startedAt": "2019-10-11T16:09:08Z" + } + } + } + ], + "hostIP": "192.168.64.3", + "phase": "Running", + "podIP": "172.17.0.9", + "podIPs": [ + { + "ip": "172.17.0.9" + } + ], + "qosClass": "BestEffort", + "startTime": "2019-10-01T04:54:32Z" + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "", + "selfLink": "" + } +} diff --git a/kubernetes/e2e_test/test_json/pod.json b/kubernetes/e2e_test/test_json/pod.json new file mode 100644 index 0000000000..d799554488 --- /dev/null +++ b/kubernetes/e2e_test/test_json/pod.json @@ -0,0 +1,148 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "creationTimestamp": "2019-09-30T03:58:39Z", + "generateName": "nginx-deployment-54f57cf6bf-", + "labels": { + "app": "nginx", + "pod-template-hash": "54f57cf6bf" + }, + "name": "nginx-deployment-54f57cf6bf-hpphg", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "nginx-deployment-54f57cf6bf", + "uid": "8617a3fc-9155-46ed-b033-ab55fb7b243e" + } + ], + "resourceVersion": "135926", + "selfLink": "/api/v1/namespaces/default/pods/nginx-deployment-54f57cf6bf-hpphg", + "uid": "aaa0c5d9-40ff-4da6-a53c-095c1eee1a7d" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.7.9", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-ljqzq", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-ljqzq", + "secret": { + "defaultMode": 420, + "secretName": "default-token-ljqzq" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-09-30T03:58:39Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-11T16:09:08Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-10-11T16:09:08Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-09-30T03:58:39Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://02b144e443704007d0588ac8397c5524f52d685e6e9cea4e36642d53a65f7c56", + "image": "nginx:1.7.9", + "imageID": "docker-pullable://nginx@sha256:e3456c851a152494c3e4ff5fcc26f240206abac0c9d794affb40e0714846c451", + "lastState": { + "terminated": { + "containerID": "docker://b84d562ab50ee6453e945756c94ba9e8000f4f970eb62f5b45eab074435ddd0b", + "exitCode": 0, + "finishedAt": "2019-10-03T14:49:27Z", + "reason": "Completed", + "startedAt": "2019-10-02T04:19:27Z" + } + }, + "name": "nginx", + "ready": true, + "restartCount": 3, + "started": true, + "state": { + "running": { + "startedAt": "2019-10-11T16:09:08Z" + } + } + } + ], + "hostIP": "192.168.64.3", + "phase": "Running", + "podIP": "172.17.0.5", + "podIPs": [ + { + "ip": "172.17.0.5" + } + ], + "qosClass": "BestEffort", + "startTime": "2019-09-30T03:58:39Z" + } +} diff --git a/kubernetes/e2e_test/test_json/replicaset.json b/kubernetes/e2e_test/test_json/replicaset.json new file mode 100644 index 0000000000..52c0c741c0 --- /dev/null +++ b/kubernetes/e2e_test/test_json/replicaset.json @@ -0,0 +1,76 @@ +{ + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "metadata": { + "annotations": { + "deployment.kubernetes.io/desired-replicas": "1", + "deployment.kubernetes.io/max-replicas": "2", + "deployment.kubernetes.io/revision": "1" + }, + "creationTimestamp": "2019-10-01T04:51:46Z", + "generation": 1, + "labels": { + "pod-template-hash": "748c7587cf", + "run": "curl3" + }, + "name": "curl3-748c7587cf", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Deployment", + "name": "curl3", + "uid": "6e9bb86b-4e2a-4567-9519-393a3d7245da" + } + ], + "resourceVersion": "469652", + "selfLink": "/apis/apps/v1/namespaces/default/replicasets/curl3-748c7587cf", + "uid": "1eb825e8-8983-492c-949e-946fe31612ce" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "pod-template-hash": "748c7587cf", + "run": "curl3" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "pod-template-hash": "748c7587cf", + "run": "curl3" + } + }, + "spec": { + "containers": [ + { + "image": "radial/busyboxplus:curl", + "imagePullPolicy": "IfNotPresent", + "name": "curl3", + "resources": {}, + "stdin": true, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "tty": true + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 1, + "fullyLabeledReplicas": 1, + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1 + } +} diff --git a/kubernetes/e2e_test/test_json/service.json b/kubernetes/e2e_test/test_json/service.json new file mode 100644 index 0000000000..e34a0c0571 --- /dev/null +++ b/kubernetes/e2e_test/test_json/service.json @@ -0,0 +1,33 @@ +{ + "apiVersion": "v1", + "kind": "Service", + "metadata": { + "creationTimestamp": "2019-09-30T04:03:08Z", + "labels": { + "app": "nginx" + }, + "name": "nginx-deployment", + "namespace": "default", + "resourceVersion": "1879", + "selfLink": "/api/v1/namespaces/default/services/nginx-deployment", + "uid": "04c42185-b00f-4245-947d-4fb473979e15" + }, + "spec": { + "clusterIP": "10.98.219.120", + "ports": [ + { + "port": 80, + "protocol": "TCP", + "targetPort": 80 + } + ], + "selector": { + "app": "nginx" + }, + "sessionAffinity": "None", + "type": "ClusterIP" + }, + "status": { + "loadBalancer": {} + } +} diff --git a/kubernetes/e2e_test/test_utils_deserialize.py b/kubernetes/e2e_test/test_utils_deserialize.py new file mode 100644 index 0000000000..0f6f42a60c --- /dev/null +++ b/kubernetes/e2e_test/test_utils_deserialize.py @@ -0,0 +1,425 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +import yaml +from kubernetes import utils, client +from datetime import datetime + + +class TestUtils(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.json_path_prefix = "kubernetes/e2e_test/test_json/" + cls.yaml_path_prefix = "kubernetes/e2e_test/test_yaml/" + + def test_load_config_map_env_source_from_simple_dict(self): + """ + Should be able to load an object with limited data + and passing the klass. + """ + data = {"name": "source"} + obj = utils.load_from_dict(data, klass="V1ConfigMapEnvSource") + self.assertIsInstance(obj, client.models.v1_config_map_env_source.V1ConfigMapEnvSource) + self.assertEqual(obj.name, "source") + + def test_load_secret_env_source_from_simple_dict(self): + """ + Should be able to load an object with limited data + and passing the klass. + """ + data = {"name": "source"} + obj = utils.load_from_dict(data, klass="V1SecretEnvSource") + self.assertIsInstance(obj, client.models.v1_secret_env_source.V1SecretEnvSource) + self.assertEqual(obj.name, "source") + + def test_load_apps_deployment_from_dict(self): + """ + Should be able to load apps/v1 deployment. + """ + with open(self.yaml_path_prefix + "apps-deployment.yaml") as f: + yml_obj = yaml.safe_load(f) + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_deployment.V1Deployment) + + def test_load_apps_deployment_from_yaml(self): + """ + Should be able to load apps/v1 deployment. + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "apps-deployment.yaml") + self.assertIsInstance(obj, client.models.v1_deployment.V1Deployment) + + def test_load_pod_from_dict(self): + """ + Should be able to load a pod object. + """ + with open(self.yaml_path_prefix + "core-pod.yaml") as f: + yml_obj = yaml.safe_load(f) + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_pod.V1Pod) + + def test_load_pod_from_yaml(self): + """ + Should be able to load a pod. + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "core-pod.yaml") + self.assertIsInstance(obj, client.models.v1_pod.V1Pod) + + def test_load_service_from_dict(self): + """ + Should be able to load a service. + """ + with open(self.yaml_path_prefix + "core-service.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_service.V1Service) + + def test_load_service_from_yaml(self): + """ + Should be able to load a service. + """ + with open(self.yaml_path_prefix + "core-service.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_yaml(self.yaml_path_prefix + "core-service.yaml") + self.assertIsInstance(obj, client.models.v1_service.V1Service) + + def test_load_namespace_from_dict(self): + """ + Should be able to load a namespace. + """ + with open(self.yaml_path_prefix + "core-namespace.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_namespace.V1Namespace) + + def test_load_namespace_from_yaml(self): + """ + Should be able to load a namespace. + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "core-namespace.yaml") + self.assertIsInstance(obj, client.models.v1_namespace.V1Namespace) + + def test_load_rbac_role_from_dict(self): + """ + Should be able to load an rbac role. + """ + with open(self.yaml_path_prefix + "rbac-role.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_role.V1Role) + + def test_load_rbac_role_from_yaml(self): + """ + Should be able to load an rbac role. + """ + with open(self.yaml_path_prefix + "rbac-role.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_yaml(self.yaml_path_prefix + "rbac-role.yaml") + self.assertIsInstance(obj, client.models.v1_role.V1Role) + + def test_load_non_default_namespace_from_dict(self): + """ + Should be able to load a namespace "dep" + """ + with open(self.yaml_path_prefix + "dep-namespace.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_namespace.V1Namespace) + self.assertEqual(obj.metadata.name, "dep") + + def test_load_non_default_namespace_from_yaml(self): + """ + Should be able to load a namespace "dep", + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "dep-namespace.yaml") + self.assertIsInstance(obj, client.models.v1_namespace.V1Namespace) + self.assertEqual(obj.metadata.name, "dep") + + def test_load_deployment_non_default_namespace_from_dict(self): + """ + Should be able to load a namespace "dep", + """ + with open(self.yaml_path_prefix + "dep-deployment.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_deployment.V1Deployment) + + def test_load_deployment_non_default_namespace_from_yaml(self): + """ + Should be able to load a namespace "dep", + and then create a deployment in the just-created namespace. + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "dep-deployment.yaml") + self.assertIsInstance(obj, client.models.v1_deployment.V1Deployment) + + def test_load_apiservice_from_dict(self): + """ + Should be able to load an API service. + """ + with open(self.yaml_path_prefix + "api-service.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1beta1_api_service.V1beta1APIService) + + def test_load_apiservice_from_yaml(self): + """ + Should be able to load an API service. + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "api-service.yaml") + self.assertIsInstance(obj, client.models.v1beta1_api_service.V1beta1APIService) + + # # Tests for creating API objects from lists + # + def test_load_general_list_from_dict(self): + """ + Should be able to load a service and a deployment + from a kind: List yaml file + """ + with open(self.yaml_path_prefix + "list.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, list) + self.assertEqual(len(obj), 2) + self.assertIsInstance(obj[0], client.models.v1_service_list.V1ServiceList) + self.assertIsInstance(obj[0].items[0], client.models.v1_service.V1Service) + self.assertIsInstance(obj[1], client.models.v1_deployment_list.V1DeploymentList) + self.assertIsInstance(obj[1].items[0], client.models.v1_deployment.V1Deployment) + + def test_load_general_list_from_yaml(self): + obj = utils.load_from_yaml(self.yaml_path_prefix + "list.yaml") + self.assertIsInstance(obj, list) + self.assertEqual(len(obj), 2) + self.assertIsInstance(obj[0], client.models.v1_service_list.V1ServiceList) + self.assertIsInstance(obj[0].items[0], client.models.v1_service.V1Service) + self.assertIsInstance(obj[1], client.models.v1_deployment_list.V1DeploymentList) + self.assertIsInstance(obj[1].items[0], client.models.v1_deployment.V1Deployment) + + def test_load_namespace_list_from_dict(self): + """ + Should be able to load two namespaces + from a kind: NamespaceList yaml file + """ + with open(self.yaml_path_prefix + "namespace-list.yaml") as f: + yml_obj = yaml.safe_load(f) + + obj = utils.load_from_dict(yml_obj) + self.assertIsInstance(obj, client.models.v1_namespace_list.V1NamespaceList) + self.assertEqual(len(obj.items), 2) + self.assertIsInstance(obj.items[0], client.models.v1_namespace.V1Namespace) + self.assertIsInstance(obj.items[1], client.models.v1_namespace.V1Namespace) + nmsp_1 = obj.items[0] + self.assertIsNotNone(nmsp_1) + nmsp_2 = obj.items[1] + self.assertIsNotNone(nmsp_2) + self.assertEqual(nmsp_1.metadata.name, "mock-1") + self.assertEqual(nmsp_2.metadata.name, "mock-2") + + def test_load_namespace_list_from_yaml(self): + """ + Should be able to load two namespaces + from a kind: NamespaceList yaml file + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "namespace-list.yaml") + self.assertIsInstance(obj, client.models.v1_namespace_list.V1NamespaceList) + self.assertEqual(len(obj.items), 2) + self.assertIsInstance(obj.items[0], client.models.v1_namespace.V1Namespace) + self.assertIsInstance(obj.items[1], client.models.v1_namespace.V1Namespace) + nmsp_1 = obj.items[0] + self.assertIsNotNone(nmsp_1) + nmsp_2 = obj.items[1] + self.assertIsNotNone(nmsp_2) + self.assertEqual(nmsp_1.metadata.name, "mock-1") + self.assertEqual(nmsp_2.metadata.name, "mock-2") + + # Tests for multi-resource yaml objects + def test_load_from_multi_resource_yaml(self): + """ + Should be able to load a service and a replication controller + from a multi-resource yaml file + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "multi-resource.yaml") + self.assertIsInstance(obj, list) + self.assertEqual(len(obj), 2) + self.assertIsInstance(obj[0], client.models.v1_service.V1Service) + self.assertIsInstance( + obj[1], client.models.v1_replication_controller.V1ReplicationController + ) + + def test_load_from_list_in_multi_resource_yaml(self): + """ + Should be able to load the items in the PodList and a deployment + specified in the multi-resource file + """ + obj = utils.load_from_yaml( + self.yaml_path_prefix + "multi-resource-with-list.yaml" + ) + self.assertIsInstance(obj, list) + self.assertEqual(len(obj), 2) + self.assertIsInstance(obj[0], client.models.v1_pod_list.V1PodList) + self.assertEqual(len(obj[0].items), 2) + self.assertIsInstance(obj[0].items[0], client.models.v1_pod.V1Pod) + self.assertIsInstance(obj[1], client.models.v1_deployment.V1Deployment) + + def test_load_namespaced_apps_deployment_from_yaml(self): + """ + Should be able to load an apps/v1beta1 deployment + in a test namespace. + """ + obj = utils.load_from_yaml(self.yaml_path_prefix + "apps-deployment.yaml") + self.assertIsInstance(obj, client.models.v1_deployment.V1Deployment) + + # Test json files + def test_load_pod_list_from_json(self): + """ + Should be able to load a list of pods objects + """ + pods = utils.load_from_json(self.json_path_prefix + "pod-list.json") + self.assertIsInstance(pods, client.models.v1_pod_list.V1PodList) + self.assertEqual(len(pods.items), 2) + for pod in pods.items: + self.assertIsInstance(pod, client.models.v1_pod.V1Pod) + self.assertEqual(pod.metadata.namespace, "default") + self.assertIsInstance(pod.metadata.creation_timestamp, datetime) + + def test_load_pod_list_with_klass_from_json(self): + """ + Should be able to load a list of pods objects + """ + pods = utils.load_from_json(self.json_path_prefix + "pod-list.json", klass="V1PodList") + self.assertIsInstance(pods, client.models.v1_pod_list.V1PodList) + self.assertEqual(len(pods.items), 2) + for pod in pods.items: + self.assertIsInstance(pod, client.models.v1_pod.V1Pod) + self.assertEqual(pod.metadata.namespace, "default") + self.assertIsInstance(pod.metadata.creation_timestamp, datetime) + + def test_load_pod_from_json(self): + """ + Should be able to load a pod object + """ + pod = utils.load_from_json(self.json_path_prefix + "pod.json") + self.assertIsInstance(pod, client.models.v1_pod.V1Pod) + self.assertEqual(pod.metadata.name, "nginx-deployment-54f57cf6bf-hpphg") + self.assertEqual(pod.metadata.namespace, "default") + self.assertIsInstance(pod.metadata.creation_timestamp, datetime) + + def test_load_pod_with_klass_from_json(self): + """ + Should be able to load a pod object + """ + pod = utils.load_from_json(self.json_path_prefix + "pod.json", klass="V1Pod") + self.assertIsInstance(pod, client.models.v1_pod.V1Pod) + self.assertEqual(pod.metadata.name, "nginx-deployment-54f57cf6bf-hpphg") + self.assertEqual(pod.metadata.namespace, "default") + self.assertIsInstance(pod.metadata.creation_timestamp, datetime) + + def test_load_deployment_from_json(self): + """ + Should be able to load a deployment object + """ + deployment = utils.load_from_json(self.json_path_prefix + "deployment.json") + self.assertIsInstance(deployment, client.models.v1_deployment.V1Deployment) + self.assertEqual(deployment.metadata.namespace, "default") + self.assertIsInstance(deployment.metadata.creation_timestamp, datetime) + + def test_load_replicaset_from_json(self): + """ + Should be able to load a replicaset object + """ + replicaset = utils.load_from_json(self.json_path_prefix + "replicaset.json") + self.assertIsInstance(replicaset, client.models.v1_replica_set.V1ReplicaSet) + self.assertEqual(replicaset.metadata.name, "curl3-748c7587cf") + self.assertEqual(replicaset.metadata.namespace, "default") + self.assertIsInstance(replicaset.metadata.creation_timestamp, datetime) + + # Try non-namespaced objects + def test_load_namespace_from_json(self): + """ + Should be able to load a namespace objects + """ + namespace = utils.load_from_json(self.json_path_prefix + "namespace.json") + self.assertIsInstance(namespace, client.models.v1_namespace.V1Namespace) + self.assertIsInstance(namespace.metadata.creation_timestamp, datetime) + self.assertEqual(namespace.kind, "Namespace") + self.assertEqual(namespace.metadata.name, "default") + + def test_load_node_from_json(self): + """ + Should be able to load a node object + """ + node = utils.load_from_json(self.json_path_prefix + "node.json") + self.assertIsInstance(node, client.models.v1_node.V1Node) + self.assertIsInstance(node.metadata.creation_timestamp, datetime) + self.assertEqual(node.kind, "Node") + self.assertEqual(node.metadata.name, "minikube") + + def test_load_apiservice_from_json(self): + """ + Should be able to load an apiservice objects + """ + apiservice = utils.load_from_json(self.json_path_prefix + "apiservice.json") + self.assertIsInstance(apiservice, client.models.v1_api_service.V1APIService) + self.assertIsInstance(apiservice.metadata.creation_timestamp, datetime) + self.assertEqual(apiservice.kind, "APIService") + self.assertEqual(apiservice.metadata.name, "v1.") + + # Try different failures + def test_load_apiservice_from_json_fail(self): + """ + Should not be able to load an apiservice object + """ + + with self.assertRaises(ValueError) as cm: + apiservice = utils.load_from_json( + self.json_path_prefix + "apiservice-fail.json" + ) + exp_error = "Invalid value for `service`, must not be `None`" + self.assertEqual(exp_error, str(cm.exception)) + + def test_load_failure_from_bad_dict(self): + """ + Try with a couple of of malformed dictionaries + """ + # Omit kind and apiVersion + # use keys as expected str in exceptions + bad_dicts = { + "'apiVersion'": {"BadDict": 12345, "kind": "Pod"}, + "'kind'": {"BadDict": 12345, "apiVersion": "v1"}, + } + + for k in bad_dicts: + with self.assertRaises(KeyError) as cm: + utils.load_from_dict(bad_dicts[k]) + + self.assertEqual(k, str(cm.exception)) + + def test_load_failure_from_bad_json(self): + """ + Try with a malformed kind string + """ + with self.assertRaises(AttributeError) as cm: + utils.load_from_json(self.json_path_prefix + "bad-data.json") + + exp_error = "has no attribute 'CoreV1badApi'" + self.assertIn(exp_error, str(cm.exception)) diff --git a/kubernetes/utils/__init__.py b/kubernetes/utils/__init__.py index 8add80bcfe..f318ae370f 100644 --- a/kubernetes/utils/__init__.py +++ b/kubernetes/utils/__init__.py @@ -16,4 +16,6 @@ from .create_from_yaml import (FailToCreateError, create_from_dict, create_from_yaml) +from .deserialize import (FailToLoadError, load_from_dict, load_from_json, + load_from_yaml) from .quantity import parse_quantity diff --git a/kubernetes/utils/deserialize.py b/kubernetes/utils/deserialize.py new file mode 100644 index 0000000000..1c276e95a0 --- /dev/null +++ b/kubernetes/utils/deserialize.py @@ -0,0 +1,373 @@ +# Copyright 2020 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import pydoc +import re +import sys +from copy import deepcopy +from os import path + +import yaml + +from kubernetes import client + +PYDOC_RETURN_LABEL = ":return:" + + +class RespMock(object): + """ + A dummy class to mock RESTResponse object + """ + + def __init__(self, *args): + self.data = None + + +def load_from_json(json_file, klass=None, verbose=False): + """ + Load objects from json file. Pass True for verbose to + print additional information. + Input: + json_file: string. Contains the path to json file. + klass: class literal for deserialized object, + or string of class name. + verbose: If True, print information about the object lookup info. + Default is False. + + Raises: + FailToLoadError which holds list of load failures.. + """ + with open(path.abspath(json_file)) as file: + data = file.read() + # python 2 has problems decoding unicode from json.load + # use yaml.load instead + if sys.version_info[0] < 3: + data = yaml.safe_load(data) + else: + data = json.loads(data) + obj = load_from_dict(data, klass=klass, verbose=verbose) + + return obj + + +def load_from_yaml(yaml_file, klass=None, verbose=False): + """ + Perform an action from a yaml file. Pass True for verbose to + print additional information. + Input: + yaml_file: string. Contains the path to yaml file. + klass: class literal for deserialized object, + or string of class name. + verbose: If True, print information about the object lookup info. + Default is False. + + Raises: + FailToLoadError which holds list of load failures. + """ + with open(path.abspath(yaml_file)) as f: + yml_document_all = yaml.safe_load_all(f) + failures = [] + objs = [] + for yml_document in yml_document_all: + try: + obj = load_from_dict( + yml_document, klass=klass, verbose=verbose) + # Prevent returning list of lists when doing multi + # document yaml + if isinstance(obj, list): + objs.extend(obj) + else: + objs.append(obj) + + except FailToLoadError as failure: + failures.extend(failure.load_exceptions) + + if objs: + result = objs if len(objs) > 1 else objs[0] + + if failures: + raise FailToLoadError(failures) + + return result + + +def response_type_from_dict(data, verbose=False): + """ + Helper function to do source code lookup and extract the + response_type of an kubernetes object. + Input: + data: a dictionary holding a valid kubernetes object + verbose: If True, print confirmation from the create action. + Default is False. + + Raises: + FailToLoadError which holds list of load failures. + """ + response_type = None + items_kind = None + failures = [] + + if "List" in data["kind"]: + # Lookup the first item apiVersion + group, _, version = data["items"][0]["apiVersion"].partition("/") + else: + group, _, version = data["apiVersion"].partition("/") + if version == "": + version = group + group = "core" + # Take care for the case e.g. api_type is "apiextensions.k8s.io" + # Only replace the last instance + group = "".join(group.rsplit(".k8s.io", 1)) + # convert group name from DNS subdomain format to + # python class name convention + group = "".join(word.capitalize() for word in group.split(".")) + fcn_to_call = "{0}{1}Api".format(group, version.capitalize()) + k8s_api = getattr(client, fcn_to_call) + # Replace CamelCased action_type into snake_case + kind = data["kind"] + kind = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", kind) + kind = re.sub("([a-z0-9])([A-Z])", r"\1_\2", kind).lower() + + # Expect List of the same kind + if "List" in data["kind"] and data["items"]: + # Lookup the first item kind + items_kind = data["items"][0]["kind"] + # Replace CamelCased action_type into snake_case + items_kind = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", items_kind) + items_kind = re.sub("([a-z0-9])([A-Z])", r"\1_\2", items_kind).lower() + # Expect the user to load list of namespaced objects more often + if hasattr( + k8s_api, + "list_namespaced_{0}_with_http_info".format(items_kind)): + fnc_lookup = "list_namespaced_{0}_with_http_info".format( + items_kind) + elif hasattr( + k8s_api, "list_{0}_for_all_namespaces_with_http_info".format(items_kind) + ): + fnc_lookup = "list_{0}__for_all_namespaces_with_http_info".format( + items_kind + ) + # Try with non-namespaced list + elif hasattr(k8s_api, "list_{0}_with_http_info".format(items_kind)): + fnc_lookup = "list_{0}_with_http_info".format(items_kind) + else: + fnc_lookup = None + else: + # Expect the user to load namespaced objects more often + if hasattr(k8s_api, "read_namespaced_{0}_with_http_info".format(kind)): + fnc_lookup = "read_namespaced_{0}_with_http_info".format(kind) + elif hasattr(k8s_api, "read_{0}_with_http_info".format(kind)): + fnc_lookup = "read_{0}_with_http_info".format(kind) + # Try with the get if no read fnc found + elif hasattr(k8s_api, "get_{0}_with_http_info".format(kind)): + fnc_lookup = "get_{0}_with_http_info".format(kind) + else: + fnc_lookup = None + + info = { + "group": group, + "version": version, + "kind": kind, + "items_kind": items_kind, + "fcn_to_call": fcn_to_call, + "fnc_lookup": fnc_lookup, + } + if verbose: + print("Lookup info: {}".format(info)) + + if fnc_lookup: + if hasattr(k8s_api, fnc_lookup): + # Get response_type from func pydoc + response_type = _find_response_type(getattr(k8s_api, fnc_lookup)) + if verbose: + print( + "Lookup function found: {} in k8s_api: {} response_type: " + "{} lookup info: {}".format( + fnc_lookup, k8s_api, response_type, info + ) + ) + else: + if verbose: + print( + "Lookup function not found: {} in k8s_api: {} " + "response_type: {} lookup info: {}".format( + fnc_lookup, k8s_api, response_type, info + ) + ) + else: + msg = "Failed to find a function to inspect; lookup info: {}".format( + info) + raise FailToLoadError(reason=msg) + + return response_type + + +def _find_response_type(func): + """ + Lookup the response type of a function from the pydoc text + """ + func_response_type = "" + for line in pydoc.getdoc(func).splitlines(): + if line.startswith(PYDOC_RETURN_LABEL): + func_return_line_content = line[len(PYDOC_RETURN_LABEL):].strip() + # The latest codegen generates different return line text with + # tuple items + if func_return_line_content.startswith("tuple("): + # The response type is the first item in the tuple + func_response_type = func_return_line_content.split( + ",")[0].replace("tuple(", "") + else: + func_response_type = func_return_line_content + + return func_response_type + + +def load_from_dict(data, klass=None, verbose=False): + """ + Load object/objects from a dictionary containing valid kubernetes + API object (i.e. List, Service, etc). + + Input: + data: a dictionary holding valid kubernetes objects + klass: class literal for deserialized object, + or string of class name. + verbose: If True, print additional info. + Default is False. + Raises: + FailToLoadError which holds list of load failures. + """ + load_exceptions = [] + obj = None + # Check if List has multiple kinds and break it: + if "kind" in data and "List" in data["kind"] and _is_list_multi_kind(data): + if verbose: + print( + "Multi kind list detected kinds: {} items: {}".format( + len(_get_list_kinds(data)), len(data["items"]) + ) + ) + kind_lists = [] + for kind_list in _break_list_multi_kind(data): + try: + obj = load_single_obj(kind_list, klass=klass, verbose=verbose) + kind_lists.append(obj) + except FailToLoadError as load_exception: + load_exceptions.append(load_exception) + + obj = kind_lists + + else: + try: + obj = load_single_obj(data, klass=klass, verbose=verbose) + except FailToLoadError as load_exception: + load_exceptions.append(load_exception) + + # In case we have exceptions waiting for us, raise them + if load_exceptions: + raise FailToLoadError(load_exceptions) + + return obj + + +def _is_list_multi_kind(data): + """ + Helper function to detect if it's a multi-kind list. + Input: + data: a dictionary holding a kubernetes object list + """ + return True if len(_get_list_kinds(data)) > 1 else False + + +def _get_list_kinds(data): + """ + Helper function to extract the kinds in a list. + Input: + data: a dictionary holding a kubernetes object list + """ + kinds = [] + + # Add kinds in an ordered way: + for obj in data.get("items"): + kind = obj.get("kind") + if kind not in kinds: + kinds.append(kind) + + return kinds + + +def _break_list_multi_kind(data): + """ + Helper function to break a multi kind list into separate + kind lists. + Input: + data: a dictionary holding a multi kind kubernetes object list + """ + kinds = _get_list_kinds(data) + new_list = [] + + for kind in kinds: + new_data = deepcopy(data) + kind_list = list( + filter( + lambda obj: obj["kind"] == kind, + data["items"])) + new_data["items"] = kind_list + new_list.append(new_data) + + return new_list + + +def load_single_obj(data, klass=None, verbose=False): + """ + Load a single object from a dictionary containing valid kubernetes + API object (i.e. List, Service, etc). + Input: + data: a dictionary holding a valid kubernetes object + klass: class literal for deserialized object, + or string of class name. + verbose: If True, print additional info. + Default is False. + """ + # Mock api response from dict + resp_mock = RespMock() + resp_mock.data = json.dumps(data) + + # Infer response_type from json data when not provided + if not klass: + klass = response_type_from_dict(data, verbose=verbose) + api_client = client.api_client.ApiClient() + obj = api_client.deserialize(response=resp_mock, response_type=klass) + + return obj + + +class FailToLoadError(Exception): + """ + An exception class for handling error if an error occurred when + loading a file. + """ + + def __init__(self, load_exceptions=[], reason=""): + self.load_exceptions = load_exceptions + self.reason = reason + + def __str__(self): + msg = self.reason + if len(self.load_exceptions) > 1: + msg = "Error loading {} objects".format(len(self.load_exceptions)) + + for load_exception in self.load_exceptions: + msg += " reason: {}".format(load_exception.reason) + return msg