/* Copyright 2017 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. */ package schedulingpolicy import ( "bytes" "encoding/json" "fmt" "html/template" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "reflect" "testing" extensionsv1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authentication/user" core "k8s.io/client-go/testing" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" ) func TestNewAdmissionController(t *testing.T) { tempfile, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("Unexpected error while creating temporary file: %v", err) } p := tempfile.Name() defer os.Remove(p) kubeconfig := ` clusters: - name: foo cluster: server: https://example.com users: - name: alice user: token: deadbeef contexts: - name: default context: cluster: foo user: alice current-context: default ` if _, err := tempfile.WriteString(kubeconfig); err != nil { t.Fatalf("Unexpected error while writing test kubeconfig file: %v", err) } tests := []struct { note string input string wantErr bool }{ {"no config", "", true}, {"bad json", `{"foo": `, true}, {"bad yaml", `{foo" `, true}, { "missing kubeconfig", `{"foo": {}}`, true, }, { "kubeconfig not found", `{ "kubeconfig": "/kube-federation-scheduling-policy-file-not-found-test" }`, true, }, { "bad retry backoff", fmt.Sprintf(` { "kubeconfig": %q, "retryBackoff": -1 } `, p), true, }, { "a valid config", fmt.Sprintf(` { "kubeconfig": %q } `, p), false, }, { "a valid config with retry backoff", fmt.Sprintf(` { "kubeconfig": %q, "retryBackoff": 200 } `, p), false, }, } for _, tc := range tests { var file io.Reader if tc.input == "" { file = nil } else { file = bytes.NewBufferString(tc.input) } _, err := newAdmissionController(file) if tc.wantErr && err == nil { t.Errorf("%v: Expected error", tc.note) } else if !tc.wantErr && err != nil { t.Errorf("%v: Unexpected error: %v", tc.note, err) } } } func TestAdmitQueryPayload(t *testing.T) { var body interface{} serve := func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("Unexpected error reading admission payload: %v", err) } // No errors or annotations. w.Write([]byte(`{}`)) } controller, err := newControllerWithTestServer(serve, true) if err != nil { t.Fatalf("Unexpected error while creating test admission controller/server: %v", err) } rs := makeReplicaSet() rs.Spec.MinReadySeconds = 100 attrs := makeAdmissionRecord(rs) err = controller.Admit(attrs) if err != nil { t.Fatalf("Unexpected error from admission controller: %v", err) } obj := body.(map[string]interface{}) metadata := obj["metadata"].(map[string]interface{}) spec := obj["spec"].(map[string]interface{}) name := metadata["name"].(string) minReadySeconds := spec["minReadySeconds"].(float64) expectedName := "myapp" if name != expectedName { t.Fatalf("Expected replicaset.metadata.name to be %v but got: %v", expectedName, name) } expectedMinReadySeconds := float64(100) if minReadySeconds != expectedMinReadySeconds { t.Fatalf("Expected replicaset.spec.minReadySeconds to be %v but got: %v", expectedMinReadySeconds, minReadySeconds) } } func TestAdmitFailInternal(t *testing.T) { serve := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } controller, err := newControllerWithTestServer(serve, false) if err != nil { t.Fatalf("Unexpected error while creating test admission controller/server: %v", err) } mockClient := &fake.Clientset{} mockClient.AddReactor("list", "configmaps", func(action core.Action) (bool, runtime.Object, error) { return true, nil, fmt.Errorf("unknown error") }) controller.SetInternalKubeClientSet(mockClient) attrs := makeAdmissionRecord(makeReplicaSet()) err = controller.Admit(attrs) if err == nil { t.Fatalf("Expected admission controller to fail closed") } } func TestAdmitPolicyDoesNotExist(t *testing.T) { controller, err := newControllerWithTestServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) }, false) if err != nil { t.Fatalf("Unexpected error while creating test admission controller/server: %v", err) } attrs := makeAdmissionRecord(makeReplicaSet()) err = controller.Admit(attrs) if err != nil { t.Fatalf("Expected admission controller to fail open but got error: %v", err) } } func TestAdmitFailClosed(t *testing.T) { tests := []struct { note string statusCode int body string }{ {"server error", 500, ""}, {"unmarshal error", 200, "{"}, {"undefined result", 404, ``}, {"policy errors", 200, `{"errors": ["conflicting replica-set-preferences"]}`}, } for _, tc := range tests { serve := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tc.statusCode) if len(tc.body) > 0 { w.Write([]byte(tc.body)) } } controller, err := newControllerWithTestServer(serve, true) if err != nil { t.Errorf("%v: Unexpected error while creating test admission controller/server: %v", tc.note, err) continue } obj := makeReplicaSet() attrs := admission.NewAttributesRecord(obj, nil, obj.GroupVersionKind(), obj.Namespace, obj.Name, api.Resource("replicasets").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) err = controller.Admit(attrs) if err == nil { t.Errorf("%v: Expected admission controller to fail closed", tc.note) } } } func TestAdmitRetries(t *testing.T) { var numQueries int controller, err := newControllerWithTestServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) numQueries++ }, true) if err != nil { t.Fatalf("Unexpected error while creating test admission controller/server: %v", err) } err = controller.Admit(makeAdmissionRecord(makeReplicaSet())) if err == nil { t.Fatalf("Expected admission controller to fail closed") } if numQueries <= 1 { t.Fatalf("Expected multiple queries/retries but got (numQueries): %v", numQueries) } } func TestAdmitSuccessWithAnnotationMerge(t *testing.T) { controller, err := newControllerWithTestServer(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(` { "annotations": { "foo": "bar-2" } } `)) }, true) if err != nil { t.Fatalf("Unexpected error while creating test admission controller/server: %v", err) } obj := makeReplicaSet() obj.Annotations = map[string]string{} obj.Annotations["foo"] = "bar" obj.Annotations["bar"] = "baz" attrs := admission.NewAttributesRecord(obj, nil, obj.GroupVersionKind(), obj.Namespace, obj.Name, api.Resource("replicasets").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) err = controller.Admit(attrs) if err != nil { t.Fatalf("Unexpected error from admission controller: %v", err) } annotations := attrs.GetObject().(*extensionsv1.ReplicaSet).Annotations expected := map[string]string{ "foo": "bar-2", "bar": "baz", } if !reflect.DeepEqual(annotations, expected) { t.Fatalf("Expected annotations to be %v but got: %v", expected, annotations) } } func newControllerWithTestServer(f func(w http.ResponseWriter, r *http.Request), policiesExist bool) (*admissionController, error) { server, err := newTestServer(f) if err != nil { return nil, err } kubeConfigFile, err := makeKubeConfigFile(server.URL, "/some/path/to/decision") if err != nil { return nil, err } defer os.Remove(kubeConfigFile) configFile, err := makeAdmissionControlConfigFile(kubeConfigFile) if err != nil { return nil, err } defer os.Remove(configFile) file, err := os.Open(configFile) if err != nil { return nil, err } defer file.Close() controller, err := newAdmissionController(file) if err != nil { return nil, err } mockClient := &fake.Clientset{} var items []api.ConfigMap if policiesExist { items = append(items, api.ConfigMap{}) } mockClient.AddReactor("list", "configmaps", func(action core.Action) (bool, runtime.Object, error) { if action.GetNamespace() == policyConfigMapNamespace { return true, &api.ConfigMapList{Items: items}, nil } return true, nil, nil }) controller.SetInternalKubeClientSet(mockClient) return controller, nil } func newTestServer(f func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) { server := httptest.NewUnstartedServer(http.HandlerFunc(f)) server.Start() return server, nil } func makeAdmissionControlConfigFile(kubeConfigFile string) (string, error) { tempfile, err := ioutil.TempFile("", "") if err != nil { return "", err } p := tempfile.Name() configFileTmpl := ` kubeconfig: {{ .KubeConfigFile }} retryBackoff: {{ .RetryBackoff }} ` type configFileTemplateInput struct { KubeConfigFile string RetryBackoff int } input := configFileTemplateInput{ KubeConfigFile: kubeConfigFile, RetryBackoff: 1, } tmpl, err := template.New("scheduling-policy-config").Parse(configFileTmpl) if err != nil { return "", err } if err := tmpl.Execute(tempfile, input); err != nil { return "", err } return p, nil } func makeKubeConfigFile(baseURL, path string) (string, error) { tempfile, err := ioutil.TempFile("", "") if err != nil { return "", err } p := tempfile.Name() kubeConfigTmpl := ` clusters: - name: test cluster: server: {{ .BaseURL }}{{ .Path }} users: - name: alice user: token: deadbeef contexts: - name: default context: cluster: test user: alice current-context: default` type kubeConfigTemplateInput struct { BaseURL string Path string } input := kubeConfigTemplateInput{ BaseURL: baseURL, Path: path, } tmpl, err := template.New("kubeconfig").Parse(kubeConfigTmpl) if err != nil { return "", err } if err := tmpl.Execute(tempfile, input); err != nil { return "", err } return p, nil } func makeAdmissionRecord(obj *extensionsv1.ReplicaSet) admission.Attributes { return admission.NewAttributesRecord(obj, nil, obj.GroupVersionKind(), obj.Namespace, obj.Name, api.Resource("replicasets").WithVersion("version"), "", admission.Create, &user.DefaultInfo{}) } func makeReplicaSet() *extensionsv1.ReplicaSet { return &extensionsv1.ReplicaSet{ TypeMeta: metav1.TypeMeta{ Kind: "ReplicaSet", APIVersion: "extensions/v1beta1", }, ObjectMeta: metav1.ObjectMeta{ Name: "myapp", }, Spec: extensionsv1.ReplicaSetSpec{}, } }