coredns/middleware/kubernetes/k8sclient/k8sclient_test.go
Michael Richmond 289f53d386 k8s middleware cleanup, testcases, basic SRV (#181)
* Removing unnecessary gitignore pattern

* Updating Makefile to run unittests for subpackages

* Adding Corefile validation to ignore overlapping zones

* Fixing SRV query handling

* Updating README.md now that SRV works

* Fixing debug message, adding code comment

* Clarifying implementation of zone normalization

* "Overlapping zones" is ill-defined. Reimplemented zone overlap/subzone
  checking to contain these functions in k8s middleware and provide
  better code comments explaining the normalization.

* Separate build verbosity from test verbosity

* Cleaning up comments to match repo code style

* Merging warning messages into single message
* Moving function docs to before function declaration

* Adding test cases for k8sclient connector

* Tests cover connector create and setting base url
* Fixed bugs in connector create and setting base url functions

* Updaing README to group and order development work

* Priority focused on achieving functional parity with SkyDNS.

* Adding work items to README and cleaning up formatting

* More README format cleaning

* List formating

* Refactoring k8s API call to allow dependency injection

* Add test cases for data parsing from k8s into dataobject structures

* URL is dependency-injected to allow replacement with a mock http
  server during test execution

* Adding more data validation for JSON parsing tests

* Adding test case for GetResourceList()

* Adding notes about SkyDNS embedded IP and port record names

* Marked test case implemented.

* Fixing formatting for example command.

* Fixing formatting

* Adding notes about Docker image building.

* Adding SkyDNS work item

* Updating TODO list

* Adding name template to Corefile to specify how k8s record names are assembled

* Adding template support for multi-segment zones

* Updating example CoreFile for k8s with template comment

* Misc whitespace cleanup

* Adding SkyDNS naming notes

* Adding namespace filtering to CoreFile config

* Updating example k8sCoreFile to specify namespaces

* Removing unused codepath

* Adding check for valid namespace

* More README TODO restructuring to focus effort

* Adding template validation while parsing CoreFile

* Record name template is considered invalid if it contains a symbol of the form ${bar} where the symbol
  "${bar}" is not an accepted template symbol.

* Refactoring generation of answer records

* Parse typeName out of query string
* Refactor answer record creation as operation over list of ServiceItems

* Moving k8s API caching into SkyDNS equivalency segment

* Adding function to assemble record names from template

* Warning: This commit may be broken. Syncing to get laptop code over to dev machine.

* More todo notes

* Adding comment describing sample test data.

* Update k8sCorefile

* Adding comment

* Adding filtering support for kubernetes "type"

* Required refactoring to support reuse of the StringInSlice function.

* Cleaning up formatting

* Adding note about SkyDNS supporting word "any".

* baseUrl -> baseURL

* Also removed debug statement from core/setup/kubernetes.go

* Fixing test breaking from Url -> URL naming changes

* Changing record name template language ${...} -> {...}

* Fix formatting with go fmt

* Updating all k8sclient data getters to return error value

* Adding error message to k8sclient data accessors

* Cleaning up setup for kubernetes

* Removed verbose nils in initial k8s middleware instance
* Set reasonable defaults if CoreFile has no parameters in the
kubernetes block. (k8s endpoint, and name template)

* Formatting cleanup -- go fmt
2016-07-07 09:40:58 +01:00

680 lines
16 KiB
Go

package k8sclient
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
var validURLs = []string{
"http://www.github.com",
"http://www.github.com:8080",
"http://8.8.8.8",
"http://8.8.8.8:9090",
"www.github.com:8080",
}
var invalidURLs = []string{
"www.github.com",
"8.8.8.8",
"8.8.8.8:1010",
"8.8`8.8",
}
func TestNewK8sConnector(t *testing.T) {
var conn *K8sConnector
var url string
// Create with empty URL
conn = nil
url = ""
conn = NewK8sConnector("")
if conn == nil {
t.Errorf("Expected K8sConnector instance. Instead got '%v'", conn)
}
url = conn.GetBaseURL()
if url != defaultBaseURL {
t.Errorf("Expected K8sConnector instance to be initialized with defaultBaseURL. Instead got '%v'", url)
}
// Create with valid URL
for _, validURL := range validURLs {
conn = nil
url = ""
conn = NewK8sConnector(validURL)
if conn == nil {
t.Errorf("Expected K8sConnector instance. Instead got '%v'", conn)
}
url = conn.GetBaseURL()
if url != validURL {
t.Errorf("Expected K8sConnector instance to be initialized with supplied url '%v'. Instead got '%v'", validURL, url)
}
}
// Create with invalid URL
for _, invalidURL := range invalidURLs {
conn = nil
url = ""
conn = NewK8sConnector(invalidURL)
if conn != nil {
t.Errorf("Expected to not get K8sConnector instance. Instead got '%v'", conn)
continue
}
}
}
func TestSetBaseURL(t *testing.T) {
// SetBaseURL with valid URLs should work...
for _, validURL := range validURLs {
conn := NewK8sConnector(defaultBaseURL)
err := conn.SetBaseURL(validURL)
if err != nil {
t.Errorf("Expected to receive nil, instead got error '%v'", err)
continue
}
url := conn.GetBaseURL()
if url != validURL {
t.Errorf("Expected to connector url to be set to value '%v', instead set to '%v'", validURL, url)
continue
}
}
// SetBaseURL with invalid or non absolute URLs should not change state...
for _, invalidURL := range invalidURLs {
conn := NewK8sConnector(defaultBaseURL)
originalURL := conn.GetBaseURL()
err := conn.SetBaseURL(invalidURL)
if err == nil {
t.Errorf("Expected to receive an error value, instead got nil")
}
url := conn.GetBaseURL()
if url != originalURL {
t.Errorf("Expected base url to not change, instead it changed to '%v'", url)
}
}
}
func TestGetNamespaceList(t *testing.T) {
// Set up a test http server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, namespaceListJsonData)
}))
defer testServer.Close()
// Overwrite URL constructor to access testServer
makeURL = func(parts []string) string {
return testServer.URL
}
expectedNamespaces := []string{"default", "demo", "test"}
apiConn := NewK8sConnector("")
namespaceList, err := apiConn.GetNamespaceList()
if err != nil {
t.Errorf("Expected no error from from GetNamespaceList(), instead got %v", err)
}
if namespaceList == nil {
t.Errorf("Expected data from GetNamespaceList(), instead got nil")
}
kind := namespaceList.Kind
if kind != "NamespaceList" {
t.Errorf("Expected data from GetNamespaceList() to have Kind='NamespaceList', instead got Kind='%v'", kind)
}
// Ensure correct number of namespaces found
expectedCount := len(expectedNamespaces)
namespaceCount := len(namespaceList.Items)
if namespaceCount != expectedCount {
t.Errorf("Expected '%v' namespaces from GetNamespaceList(), instead found '%v' namespaces", expectedCount, namespaceCount)
}
// Check that all expectedNamespaces are found in the parsed data
for _, ns := range expectedNamespaces {
found := false
for _, item := range namespaceList.Items {
if item.Metadata.Name == ns {
found = true
break
}
}
if !found {
t.Errorf("Expected '%v' namespace is not in the parsed data from GetServicesByNamespace()", ns)
}
}
}
func TestGetServiceList(t *testing.T) {
// Set up a test http server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, serviceListJsonData)
}))
defer testServer.Close()
// Overwrite URL constructor to access testServer
makeURL = func(parts []string) string {
return testServer.URL
}
expectedServices := []string{"kubernetes", "mynginx", "mywebserver"}
apiConn := NewK8sConnector("")
serviceList, err := apiConn.GetServiceList()
if err != nil {
t.Errorf("Expected no error from from GetNamespaceList(), instead got %v", err)
}
if serviceList == nil {
t.Errorf("Expected data from GetServiceList(), instead got nil")
}
kind := serviceList.Kind
if kind != "ServiceList" {
t.Errorf("Expected data from GetServiceList() to have Kind='ServiceList', instead got Kind='%v'", kind)
}
// Ensure correct number of services found
expectedCount := len(expectedServices)
serviceCount := len(serviceList.Items)
if serviceCount != expectedCount {
t.Errorf("Expected '%v' services from GetServiceList(), instead found '%v' services", expectedCount, serviceCount)
}
// Check that all expectedServices are found in the parsed data
for _, s := range expectedServices {
found := false
for _, item := range serviceList.Items {
if item.Metadata.Name == s {
found = true
break
}
}
if !found {
t.Errorf("Expected '%v' service is not in the parsed data from GetServiceList()", s)
}
}
}
func TestGetServicesByNamespace(t *testing.T) {
// Set up a test http server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, serviceListJsonData)
}))
defer testServer.Close()
// Overwrite URL constructor to access testServer
makeURL = func(parts []string) string {
return testServer.URL
}
expectedNamespaces := []string{"default", "demo"}
apiConn := NewK8sConnector("")
servicesByNamespace, err := apiConn.GetServicesByNamespace()
if err != nil {
t.Errorf("Expected no error from from GetServicesByNamespace(), instead got %v", err)
}
// Ensure correct number of namespaces found
expectedCount := len(expectedNamespaces)
namespaceCount := len(servicesByNamespace)
if namespaceCount != expectedCount {
t.Errorf("Expected '%v' namespaces from GetServicesByNamespace(), instead found '%v' namespaces", expectedCount, namespaceCount)
}
// Check that all expectedNamespaces are found in the parsed data
for _, ns := range expectedNamespaces {
_, ok := servicesByNamespace[ns]
if !ok {
t.Errorf("Expected '%v' namespace is not in the parsed data from GetServicesByNamespace()", ns)
}
}
}
func TestGetResourceList(t *testing.T) {
// Set up a test http server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, resourceListJsonData)
}))
defer testServer.Close()
// Overwrite URL constructor to access testServer
makeURL = func(parts []string) string {
return testServer.URL
}
expectedResources := []string{"bindings",
"componentstatuses",
"configmaps",
"endpoints",
"events",
"limitranges",
"namespaces",
"namespaces/finalize",
"namespaces/status",
"nodes",
"nodes/proxy",
"nodes/status",
"persistentvolumeclaims",
"persistentvolumeclaims/status",
"persistentvolumes",
"persistentvolumes/status",
"pods",
"pods/attach",
"pods/binding",
"pods/exec",
"pods/log",
"pods/portforward",
"pods/proxy",
"pods/status",
"podtemplates",
"replicationcontrollers",
"replicationcontrollers/scale",
"replicationcontrollers/status",
"resourcequotas",
"resourcequotas/status",
"secrets",
"serviceaccounts",
"services",
"services/proxy",
"services/status",
}
apiConn := NewK8sConnector("")
resourceList, err := apiConn.GetResourceList()
if err != nil {
t.Errorf("Expected no error from from GetResourceList(), instead got %v", err)
}
if resourceList == nil {
t.Errorf("Expected data from GetResourceList(), instead got nil")
}
kind := resourceList.Kind
if kind != "APIResourceList" {
t.Errorf("Expected data from GetResourceList() to have Kind='ResourceList', instead got Kind='%v'", kind)
}
// Ensure correct number of resources found
expectedCount := len(expectedResources)
resourceCount := len(resourceList.Resources)
if resourceCount != expectedCount {
t.Errorf("Expected '%v' resources from GetResourceList(), instead found '%v' resources", expectedCount, resourceCount)
}
// Check that all expectedResources are found in the parsed data
for _, r := range expectedResources {
found := false
for _, item := range resourceList.Resources {
if item.Name == r {
found = true
break
}
}
if !found {
t.Errorf("Expected '%v' resource is not in the parsed data from GetResourceList()", r)
}
}
}
// Sample namespace data for kubernetes with 3 namespaces:
// "default", "demo", and "test".
const namespaceListJsonData string = `{
"kind": "NamespaceList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/namespaces/",
"resourceVersion": "121279"
},
"items": [
{
"metadata": {
"name": "default",
"selfLink": "/api/v1/namespaces/default",
"uid": "fb1c92d1-2f39-11e6-b9db-0800279930f6",
"resourceVersion": "6",
"creationTimestamp": "2016-06-10T18:34:35Z"
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
},
{
"metadata": {
"name": "demo",
"selfLink": "/api/v1/namespaces/demo",
"uid": "73be8ffd-2f3a-11e6-b9db-0800279930f6",
"resourceVersion": "111",
"creationTimestamp": "2016-06-10T18:37:57Z"
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
},
{
"metadata": {
"name": "test",
"selfLink": "/api/v1/namespaces/test",
"uid": "c0be05fa-3352-11e6-b9db-0800279930f6",
"resourceVersion": "121276",
"creationTimestamp": "2016-06-15T23:41:59Z"
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
}
]
}`
// Sample service data for kubernetes with 3 services:
// * "kubernetes" (in "default" namespace)
// * "mynginx" (in "demo" namespace)
// * "webserver" (in "demo" namespace)
const serviceListJsonData string = `
{
"kind": "ServiceList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/services",
"resourceVersion": "147965"
},
"items": [
{
"metadata": {
"name": "kubernetes",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/services/kubernetes",
"uid": "fb1cb0d3-2f39-11e6-b9db-0800279930f6",
"resourceVersion": "7",
"creationTimestamp": "2016-06-10T18:34:35Z",
"labels": {
"component": "apiserver",
"provider": "kubernetes"
}
},
"spec": {
"ports": [
{
"name": "https",
"protocol": "TCP",
"port": 443,
"targetPort": 443
}
],
"clusterIP": "10.0.0.1",
"type": "ClusterIP",
"sessionAffinity": "None"
},
"status": {
"loadBalancer": {}
}
},
{
"metadata": {
"name": "mynginx",
"namespace": "demo",
"selfLink": "/api/v1/namespaces/demo/services/mynginx",
"uid": "93c117ac-2f3a-11e6-b9db-0800279930f6",
"resourceVersion": "147",
"creationTimestamp": "2016-06-10T18:38:51Z",
"labels": {
"run": "mynginx"
}
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 80,
"targetPort": 80
}
],
"selector": {
"run": "mynginx"
},
"clusterIP": "10.0.0.132",
"type": "ClusterIP",
"sessionAffinity": "None"
},
"status": {
"loadBalancer": {}
}
},
{
"metadata": {
"name": "mywebserver",
"namespace": "demo",
"selfLink": "/api/v1/namespaces/demo/services/mywebserver",
"uid": "aed62187-33e5-11e6-a224-0800279930f6",
"resourceVersion": "138185",
"creationTimestamp": "2016-06-16T17:13:45Z",
"labels": {
"run": "mywebserver"
}
},
"spec": {
"ports": [
{
"protocol": "TCP",
"port": 443,
"targetPort": 443
}
],
"selector": {
"run": "mywebserver"
},
"clusterIP": "10.0.0.63",
"type": "ClusterIP",
"sessionAffinity": "None"
},
"status": {
"loadBalancer": {}
}
}
]
}
`
// Sample resource data for kubernetes.
const resourceListJsonData string = `{
"kind": "APIResourceList",
"groupVersion": "v1",
"resources": [
{
"name": "bindings",
"namespaced": true,
"kind": "Binding"
},
{
"name": "componentstatuses",
"namespaced": false,
"kind": "ComponentStatus"
},
{
"name": "configmaps",
"namespaced": true,
"kind": "ConfigMap"
},
{
"name": "endpoints",
"namespaced": true,
"kind": "Endpoints"
},
{
"name": "events",
"namespaced": true,
"kind": "Event"
},
{
"name": "limitranges",
"namespaced": true,
"kind": "LimitRange"
},
{
"name": "namespaces",
"namespaced": false,
"kind": "Namespace"
},
{
"name": "namespaces/finalize",
"namespaced": false,
"kind": "Namespace"
},
{
"name": "namespaces/status",
"namespaced": false,
"kind": "Namespace"
},
{
"name": "nodes",
"namespaced": false,
"kind": "Node"
},
{
"name": "nodes/proxy",
"namespaced": false,
"kind": "Node"
},
{
"name": "nodes/status",
"namespaced": false,
"kind": "Node"
},
{
"name": "persistentvolumeclaims",
"namespaced": true,
"kind": "PersistentVolumeClaim"
},
{
"name": "persistentvolumeclaims/status",
"namespaced": true,
"kind": "PersistentVolumeClaim"
},
{
"name": "persistentvolumes",
"namespaced": false,
"kind": "PersistentVolume"
},
{
"name": "persistentvolumes/status",
"namespaced": false,
"kind": "PersistentVolume"
},
{
"name": "pods",
"namespaced": true,
"kind": "Pod"
},
{
"name": "pods/attach",
"namespaced": true,
"kind": "Pod"
},
{
"name": "pods/binding",
"namespaced": true,
"kind": "Binding"
},
{
"name": "pods/exec",
"namespaced": true,
"kind": "Pod"
},
{
"name": "pods/log",
"namespaced": true,
"kind": "Pod"
},
{
"name": "pods/portforward",
"namespaced": true,
"kind": "Pod"
},
{
"name": "pods/proxy",
"namespaced": true,
"kind": "Pod"
},
{
"name": "pods/status",
"namespaced": true,
"kind": "Pod"
},
{
"name": "podtemplates",
"namespaced": true,
"kind": "PodTemplate"
},
{
"name": "replicationcontrollers",
"namespaced": true,
"kind": "ReplicationController"
},
{
"name": "replicationcontrollers/scale",
"namespaced": true,
"kind": "Scale"
},
{
"name": "replicationcontrollers/status",
"namespaced": true,
"kind": "ReplicationController"
},
{
"name": "resourcequotas",
"namespaced": true,
"kind": "ResourceQuota"
},
{
"name": "resourcequotas/status",
"namespaced": true,
"kind": "ResourceQuota"
},
{
"name": "secrets",
"namespaced": true,
"kind": "Secret"
},
{
"name": "serviceaccounts",
"namespaced": true,
"kind": "ServiceAccount"
},
{
"name": "services",
"namespaced": true,
"kind": "Service"
},
{
"name": "services/proxy",
"namespaced": true,
"kind": "Service"
},
{
"name": "services/status",
"namespaced": true,
"kind": "Service"
}
]
}`