pkg/scheduler/admission/podnodeconstraints/admission_test.go
a15d066f
 package podnodeconstraints
 
 import (
 	"bytes"
 	"fmt"
 	"reflect"
 	"testing"
 
 	_ "github.com/openshift/origin/pkg/api/install"
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
 	"github.com/openshift/origin/pkg/authorization/authorizer"
 	oadmission "github.com/openshift/origin/pkg/cmd/server/admission"
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
 	"github.com/openshift/origin/pkg/scheduler/admission/podnodeconstraints/api"
4cf2744c
 	securityapi "github.com/openshift/origin/pkg/security/api"
a15d066f
 
 	admission "k8s.io/kubernetes/pkg/admission"
 	kapi "k8s.io/kubernetes/pkg/api"
 	"k8s.io/kubernetes/pkg/api/unversioned"
 	"k8s.io/kubernetes/pkg/apimachinery/registered"
997787d8
 	"k8s.io/kubernetes/pkg/apis/batch"
a15d066f
 	"k8s.io/kubernetes/pkg/apis/extensions"
 	"k8s.io/kubernetes/pkg/auth/user"
 	"k8s.io/kubernetes/pkg/runtime"
 	"k8s.io/kubernetes/pkg/serviceaccount"
 	"k8s.io/kubernetes/pkg/util/sets"
 )
 
 func TestPodNodeConstraints(t *testing.T) {
 	ns := kapi.NamespaceDefault
 	tests := []struct {
 		config           *api.PodNodeConstraintsConfig
 		resource         runtime.Object
 		kind             unversioned.GroupKind
 		groupresource    unversioned.GroupResource
 		userinfo         user.Info
 		reviewResponse   *authorizationapi.SubjectAccessReviewResponse
 		expectedResource string
 		expectedErrorMsg string
 	}{
 		// 0: expect unspecified defaults to not error
 		{
 			config:           emptyConfig(),
 			resource:         defaultPod(),
 			userinfo:         serviceaccount.UserInfo("", "", ""),
 			reviewResponse:   reviewResponse(false, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "",
 		},
 		// 1: expect nodeSelector to error with user which lacks "pods/binding" access
 		{
 			config:           testConfig(),
 			resource:         nodeSelectorPod(),
 			userinfo:         serviceaccount.UserInfo("", "", ""),
 			reviewResponse:   reviewResponse(false, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "node selection by label(s) [bogus] is prohibited by policy for your role",
 		},
 		// 2: expect nodeName to fail with user that lacks "pods/binding" access
 		{
 			config:           testConfig(),
 			resource:         nodeNamePod(),
 			userinfo:         serviceaccount.UserInfo("herpy", "derpy", ""),
 			reviewResponse:   reviewResponse(false, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "node selection by nodeName is prohibited by policy for your role",
 		},
 		// 3: expect nodeName and nodeSelector to fail with user that lacks "pods/binding" access
 		{
 			config:           testConfig(),
 			resource:         nodeNameNodeSelectorPod(),
 			userinfo:         serviceaccount.UserInfo("herpy", "derpy", ""),
 			reviewResponse:   reviewResponse(false, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "node selection by nodeName and label(s) [bogus] is prohibited by policy for your role",
 		},
 		// 4: expect nodeSelector to succeed with user that has "pods/binding" access
 		{
 			config:           testConfig(),
 			resource:         nodeSelectorPod(),
 			userinfo:         serviceaccount.UserInfo("openshift-infra", "daemonset-controller", ""),
 			reviewResponse:   reviewResponse(true, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "",
 		},
 		// 5: expect nodeName to succeed with user that has "pods/binding" access
 		{
 			config:           testConfig(),
 			resource:         nodeNamePod(),
 			userinfo:         serviceaccount.UserInfo("openshift-infra", "daemonset-controller", ""),
 			reviewResponse:   reviewResponse(true, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "",
 		},
 		// 6: expect nil config to bypass admission
 		{
 			config:           nil,
 			resource:         defaultPod(),
 			userinfo:         serviceaccount.UserInfo("", "", ""),
 			reviewResponse:   reviewResponse(false, ""),
 			expectedResource: "pods/binding",
 			expectedErrorMsg: "",
 		},
 	}
 	for i, tc := range tests {
 		var expectedError error
 		errPrefix := fmt.Sprintf("%d", i)
 		prc := NewPodNodeConstraints(tc.config)
 		prc.(oadmission.WantsAuthorizer).SetAuthorizer(fakeAuthorizer(t))
 		err := prc.(oadmission.Validator).Validate()
 		if err != nil {
 			checkAdmitError(t, err, expectedError, errPrefix)
 			continue
 		}
8201efbe
 		attrs := admission.NewAttributesRecord(tc.resource, nil, kapi.Kind("Pod").WithVersion("version"), ns, "test", kapi.Resource("pods").WithVersion("version"), "", admission.Create, tc.userinfo)
a15d066f
 		if tc.expectedErrorMsg != "" {
 			expectedError = admission.NewForbidden(attrs, fmt.Errorf(tc.expectedErrorMsg))
 		}
 		err = prc.Admit(attrs)
 		checkAdmitError(t, err, expectedError, errPrefix)
 	}
 }
 
 func TestPodNodeConstraintsPodUpdate(t *testing.T) {
 	ns := kapi.NamespaceDefault
 	var expectedError error
 	errPrefix := "PodUpdate"
 	prc := NewPodNodeConstraints(testConfig())
 	prc.(oadmission.WantsAuthorizer).SetAuthorizer(fakeAuthorizer(t))
 	err := prc.(oadmission.Validator).Validate()
 	if err != nil {
 		checkAdmitError(t, err, expectedError, errPrefix)
 		return
 	}
8201efbe
 	attrs := admission.NewAttributesRecord(nodeNamePod(), nodeNamePod(), kapi.Kind("Pod").WithVersion("version"), ns, "test", kapi.Resource("pods").WithVersion("version"), "", admission.Update, serviceaccount.UserInfo("", "", ""))
a15d066f
 	err = prc.Admit(attrs)
 	checkAdmitError(t, err, expectedError, errPrefix)
 }
 
 func TestPodNodeConstraintsNonHandledResources(t *testing.T) {
 	ns := kapi.NamespaceDefault
 	errPrefix := "ResourceQuotaTest"
 	var expectedError error
 	prc := NewPodNodeConstraints(testConfig())
 	prc.(oadmission.WantsAuthorizer).SetAuthorizer(fakeAuthorizer(t))
 	err := prc.(oadmission.Validator).Validate()
 	if err != nil {
 		checkAdmitError(t, err, expectedError, errPrefix)
 		return
 	}
8201efbe
 	attrs := admission.NewAttributesRecord(resourceQuota(), nil, kapi.Kind("ResourceQuota").WithVersion("version"), ns, "test", kapi.Resource("resourcequotas").WithVersion("version"), "", admission.Create, serviceaccount.UserInfo("", "", ""))
a15d066f
 	err = prc.Admit(attrs)
 	checkAdmitError(t, err, expectedError, errPrefix)
 }
 
 func TestPodNodeConstraintsResources(t *testing.T) {
 	ns := kapi.NamespaceDefault
 	testconfigs := []struct {
 		config         *api.PodNodeConstraintsConfig
 		userinfo       user.Info
 		reviewResponse *authorizationapi.SubjectAccessReviewResponse
 	}{
 		{
 			config:         testConfig(),
 			userinfo:       serviceaccount.UserInfo("", "", ""),
 			reviewResponse: reviewResponse(false, ""),
 		},
 	}
 	testresources := []struct {
 		resource      func(bool) runtime.Object
 		kind          unversioned.GroupKind
 		groupresource unversioned.GroupResource
 		prefix        string
 	}{
 		{
 			resource:      replicationController,
 			kind:          kapi.Kind("ReplicationController"),
 			groupresource: kapi.Resource("replicationcontrollers"),
 			prefix:        "ReplicationController",
 		},
 		{
 			resource:      deployment,
 			kind:          extensions.Kind("Deployment"),
 			groupresource: extensions.Resource("deployments"),
 			prefix:        "Deployment",
 		},
 		{
 			resource:      replicaSet,
 			kind:          extensions.Kind("ReplicaSet"),
 			groupresource: extensions.Resource("replicasets"),
 			prefix:        "ReplicaSet",
 		},
 		{
 			resource:      job,
 			kind:          extensions.Kind("Job"),
 			groupresource: extensions.Resource("jobs"),
 			prefix:        "Job",
 		},
 		{
997787d8
 			resource:      job,
 			kind:          batch.Kind("Job"),
 			groupresource: batch.Resource("jobs"),
 			prefix:        "Job",
 		},
 		{
a15d066f
 			resource:      deploymentConfig,
 			kind:          deployapi.Kind("DeploymentConfig"),
 			groupresource: deployapi.Resource("deploymentconfigs"),
 			prefix:        "DeploymentConfig",
 		},
 		{
 			resource:      podTemplate,
 			kind:          deployapi.Kind("PodTemplate"),
 			groupresource: deployapi.Resource("podtemplates"),
 			prefix:        "PodTemplate",
 		},
4cf2744c
 		{
 			resource:      podSecurityPolicySubjectReview,
 			kind:          securityapi.Kind("PodSecurityPolicySubjectReview"),
 			groupresource: securityapi.Resource("podsecuritypolicysubjectreviews"),
 			prefix:        "PodSecurityPolicy",
 		},
 		{
 			resource:      podSecurityPolicySelfSubjectReview,
 			kind:          securityapi.Kind("PodSecurityPolicySelfSubjectReview"),
 			groupresource: securityapi.Resource("podsecuritypolicyselfsubjectreviews"),
 			prefix:        "PodSecurityPolicy",
 		},
 		{
 			resource:      podSecurityPolicyReview,
 			kind:          securityapi.Kind("PodSecurityPolicyReview"),
 			groupresource: securityapi.Resource("podsecuritypolicyreviews"),
 			prefix:        "PodSecurityPolicy",
 		},
a15d066f
 	}
 	testparams := []struct {
 		nodeselector     bool
 		expectedErrorMsg string
 		prefix           string
 	}{
 		{
 			nodeselector:     true,
 			expectedErrorMsg: "node selection by label(s) [bogus] is prohibited by policy for your role",
 			prefix:           "with nodeSelector",
 		},
 		{
 			nodeselector:     false,
 			expectedErrorMsg: "",
 			prefix:           "without nodeSelector",
 		},
 	}
 	testops := []struct {
 		operation admission.Operation
 	}{
 		{
 			operation: admission.Create,
 		},
 		{
 			operation: admission.Update,
 		},
 	}
 	for _, tc := range testconfigs {
 		for _, tr := range testresources {
 			for _, tp := range testparams {
 				for _, top := range testops {
 					var expectedError error
 					errPrefix := fmt.Sprintf("%s; %s; %s", tr.prefix, tp.prefix, top.operation)
 					prc := NewPodNodeConstraints(tc.config)
 					prc.(oadmission.WantsAuthorizer).SetAuthorizer(fakeAuthorizer(t))
 					err := prc.(oadmission.Validator).Validate()
 					if err != nil {
 						checkAdmitError(t, err, expectedError, errPrefix)
 						continue
 					}
8201efbe
 					attrs := admission.NewAttributesRecord(tr.resource(tp.nodeselector), nil, tr.kind.WithVersion("version"), ns, "test", tr.groupresource.WithVersion("version"), "", top.operation, tc.userinfo)
a15d066f
 					if tp.expectedErrorMsg != "" {
 						expectedError = admission.NewForbidden(attrs, fmt.Errorf(tp.expectedErrorMsg))
 					}
 					err = prc.Admit(attrs)
 					checkAdmitError(t, err, expectedError, errPrefix)
 				}
 			}
 		}
 	}
 }
 
 func emptyConfig() *api.PodNodeConstraintsConfig {
 	return &api.PodNodeConstraintsConfig{}
 }
 
 func testConfig() *api.PodNodeConstraintsConfig {
 	return &api.PodNodeConstraintsConfig{
 		NodeSelectorLabelBlacklist: []string{"bogus"},
 	}
 }
 
 func defaultPod() *kapi.Pod {
 	pod := &kapi.Pod{}
 	return pod
 }
 
 func pod(ns bool) runtime.Object {
 	pod := &kapi.Pod{}
 	if ns {
 		pod.Spec.NodeSelector = map[string]string{"bogus": "frank"}
 	}
 	return pod
 }
 
 func nodeNameNodeSelectorPod() *kapi.Pod {
 	pod := &kapi.Pod{}
 	pod.Spec.NodeName = "frank"
 	pod.Spec.NodeSelector = map[string]string{"bogus": "frank"}
 	return pod
 }
 
 func nodeNamePod() *kapi.Pod {
 	pod := &kapi.Pod{}
 	pod.Spec.NodeName = "frank"
 	return pod
 }
 
 func nodeSelectorPod() *kapi.Pod {
 	pod := &kapi.Pod{}
 	pod.Spec.NodeSelector = map[string]string{"bogus": "frank"}
 	return pod
 }
 
 func emptyNodeSelectorPod() *kapi.Pod {
 	pod := &kapi.Pod{}
 	pod.Spec.NodeSelector = map[string]string{}
 	return pod
 }
 
4cf2744c
 func podSpec(setNodeSelector bool) *kapi.PodSpec {
 	ps := &kapi.PodSpec{}
 	if setNodeSelector {
 		ps.NodeSelector = map[string]string{"bogus": "frank"}
 	}
 	return ps
 }
 
a15d066f
 func podTemplateSpec(setNodeSelector bool) *kapi.PodTemplateSpec {
 	pts := &kapi.PodTemplateSpec{}
 	if setNodeSelector {
 		pts.Spec.NodeSelector = map[string]string{"bogus": "frank"}
 	}
 	return pts
 }
 
 func podTemplate(setNodeSelector bool) runtime.Object {
 	pt := &kapi.PodTemplate{}
 	pt.Template = *podTemplateSpec(setNodeSelector)
 	return pt
 }
 
 func replicationController(setNodeSelector bool) runtime.Object {
 	rc := &kapi.ReplicationController{}
 	rc.Spec.Template = podTemplateSpec(setNodeSelector)
 	return rc
 }
 
 func deployment(setNodeSelector bool) runtime.Object {
 	d := &extensions.Deployment{}
 	d.Spec.Template = *podTemplateSpec(setNodeSelector)
 	return d
 }
 
 func replicaSet(setNodeSelector bool) runtime.Object {
 	rs := &extensions.ReplicaSet{}
997787d8
 	rs.Spec.Template = *podTemplateSpec(setNodeSelector)
a15d066f
 	return rs
 }
 
 func job(setNodeSelector bool) runtime.Object {
508f280e
 	j := &batch.Job{}
a15d066f
 	j.Spec.Template = *podTemplateSpec(setNodeSelector)
 	return j
 }
 
 func resourceQuota() runtime.Object {
 	rq := &kapi.ResourceQuota{}
 	return rq
 }
 
 func deploymentConfig(setNodeSelector bool) runtime.Object {
 	dc := &deployapi.DeploymentConfig{}
 	dc.Spec.Template = podTemplateSpec(setNodeSelector)
 	return dc
 }
 
4cf2744c
 func podSecurityPolicySubjectReview(setNodeSelector bool) runtime.Object {
 	pspsr := &securityapi.PodSecurityPolicySubjectReview{}
 	pspsr.Spec.PodSpec = *podSpec(setNodeSelector)
 	return pspsr
 }
 
 func podSecurityPolicySelfSubjectReview(setNodeSelector bool) runtime.Object {
 	pspssr := &securityapi.PodSecurityPolicySelfSubjectReview{}
 	pspssr.Spec.PodSpec = *podSpec(setNodeSelector)
 	return pspssr
 }
 
 func podSecurityPolicyReview(setNodeSelector bool) runtime.Object {
 	pspr := &securityapi.PodSecurityPolicyReview{}
 	pspr.Spec.PodSpec = *podSpec(setNodeSelector)
 	return pspr
 }
 
a15d066f
 func checkAdmitError(t *testing.T, err error, expectedError error, prefix string) {
 	switch {
 	case expectedError == nil && err == nil:
 		// continue
 	case expectedError != nil && err != nil && err.Error() != expectedError.Error():
 		t.Errorf("%s: expected error %q, got: %q", prefix, expectedError.Error(), err.Error())
 	case expectedError == nil && err != nil:
 		t.Errorf("%s: expected no error, got: %q", prefix, err.Error())
 	case expectedError != nil && err == nil:
 		t.Errorf("%s: expected error %q, no error recieved", prefix, expectedError.Error())
 	}
 }
 
 type fakeTestAuthorizer struct {
 	t *testing.T
 }
 
 func fakeAuthorizer(t *testing.T) authorizer.Authorizer {
 	return &fakeTestAuthorizer{
 		t: t,
 	}
 }
 
 func (a *fakeTestAuthorizer) Authorize(ctx kapi.Context, passedAttributes authorizer.AuthorizationAttributes) (bool, string, error) {
 	a.t.Logf("Authorize: ctx: %#v", ctx)
 	ui, ok := kapi.UserFrom(ctx)
 	if !ok {
 		return false, "", fmt.Errorf("No valid UserInfo for Context")
 	}
 	// User with pods/bindings. permission:
 	if ui.GetName() == "system:serviceaccount:openshift-infra:daemonset-controller" {
 		return true, "", nil
 	}
 	// User without pods/bindings. permission:
 	return false, "", nil
 }
 
 func (a *fakeTestAuthorizer) GetAllowedSubjects(ctx kapi.Context, attributes authorizer.AuthorizationAttributes) (sets.String, sets.String, error) {
 	return nil, nil, nil
 }
 
 func reviewResponse(allowed bool, msg string) *authorizationapi.SubjectAccessReviewResponse {
 	return &authorizationapi.SubjectAccessReviewResponse{
 		Allowed: allowed,
 		Reason:  msg,
 	}
 }
 
 func TestReadConfig(t *testing.T) {
 	configStr := `apiVersion: v1
 kind: PodNodeConstraintsConfig
 nodeSelectorLabelBlacklist:
   - bogus
   - foo
 `
 	buf := bytes.NewBufferString(configStr)
 	config, err := readConfig(buf)
 	if err != nil {
 		t.Fatalf("unexpected error reading config: %v", err)
 	}
 	if len(config.NodeSelectorLabelBlacklist) == 0 {
 		t.Fatalf("NodeSelectorLabelBlacklist didn't take specified value")
 	}
 }
 
 func TestResourcesToCheck(t *testing.T) {
 	known := knownResourceKinds()
 	detected := kindsWithPodSpecs()
 	for _, k := range detected {
 		if _, isKnown := known[k]; !isKnown {
 			t.Errorf("Unknown resource kind %s contains a PodSpec", (&k).String())
 			continue
 		}
 		delete(known, k)
 	}
 	if len(known) > 0 {
 		t.Errorf("These known kinds were not detected to have a PodSpec: %#v", known)
 	}
 }
 
 var podSpecType = reflect.TypeOf(kapi.PodSpec{})
 
51a1e57a
 func hasPodSpec(visited map[reflect.Type]bool, t reflect.Type) bool {
 	if visited[t] {
 		return false
 	}
 	visited[t] = true
 
a15d066f
 	switch t.Kind() {
 	case reflect.Struct:
 		if t == podSpecType {
 			return true
 		}
c2147930
 		for i := 0; i < t.NumField(); i++ {
51a1e57a
 			if hasPodSpec(visited, t.Field(i).Type) {
a15d066f
 				return true
 			}
 		}
 	case reflect.Array, reflect.Slice, reflect.Chan, reflect.Map, reflect.Ptr:
51a1e57a
 		return hasPodSpec(visited, t.Elem())
a15d066f
 	}
 	return false
 }
 
 func internalGroupVersions() []unversioned.GroupVersion {
 	groupVersions := registered.EnabledVersions()
 	groups := map[string]struct{}{}
 	for _, gv := range groupVersions {
 		groups[gv.Group] = struct{}{}
 	}
 	result := []unversioned.GroupVersion{}
 	for group := range groups {
 		result = append(result, unversioned.GroupVersion{Group: group, Version: runtime.APIVersionInternal})
 	}
 	return result
 }
 
 func isList(t reflect.Type) bool {
 	if t.Kind() != reflect.Struct {
 		return false
 	}
 
 	_, hasListMeta := t.FieldByName("ListMeta")
 	return hasListMeta
 }
 
 func kindsWithPodSpecs() []unversioned.GroupKind {
 	result := []unversioned.GroupKind{}
 	for _, gv := range internalGroupVersions() {
 		knownTypes := kapi.Scheme.KnownTypes(gv)
 		for kind, knownType := range knownTypes {
51a1e57a
 			if !isList(knownType) && hasPodSpec(map[reflect.Type]bool{}, knownType) {
a15d066f
 				result = append(result, unversioned.GroupKind{Group: gv.Group, Kind: kind})
 			}
 		}
 	}
51a1e57a
 
a15d066f
 	return result
 }
 
 func knownResourceKinds() map[unversioned.GroupKind]struct{} {
 	result := map[unversioned.GroupKind]struct{}{}
 	for _, ka := range resourcesToCheck {
 		result[ka] = struct{}{}
 	}
 	for _, ki := range resourcesToIgnore {
 		result[ki] = struct{}{}
 	}
 	return result
 }