Browse code

Added support for project node selector

Project node selector will constrain all pods within a project
to a pool of nodes that satisfies given label selector.

- Cluster admin can optionally specify global default project node selector.
Either pass '--project-node-selector=<selector>' flag as command line argument
to openshift start or pass 'projectNodeSelector: <selector>' entry in the master config.
- User can optionally specify node selector for the project.
REST API: Pass node selector as annotation to the project.
Example:
{
kind: 'Project',
Annotations: map[string]string{
'nodeSelector': <node_label_selector>
}
...
}
CLI: osadm new-project <name> --node-selector=<node_label_selector>
- Any project with no node selector will use global node selector if present.

Ravi Sankar Penta authored on 2015/05/01 07:31:20
Showing 25 changed files
... ...
@@ -20,9 +20,10 @@ import (
20 20
 const NewProjectRecommendedName = "new-project"
21 21
 
22 22
 type NewProjectOptions struct {
23
-	ProjectName string
24
-	DisplayName string
25
-	Description string
23
+	ProjectName  string
24
+	DisplayName  string
25
+	Description  string
26
+	NodeSelector string
26 27
 
27 28
 	Client client.Interface
28 29
 
... ...
@@ -59,6 +60,7 @@ func NewCmdNewProject(name, fullName string, f *clientcmd.Factory, out io.Writer
59 59
 	cmd.Flags().StringVar(&options.AdminUser, "admin", "", "project admin username")
60 60
 	cmd.Flags().StringVar(&options.DisplayName, "display-name", "", "project display name")
61 61
 	cmd.Flags().StringVar(&options.Description, "description", "", "project description")
62
+	cmd.Flags().StringVar(&options.NodeSelector, "node-selector", "", "Restrict pods onto nodes matching given label selector")
62 63
 
63 64
 	return cmd
64 65
 }
... ...
@@ -86,6 +88,7 @@ func (o *NewProjectOptions) Run() error {
86 86
 	project.Annotations = make(map[string]string)
87 87
 	project.Annotations["description"] = o.Description
88 88
 	project.Annotations["displayName"] = o.DisplayName
89
+	project.Annotations["nodeSelector"] = o.NodeSelector
89 90
 	project, err := o.Client.Projects().Create(project)
90 91
 	if err != nil {
91 92
 		return err
... ...
@@ -18,9 +18,10 @@ import (
18 18
 )
19 19
 
20 20
 type NewProjectOptions struct {
21
-	ProjectName string
22
-	DisplayName string
23
-	Description string
21
+	ProjectName  string
22
+	DisplayName  string
23
+	Description  string
24
+	NodeSelector string
24 25
 
25 26
 	Client client.Interface
26 27
 
... ...
@@ -40,8 +41,8 @@ After your project is created you can switch to it using %[2]s <project name>.`
40 40
 	requestProject_example = `  // Create a new project with minimal information
41 41
   $ %[1]s web-team-dev
42 42
 
43
-  // Create a new project with a description
44
-  $ %[1]s web-team-dev --display-name="Web Team Development" --description="Development project for the web team."`
43
+  // Create a new project with a description and node selector
44
+  $ %[1]s web-team-dev --display-name="Web Team Development" --description="Development project for the web team." --node-selector="env=dev"`
45 45
 )
46 46
 
47 47
 func NewCmdRequestProject(name, fullName, oscLoginName, oscProjectName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
... ...
@@ -49,7 +50,7 @@ func NewCmdRequestProject(name, fullName, oscLoginName, oscProjectName string, f
49 49
 	options.Out = out
50 50
 
51 51
 	cmd := &cobra.Command{
52
-		Use:     fmt.Sprintf("%s NAME [--display-name=DISPLAYNAME] [--description=DESCRIPTION]", name),
52
+		Use:     fmt.Sprintf("%s NAME [--display-name=DISPLAYNAME] [--description=DESCRIPTION] [--node-selector=<label selector>]", name),
53 53
 		Short:   "Request a new project",
54 54
 		Long:    fmt.Sprintf(requestProject_long, oscLoginName, oscProjectName),
55 55
 		Example: fmt.Sprintf(requestProject_example, fullName),
... ...
@@ -71,6 +72,7 @@ func NewCmdRequestProject(name, fullName, oscLoginName, oscProjectName string, f
71 71
 
72 72
 	cmd.Flags().StringVar(&options.DisplayName, "display-name", "", "project display name")
73 73
 	cmd.Flags().StringVar(&options.Description, "description", "", "project description")
74
+	cmd.Flags().StringVar(&options.NodeSelector, "node-selector", "", "Restrict pods onto nodes matching given label selector")
74 75
 
75 76
 	return cmd
76 77
 }
... ...
@@ -105,6 +107,7 @@ func (o *NewProjectOptions) Run() error {
105 105
 	projectRequest.DisplayName = o.DisplayName
106 106
 	projectRequest.Annotations = make(map[string]string)
107 107
 	projectRequest.Annotations["description"] = o.Description
108
+	projectRequest.Annotations["nodeSelector"] = o.NodeSelector
108 109
 
109 110
 	project, err := o.Client.ProjectRequests().Create(projectRequest)
110 111
 	if err != nil {
... ...
@@ -451,11 +451,18 @@ func (d *ProjectDescriber) Describe(namespace, name string) (string, error) {
451 451
 	if err != nil {
452 452
 		return "", err
453 453
 	}
454
+	nodeSelector := ""
455
+	if len(project.ObjectMeta.Annotations) > 0 {
456
+		if ns, ok := project.ObjectMeta.Annotations["nodeSelector"]; ok {
457
+			nodeSelector = ns
458
+		}
459
+	}
454 460
 
455 461
 	return tabbedString(func(out *tabwriter.Writer) error {
456 462
 		formatMeta(out, project.ObjectMeta)
457 463
 		formatString(out, "Display Name", project.Annotations["displayName"])
458 464
 		formatString(out, "Status", project.Status.Phase)
465
+		formatString(out, "Node Selector", nodeSelector)
459 466
 		return nil
460 467
 	})
461 468
 }
... ...
@@ -89,6 +89,8 @@ func GetMasterFileReferences(config *MasterConfig) []*string {
89 89
 
90 90
 	refs = append(refs, &config.PolicyConfig.BootstrapPolicyFile)
91 91
 
92
+	refs = append(refs, &config.ProjectNodeSelector)
93
+
92 94
 	return refs
93 95
 }
94 96
 
... ...
@@ -85,6 +85,8 @@ type MasterConfig struct {
85 85
 	// PolicyConfig holds information about where to locate critical pieces of bootstrapping policy
86 86
 	PolicyConfig PolicyConfig
87 87
 
88
+	// ProjectNodeSelector holds default project node label selector
89
+	ProjectNodeSelector string `json:"projectNodeSelector,omitempty"`
88 90
 	// ProjectRequestConfig holds information about how to handle new project requests
89 91
 	ProjectRequestConfig ProjectRequestConfig
90 92
 }
... ...
@@ -81,6 +81,8 @@ type MasterConfig struct {
81 81
 
82 82
 	PolicyConfig PolicyConfig `json:"policyConfig"`
83 83
 
84
+	// ProjectNodeSelector holds default project node label selector
85
+	ProjectNodeSelector string `json:"projectNodeSelector,omitempty"`
84 86
 	// ProjectRequestConfig holds information about how to handle new project requests
85 87
 	ProjectRequestConfig ProjectRequestConfig `json:"projectRequestConfig"`
86 88
 }
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"net/url"
7 7
 	"strings"
8 8
 
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
9 10
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
10 11
 
11 12
 	"github.com/openshift/origin/pkg/cmd/server/api"
... ...
@@ -88,6 +89,8 @@ func ValidateMasterConfig(config *api.MasterConfig) fielderrors.ValidationErrorL
88 88
 		allErrs = append(allErrs, ValidateOAuthConfig(config.OAuthConfig).Prefix("oauthConfig")...)
89 89
 	}
90 90
 
91
+	allErrs = append(allErrs, ValidateProjectNodeSelector(config.ProjectNodeSelector)...)
92
+
91 93
 	allErrs = append(allErrs, ValidateServingInfo(config.ServingInfo).Prefix("servingInfo")...)
92 94
 
93 95
 	allErrs = append(allErrs, ValidateProjectRequestConfig(config.ProjectRequestConfig).Prefix("projectRequestConfig")...)
... ...
@@ -108,6 +111,19 @@ func ValidateEtcdStorageConfig(config api.EtcdStorageConfig) fielderrors.Validat
108 108
 	return allErrs
109 109
 }
110 110
 
111
+func ValidateProjectNodeSelector(nodeSelector string) fielderrors.ValidationErrorList {
112
+	allErrs := fielderrors.ValidationErrorList{}
113
+
114
+	if len(nodeSelector) > 0 {
115
+		_, err := labels.Parse(nodeSelector)
116
+		if err != nil {
117
+			allErrs = append(allErrs, fielderrors.NewFieldInvalid("projectNodeSelector", nodeSelector, "must be a valid label selector"))
118
+		}
119
+	}
120
+
121
+	return allErrs
122
+}
123
+
111 124
 func ValidateAssetConfig(config *api.AssetConfig) fielderrors.ValidationErrorList {
112 125
 	allErrs := fielderrors.ValidationErrorList{}
113 126
 
... ...
@@ -26,6 +26,7 @@ import (
26 26
 	"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory"
27 27
 
28 28
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/namespace"
29
+	originscheduler "github.com/openshift/origin/pkg/scheduler"
29 30
 )
30 31
 
31 32
 const (
... ...
@@ -171,5 +172,5 @@ func (c *MasterConfig) createSchedulerConfig() (*scheduler.Config, error) {
171 171
 	}
172 172
 
173 173
 	// if the config file isn't provided, use the default provider
174
-	return configFactory.CreateFromProvider(factory.DefaultProvider)
174
+	return configFactory.CreateFromProvider(originscheduler.DefaultProvider)
175 175
 }
... ...
@@ -74,6 +74,7 @@ import (
74 74
 	clientetcd "github.com/openshift/origin/pkg/oauth/registry/oauthclient/etcd"
75 75
 	clientauthetcd "github.com/openshift/origin/pkg/oauth/registry/oauthclientauthorization/etcd"
76 76
 	projectapi "github.com/openshift/origin/pkg/project/api"
77
+	projectcache "github.com/openshift/origin/pkg/project/cache"
77 78
 	projectcontroller "github.com/openshift/origin/pkg/project/controller"
78 79
 	projectproxy "github.com/openshift/origin/pkg/project/registry/project/proxy"
79 80
 	projectrequeststorage "github.com/openshift/origin/pkg/project/registry/projectrequest/delegated"
... ...
@@ -755,6 +756,12 @@ func (c *MasterConfig) RunDNSServer() {
755 755
 	glog.Infof("OpenShift DNS listening at %s", c.Options.DNSConfig.BindAddress)
756 756
 }
757 757
 
758
+// RunProjectCache populates project cache, used by scheduler and project admission controller.
759
+func (c *MasterConfig) RunProjectCache() {
760
+	glog.Infof("Using default project node label selector: %s", c.Options.ProjectNodeSelector)
761
+	projectcache.RunProjectCache(c.PrivilegedLoopbackKubernetesClient, c.Options.ProjectNodeSelector)
762
+}
763
+
758 764
 // RunBuildController starts the build sync loop for builds and buildConfig processing.
759 765
 func (c *MasterConfig) RunBuildController() {
760 766
 	// initialize build controller
... ...
@@ -47,6 +47,7 @@ type MasterArgs struct {
47 47
 	KubeConnectionArgs *KubeConnectionArgs
48 48
 
49 49
 	SchedulerConfigFile string
50
+	ProjectNodeSelector string
50 51
 }
51 52
 
52 53
 // BindMasterArgs binds the options to the flags with prefix + default flag names
... ...
@@ -60,6 +61,7 @@ func BindMasterArgs(args *MasterArgs, flags *pflag.FlagSet, prefix string) {
60 60
 
61 61
 	flags.Var(&args.NodeList, prefix+"nodes", "The hostnames of each node. This currently must be specified up front. Comma delimited list")
62 62
 	flags.Var(&args.CORSAllowedOrigins, prefix+"cors-allowed-origins", "List of allowed origins for CORS, comma separated.  An allowed origin can be a regular expression to support subdomain matching.  CORS is enabled for localhost, 127.0.0.1, and the asset server by default.")
63
+	flags.StringVar(&args.ProjectNodeSelector, prefix+"project-node-selector", "", "Default node label selector for the project if not explicitly specified.")
63 64
 }
64 65
 
65 66
 // NewDefaultMasterArgs creates MasterArgs with sub-objects created and default values set.
... ...
@@ -198,6 +200,10 @@ func (args MasterArgs) BuildSerializeableMasterConfig() (*configapi.MasterConfig
198 198
 		},
199 199
 	}
200 200
 
201
+	if len(args.ProjectNodeSelector) > 0 {
202
+		config.ProjectNodeSelector = args.ProjectNodeSelector
203
+	}
204
+
201 205
 	if args.ListenArg.UseTLS() {
202 206
 		config.ServingInfo.ServerCert = admin.DefaultMasterServingCertInfo(args.ConfigDir.Value())
203 207
 		config.ServingInfo.ClientCA = admin.DefaultAPIClientCAFile(args.ConfigDir.Value())
... ...
@@ -8,5 +8,8 @@ import (
8 8
 	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists"
9 9
 	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle"
10 10
 	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota"
11
-	_ "github.com/openshift/origin/pkg/project/admission"
11
+	_ "github.com/openshift/origin/pkg/project/admission/lifecycle"
12
+
13
+	// Scheduler plugins used by OpenShift
14
+	_ "github.com/openshift/origin/pkg/scheduler/algorithmprovider/defaults"
12 15
 )
... ...
@@ -308,8 +308,9 @@ func StartMaster(openshiftMasterConfig *configapi.MasterConfig) error {
308 308
 	if err != nil {
309 309
 		return err
310 310
 	}
311
-	//	 must start policy caching immediately
311
+	// Must start policy caching immediately
312 312
 	openshiftConfig.RunPolicyCache()
313
+	openshiftConfig.RunProjectCache()
313 314
 
314 315
 	unprotectedInstallers := []origin.APIInstaller{}
315 316
 
316 317
deleted file mode 100644
... ...
@@ -1,131 +0,0 @@
1
-/*
2
-Copyright 2014 Google Inc. All rights reserved.
3
-
4
-Licensed under the Apache License, Version 2.0 (the "License");
5
-you may not use this file except in compliance with the License.
6
-You may obtain a copy of the License at
7
-
8
-    http://www.apache.org/licenses/LICENSE-2.0
9
-
10
-Unless required by applicable law or agreed to in writing, software
11
-distributed under the License is distributed on an "AS IS" BASIS,
12
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
-See the License for the specific language governing permissions and
14
-limitations under the License.
15
-*/
16
-
17
-package admission
18
-
19
-import (
20
-	"fmt"
21
-	"io"
22
-
23
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
24
-	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
25
-	apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
26
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
27
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
28
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
29
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
30
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
31
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
32
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
33
-	"github.com/openshift/origin/pkg/api/latest"
34
-)
35
-
36
-// TODO: modify the upstream plug-in so this can be collapsed
37
-// need ability to specify a RESTMapper on upstream version
38
-func init() {
39
-	admission.RegisterPlugin("OriginNamespaceLifecycle", func(client client.Interface, config io.Reader) (admission.Interface, error) {
40
-		return NewLifecycle(client), nil
41
-	})
42
-}
43
-
44
-type lifecycle struct {
45
-	client client.Interface
46
-	store  cache.Store
47
-}
48
-
49
-// Admit enforces that a namespace must exist in order to associate content with it.
50
-// Admit enforces that a namespace that is terminating cannot accept new content being associated with it.
51
-func (e *lifecycle) Admit(a admission.Attributes) (err error) {
52
-	defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource())
53
-	if err != nil {
54
-		return err
55
-	}
56
-	mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion)
57
-	if err != nil {
58
-		return err
59
-	}
60
-	if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
61
-		return nil
62
-	}
63
-
64
-	// we want to allow someone to delete something in case it was phantom created somehow
65
-	if a.GetOperation() == "DELETE" {
66
-		return nil
67
-	}
68
-
69
-	// check for namespace in the cache
70
-	namespaceObj, exists, err := e.store.Get(&kapi.Namespace{
71
-		ObjectMeta: kapi.ObjectMeta{
72
-			Name:      a.GetNamespace(),
73
-			Namespace: "",
74
-		},
75
-		Status: kapi.NamespaceStatus{},
76
-	})
77
-
78
-	if err != nil {
79
-		return err
80
-	}
81
-
82
-	name := "Unknown"
83
-	obj := a.GetObject()
84
-	if obj != nil {
85
-		name, _ = meta.NewAccessor().Name(obj)
86
-	}
87
-
88
-	var namespace *kapi.Namespace
89
-	if exists {
90
-		namespace = namespaceObj.(*kapi.Namespace)
91
-	} else {
92
-		// Our watch maybe latent, so we make a best effort to get the object, and only fail if not found
93
-		namespace, err = e.client.Namespaces().Get(a.GetNamespace())
94
-		// the namespace does not exist, so prevent create and update in that namespace
95
-		if err != nil {
96
-			return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s does not exist", a.GetNamespace()))
97
-		}
98
-	}
99
-
100
-	if a.GetOperation() != "CREATE" {
101
-		return nil
102
-	}
103
-
104
-	if namespace.Status.Phase != kapi.NamespaceTerminating {
105
-		return nil
106
-	}
107
-
108
-	return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s is terminating", a.GetNamespace()))
109
-}
110
-
111
-func NewLifecycle(c client.Interface) admission.Interface {
112
-	store := cache.NewStore(cache.MetaNamespaceKeyFunc)
113
-	reflector := cache.NewReflector(
114
-		&cache.ListWatch{
115
-			ListFunc: func() (runtime.Object, error) {
116
-				return c.Namespaces().List(labels.Everything(), fields.Everything())
117
-			},
118
-			WatchFunc: func(resourceVersion string) (watch.Interface, error) {
119
-				return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion)
120
-			},
121
-		},
122
-		&kapi.Namespace{},
123
-		store,
124
-		0,
125
-	)
126
-	reflector.Run()
127
-	return &lifecycle{
128
-		client: c,
129
-		store:  store,
130
-	}
131
-}
132 1
deleted file mode 100644
... ...
@@ -1,116 +0,0 @@
1
-package admission
2
-
3
-import (
4
-	"fmt"
5
-	"testing"
6
-
7
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
8
-	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
9
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
10
-	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
11
-
12
-	buildapi "github.com/openshift/origin/pkg/build/api"
13
-)
14
-
15
-// TestAdmissionExists verifies you cannot create Origin content if namespace is not known
16
-func TestAdmissionExists(t *testing.T) {
17
-	store := cache.NewStore(cache.MetaNamespaceKeyFunc)
18
-	mockClient := &testclient.Fake{
19
-		Err: fmt.Errorf("DOES NOT EXIST"),
20
-	}
21
-	handler := &lifecycle{
22
-		client: mockClient,
23
-		store:  store,
24
-	}
25
-	build := &buildapi.Build{
26
-		ObjectMeta: kapi.ObjectMeta{Name: "buildid"},
27
-		Parameters: buildapi.BuildParameters{
28
-			Source: buildapi.BuildSource{
29
-				Type: buildapi.BuildSourceGit,
30
-				Git: &buildapi.GitBuildSource{
31
-					URI: "http://github.com/my/repository",
32
-				},
33
-				ContextDir: "context",
34
-			},
35
-			Strategy: buildapi.BuildStrategy{
36
-				Type:           buildapi.DockerBuildStrategyType,
37
-				DockerStrategy: &buildapi.DockerBuildStrategy{},
38
-			},
39
-			Output: buildapi.BuildOutput{
40
-				DockerImageReference: "repository/data",
41
-			},
42
-		},
43
-		Status: buildapi.BuildStatusNew,
44
-	}
45
-	err := handler.Admit(admission.NewAttributesRecord(build, "bogus-ns", "builds", "CREATE"))
46
-	if err == nil {
47
-		t.Errorf("Expected an error because namespace does not exist")
48
-	}
49
-}
50
-
51
-// TestAdmissionLifecycle verifies you cannot create Origin content if namespace is terminating
52
-func TestAdmissionLifecycle(t *testing.T) {
53
-	namespaceObj := &kapi.Namespace{
54
-		ObjectMeta: kapi.ObjectMeta{
55
-			Name:      "test",
56
-			Namespace: "",
57
-		},
58
-		Status: kapi.NamespaceStatus{
59
-			Phase: kapi.NamespaceActive,
60
-		},
61
-	}
62
-	store := cache.NewStore(cache.MetaNamespaceIndexFunc)
63
-	store.Add(namespaceObj)
64
-	mockClient := &testclient.Fake{}
65
-	handler := &lifecycle{
66
-		client: mockClient,
67
-		store:  store,
68
-	}
69
-	build := &buildapi.Build{
70
-		ObjectMeta: kapi.ObjectMeta{Name: "buildid"},
71
-		Parameters: buildapi.BuildParameters{
72
-			Source: buildapi.BuildSource{
73
-				Type: buildapi.BuildSourceGit,
74
-				Git: &buildapi.GitBuildSource{
75
-					URI: "http://github.com/my/repository",
76
-				},
77
-				ContextDir: "context",
78
-			},
79
-			Strategy: buildapi.BuildStrategy{
80
-				Type:           buildapi.DockerBuildStrategyType,
81
-				DockerStrategy: &buildapi.DockerBuildStrategy{},
82
-			},
83
-			Output: buildapi.BuildOutput{
84
-				DockerImageReference: "repository/data",
85
-			},
86
-		},
87
-		Status: buildapi.BuildStatusNew,
88
-	}
89
-	err := handler.Admit(admission.NewAttributesRecord(build, namespaceObj.Namespace, "builds", "CREATE"))
90
-	if err != nil {
91
-		t.Errorf("Unexpected error returned from admission handler: %v", err)
92
-	}
93
-
94
-	// change namespace state to terminating
95
-	namespaceObj.Status.Phase = kapi.NamespaceTerminating
96
-	store.Add(namespaceObj)
97
-
98
-	// verify create operations in the namespace cause an error
99
-	err = handler.Admit(admission.NewAttributesRecord(build, namespaceObj.Namespace, "builds", "CREATE"))
100
-	if err == nil {
101
-		t.Errorf("Expected error rejecting creates in a namespace when it is terminating")
102
-	}
103
-
104
-	// verify update operations in the namespace can proceed
105
-	err = handler.Admit(admission.NewAttributesRecord(build, namespaceObj.Namespace, "builds", "UPDATE"))
106
-	if err != nil {
107
-		t.Errorf("Unexpected error returned from admission handler: %v", err)
108
-	}
109
-
110
-	// verify delete operations in the namespace can proceed
111
-	err = handler.Admit(admission.NewAttributesRecord(nil, namespaceObj.Namespace, "builds", "DELETE"))
112
-	if err != nil {
113
-		t.Errorf("Unexpected error returned from admission handler: %v", err)
114
-	}
115
-
116
-}
117 1
new file mode 100644
... ...
@@ -0,0 +1,92 @@
0
+/*
1
+Copyright 2014 Google Inc. All rights reserved.
2
+
3
+Licensed under the Apache License, Version 2.0 (the "License");
4
+you may not use this file except in compliance with the License.
5
+You may obtain a copy of the License at
6
+
7
+    http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+Unless required by applicable law or agreed to in writing, software
10
+distributed under the License is distributed on an "AS IS" BASIS,
11
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+See the License for the specific language governing permissions and
13
+limitations under the License.
14
+*/
15
+
16
+package admission
17
+
18
+import (
19
+	"fmt"
20
+	"io"
21
+
22
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
23
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
24
+	apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
25
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
26
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
27
+
28
+	"github.com/openshift/origin/pkg/api/latest"
29
+	"github.com/openshift/origin/pkg/project/cache"
30
+)
31
+
32
+// TODO: modify the upstream plug-in so this can be collapsed
33
+// need ability to specify a RESTMapper on upstream version
34
+func init() {
35
+	admission.RegisterPlugin("OriginNamespaceLifecycle", func(client client.Interface, config io.Reader) (admission.Interface, error) {
36
+		return NewLifecycle()
37
+	})
38
+}
39
+
40
+type lifecycle struct {
41
+}
42
+
43
+// Admit enforces that a namespace must exist in order to associate content with it.
44
+// Admit enforces that a namespace that is terminating cannot accept new content being associated with it.
45
+func (e *lifecycle) Admit(a admission.Attributes) (err error) {
46
+	defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource())
47
+	if err != nil {
48
+		return err
49
+	}
50
+	mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion)
51
+	if err != nil {
52
+		return err
53
+	}
54
+	if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
55
+		return nil
56
+	}
57
+
58
+	// we want to allow someone to delete something in case it was phantom created somehow
59
+	if a.GetOperation() == "DELETE" {
60
+		return nil
61
+	}
62
+
63
+	name := "Unknown"
64
+	obj := a.GetObject()
65
+	if obj != nil {
66
+		name, _ = meta.NewAccessor().Name(obj)
67
+	}
68
+
69
+	projects, err := cache.GetProjectCache()
70
+	if err != nil {
71
+		return err
72
+	}
73
+	namespace, err := projects.GetNamespaceObject(a.GetNamespace())
74
+	if err != nil {
75
+		return apierrors.NewForbidden(kind, name, err)
76
+	}
77
+
78
+	if a.GetOperation() != "CREATE" {
79
+		return nil
80
+	}
81
+
82
+	if namespace.Status.Phase != kapi.NamespaceTerminating {
83
+		return nil
84
+	}
85
+
86
+	return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s is terminating", a.GetNamespace()))
87
+}
88
+
89
+func NewLifecycle() (admission.Interface, error) {
90
+	return &lifecycle{}, nil
91
+}
0 92
new file mode 100644
... ...
@@ -0,0 +1,112 @@
0
+package admission
1
+
2
+import (
3
+	"fmt"
4
+	"testing"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
7
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
10
+
11
+	buildapi "github.com/openshift/origin/pkg/build/api"
12
+	projectcache "github.com/openshift/origin/pkg/project/cache"
13
+)
14
+
15
+// TestAdmissionExists verifies you cannot create Origin content if namespace is not known
16
+func TestAdmissionExists(t *testing.T) {
17
+	mockClient := &testclient.Fake{
18
+		Err: fmt.Errorf("DOES NOT EXIST"),
19
+	}
20
+	projectcache.FakeProjectCache(mockClient, cache.NewStore(cache.MetaNamespaceKeyFunc), "")
21
+	handler := &lifecycle{}
22
+	build := &buildapi.Build{
23
+		ObjectMeta: kapi.ObjectMeta{Name: "buildid"},
24
+		Parameters: buildapi.BuildParameters{
25
+			Source: buildapi.BuildSource{
26
+				Type: buildapi.BuildSourceGit,
27
+				Git: &buildapi.GitBuildSource{
28
+					URI: "http://github.com/my/repository",
29
+				},
30
+				ContextDir: "context",
31
+			},
32
+			Strategy: buildapi.BuildStrategy{
33
+				Type:           buildapi.DockerBuildStrategyType,
34
+				DockerStrategy: &buildapi.DockerBuildStrategy{},
35
+			},
36
+			Output: buildapi.BuildOutput{
37
+				DockerImageReference: "repository/data",
38
+			},
39
+		},
40
+		Status: buildapi.BuildStatusNew,
41
+	}
42
+	err := handler.Admit(admission.NewAttributesRecord(build, "bogus-ns", "builds", "CREATE"))
43
+	if err == nil {
44
+		t.Errorf("Expected an error because namespace does not exist")
45
+	}
46
+}
47
+
48
+// TestAdmissionLifecycle verifies you cannot create Origin content if namespace is terminating
49
+func TestAdmissionLifecycle(t *testing.T) {
50
+	namespaceObj := &kapi.Namespace{
51
+		ObjectMeta: kapi.ObjectMeta{
52
+			Name:      "test",
53
+			Namespace: "",
54
+		},
55
+		Status: kapi.NamespaceStatus{
56
+			Phase: kapi.NamespaceActive,
57
+		},
58
+	}
59
+	store := cache.NewStore(cache.MetaNamespaceIndexFunc)
60
+	store.Add(namespaceObj)
61
+	mockClient := &testclient.Fake{}
62
+	projectcache.FakeProjectCache(mockClient, store, "")
63
+	handler := &lifecycle{}
64
+	build := &buildapi.Build{
65
+		ObjectMeta: kapi.ObjectMeta{Name: "buildid"},
66
+		Parameters: buildapi.BuildParameters{
67
+			Source: buildapi.BuildSource{
68
+				Type: buildapi.BuildSourceGit,
69
+				Git: &buildapi.GitBuildSource{
70
+					URI: "http://github.com/my/repository",
71
+				},
72
+				ContextDir: "context",
73
+			},
74
+			Strategy: buildapi.BuildStrategy{
75
+				Type:           buildapi.DockerBuildStrategyType,
76
+				DockerStrategy: &buildapi.DockerBuildStrategy{},
77
+			},
78
+			Output: buildapi.BuildOutput{
79
+				DockerImageReference: "repository/data",
80
+			},
81
+		},
82
+		Status: buildapi.BuildStatusNew,
83
+	}
84
+	err := handler.Admit(admission.NewAttributesRecord(build, namespaceObj.Namespace, "builds", "CREATE"))
85
+	if err != nil {
86
+		t.Errorf("Unexpected error returned from admission handler: %v", err)
87
+	}
88
+
89
+	// change namespace state to terminating
90
+	namespaceObj.Status.Phase = kapi.NamespaceTerminating
91
+	store.Add(namespaceObj)
92
+
93
+	// verify create operations in the namespace cause an error
94
+	err = handler.Admit(admission.NewAttributesRecord(build, namespaceObj.Namespace, "builds", "CREATE"))
95
+	if err == nil {
96
+		t.Errorf("Expected error rejecting creates in a namespace when it is terminating")
97
+	}
98
+
99
+	// verify update operations in the namespace can proceed
100
+	err = handler.Admit(admission.NewAttributesRecord(build, namespaceObj.Namespace, "builds", "UPDATE"))
101
+	if err != nil {
102
+		t.Errorf("Unexpected error returned from admission handler: %v", err)
103
+	}
104
+
105
+	// verify delete operations in the namespace can proceed
106
+	err = handler.Admit(admission.NewAttributesRecord(nil, namespaceObj.Namespace, "builds", "DELETE"))
107
+	if err != nil {
108
+		t.Errorf("Unexpected error returned from admission handler: %v", err)
109
+	}
110
+
111
+}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"strings"
5 5
 
6 6
 	kvalidation "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
7 8
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
8 9
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
9 10
 	"github.com/openshift/origin/pkg/project/api"
... ...
@@ -23,6 +24,7 @@ func ValidateProject(project *api.Project) fielderrors.ValidationErrorList {
23 23
 	if !validateNoNewLineOrTab(project.Annotations["displayName"]) {
24 24
 		result = append(result, fielderrors.NewFieldInvalid("displayName", project.Annotations["displayName"], "may not contain a new line or tab"))
25 25
 	}
26
+	result = append(result, validateNodeSelector(project)...)
26 27
 	return result
27 28
 }
28 29
 
... ...
@@ -35,6 +37,7 @@ func validateNoNewLineOrTab(s string) bool {
35 35
 func ValidateProjectUpdate(newProject *api.Project, oldProject *api.Project) fielderrors.ValidationErrorList {
36 36
 	allErrs := fielderrors.ValidationErrorList{}
37 37
 	allErrs = append(allErrs, kvalidation.ValidateObjectMetaUpdate(&oldProject.ObjectMeta, &newProject.ObjectMeta).Prefix("metadata")...)
38
+	allErrs = append(allErrs, validateNodeSelector(newProject)...)
38 39
 	newProject.Spec.Finalizers = oldProject.Spec.Finalizers
39 40
 	newProject.Status = oldProject.Status
40 41
 	return allErrs
... ...
@@ -46,3 +49,16 @@ func ValidateProjectRequest(request *api.ProjectRequest) fielderrors.ValidationE
46 46
 
47 47
 	return ValidateProject(project)
48 48
 }
49
+
50
+func validateNodeSelector(p *api.Project) fielderrors.ValidationErrorList {
51
+	allErrs := fielderrors.ValidationErrorList{}
52
+
53
+	if len(p.Annotations) > 0 {
54
+		if selector, ok := p.Annotations["nodeSelector"]; ok {
55
+			if _, err := labels.Parse(selector); err != nil {
56
+				allErrs = append(allErrs, fielderrors.NewFieldInvalid("nodeSelector", p.Annotations["nodeSelector"], "must be a valid label selector"))
57
+			}
58
+		}
59
+	}
60
+	return allErrs
61
+}
... ...
@@ -97,6 +97,33 @@ func TestValidateProject(t *testing.T) {
97 97
 			// Should fail because the display name has \t \n
98 98
 			numErrs: 1,
99 99
 		},
100
+		{
101
+			name: "valid node selector",
102
+			project: api.Project{
103
+				ObjectMeta: kapi.ObjectMeta{
104
+					Name:      "foo",
105
+					Namespace: "",
106
+					Annotations: map[string]string{
107
+						"nodeSelector": "infra=true, env in (prod, qa)",
108
+					},
109
+				},
110
+			},
111
+			numErrs: 0,
112
+		},
113
+		{
114
+			name: "invalid node selector",
115
+			project: api.Project{
116
+				ObjectMeta: kapi.ObjectMeta{
117
+					Name:      "foo",
118
+					Namespace: "",
119
+					Annotations: map[string]string{
120
+						"nodeSelector": "infra:true,env",
121
+					},
122
+				},
123
+			},
124
+			// Should fail because infra:true is invalid format
125
+			numErrs: 1,
126
+		},
100 127
 	}
101 128
 
102 129
 	for _, tc := range testCases {
103 130
new file mode 100644
... ...
@@ -0,0 +1,117 @@
0
+package cache
1
+
2
+import (
3
+	"fmt"
4
+
5
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
11
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
12
+)
13
+
14
+type ProjectCache struct {
15
+	Client              client.Interface
16
+	Store               cache.Store
17
+	DefaultNodeSelector string
18
+}
19
+
20
+var pcache *ProjectCache
21
+
22
+func (p *ProjectCache) GetNamespaceObject(name string) (*kapi.Namespace, error) {
23
+	// check for namespace in the cache
24
+	namespaceObj, exists, err := p.Store.Get(&kapi.Namespace{
25
+		ObjectMeta: kapi.ObjectMeta{
26
+			Name:      name,
27
+			Namespace: "",
28
+		},
29
+		Status: kapi.NamespaceStatus{},
30
+	})
31
+	if err != nil {
32
+		return nil, err
33
+	}
34
+
35
+	var namespace *kapi.Namespace
36
+	if exists {
37
+		namespace = namespaceObj.(*kapi.Namespace)
38
+	} else {
39
+		// Our watch maybe latent, so we make a best effort to get the object, and only fail if not found
40
+		namespace, err = p.Client.Namespaces().Get(name)
41
+		// the namespace does not exist, so prevent create and update in that namespace
42
+		if err != nil {
43
+			return nil, fmt.Errorf("Namespace %s does not exist", name)
44
+		}
45
+	}
46
+	return namespace, nil
47
+}
48
+
49
+func (p *ProjectCache) GetNodeSelector(namespace *kapi.Namespace) string {
50
+	selector := ""
51
+	if len(namespace.ObjectMeta.Annotations) > 0 {
52
+		if ns, ok := namespace.ObjectMeta.Annotations["nodeSelector"]; ok {
53
+			selector = ns
54
+		}
55
+	}
56
+	if len(selector) == 0 {
57
+		selector = p.DefaultNodeSelector
58
+	}
59
+	return selector
60
+}
61
+
62
+func (p *ProjectCache) GetNodeSelectorObject(namespace *kapi.Namespace) (labels.Selector, error) {
63
+	selector := p.GetNodeSelector(namespace)
64
+	if len(selector) == 0 {
65
+		return labels.Everything(), nil
66
+	} else {
67
+		selectorObj, err := labels.Parse(selector)
68
+		if err != nil {
69
+			return nil, err
70
+		}
71
+		return selectorObj, nil
72
+	}
73
+}
74
+
75
+func GetProjectCache() (*ProjectCache, error) {
76
+	if pcache == nil {
77
+		return nil, fmt.Errorf("project cache not initialized")
78
+	}
79
+	return pcache, nil
80
+}
81
+
82
+func RunProjectCache(c client.Interface, defaultNodeSelector string) {
83
+	if pcache != nil {
84
+		return
85
+	}
86
+
87
+	store := cache.NewStore(cache.MetaNamespaceKeyFunc)
88
+	reflector := cache.NewReflector(
89
+		&cache.ListWatch{
90
+			ListFunc: func() (runtime.Object, error) {
91
+				return c.Namespaces().List(labels.Everything(), fields.Everything())
92
+			},
93
+			WatchFunc: func(resourceVersion string) (watch.Interface, error) {
94
+				return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion)
95
+			},
96
+		},
97
+		&kapi.Namespace{},
98
+		store,
99
+		0,
100
+	)
101
+	reflector.Run()
102
+	pcache = &ProjectCache{
103
+		Client:              c,
104
+		Store:               store,
105
+		DefaultNodeSelector: defaultNodeSelector,
106
+	}
107
+}
108
+
109
+// Used for testing purpose only
110
+func FakeProjectCache(c client.Interface, store cache.Store, defaultNodeSelector string) {
111
+	pcache = &ProjectCache{
112
+		Client:              c,
113
+		Store:               store,
114
+		DefaultNodeSelector: defaultNodeSelector,
115
+	}
116
+}
0 117
new file mode 100644
... ...
@@ -0,0 +1,68 @@
0
+// This is the default algorithm provider for the Origin scheduler.
1
+package defaults
2
+
3
+import (
4
+	kscheduler "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler"
5
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
6
+	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/algorithmprovider/defaults"
7
+	"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory"
8
+
9
+	"github.com/openshift/origin/pkg/scheduler"
10
+)
11
+
12
+func init() {
13
+	defaultPredicates, err := defaultPredicates()
14
+	if err != nil {
15
+		panic(err)
16
+	}
17
+	defaultPriorities, err := defaultPriorities()
18
+	if err != nil {
19
+		panic(err)
20
+	}
21
+	factory.RegisterAlgorithmProvider(scheduler.DefaultProvider, defaultPredicates, defaultPriorities)
22
+
23
+	// Register non-default origin predicates/priorities here
24
+	// factory.RegisterFitPredicateFactory(...)
25
+	// factory.RegisterPriorityConfigFactory(...)
26
+}
27
+
28
+func defaultPredicates() (util.StringSet, error) {
29
+	// Fit is determined by project node label selector query.
30
+	matchProjectNodeSelector := "MatchProjectNodeSelector"
31
+	factory.RegisterFitPredicateFactory(
32
+		matchProjectNodeSelector,
33
+		func(args factory.PluginFactoryArgs) kscheduler.FitPredicate {
34
+			return scheduler.NewProjectSelectorMatchPredicate(args.NodeInfo)
35
+		},
36
+	)
37
+
38
+	// Get predicates from k8s default provider.
39
+	// If we decide not to use all the predicates from k8s default provider,
40
+	// chery-pick individual predicates from <k8s>/plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go
41
+	kprovider, err := factory.GetAlgorithmProvider(factory.DefaultProvider)
42
+	if err != nil {
43
+		return nil, err
44
+	}
45
+
46
+	originDefaultPredicates := kprovider.FitPredicateKeys
47
+	// Add default origin predicates
48
+	originDefaultPredicates.Insert(matchProjectNodeSelector)
49
+
50
+	return originDefaultPredicates, nil
51
+}
52
+
53
+func defaultPriorities() (util.StringSet, error) {
54
+	// Get priority functions from k8s default provider.
55
+	// If we decide not to use all the priority functions from k8s default provider,
56
+	// chery-pick individual priority function from <k8s>/plugin/pkg/scheduler/algorithmprovider/defaults/defaults.go
57
+	kprovider, err := factory.GetAlgorithmProvider(factory.DefaultProvider)
58
+	if err != nil {
59
+		return nil, err
60
+	}
61
+
62
+	OriginDefaultPriorities := kprovider.PriorityFunctionKeys
63
+	// Add default origin priority function keys
64
+	// OriginDefaultPriorities.Insert(...)
65
+
66
+	return OriginDefaultPriorities, nil
67
+}
0 68
new file mode 100644
... ...
@@ -0,0 +1,6 @@
0
+// This package is used to register algorithm provider plugins.
1
+package algorithmprovider
2
+
3
+import (
4
+	_ "github.com/openshift/origin/pkg/scheduler/algorithmprovider/defaults"
5
+)
0 6
new file mode 100644
... ...
@@ -0,0 +1,51 @@
0
+package algorithmprovider
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory"
6
+
7
+	"github.com/openshift/origin/pkg/scheduler"
8
+)
9
+
10
+var (
11
+	algorithmProviderNames = []string{
12
+		scheduler.DefaultProvider,
13
+	}
14
+)
15
+
16
+func TestDefaultConfigExists(t *testing.T) {
17
+	p, err := factory.GetAlgorithmProvider(scheduler.DefaultProvider)
18
+	if err != nil {
19
+		t.Errorf("error retrieving default provider: %v", err)
20
+	}
21
+	if p == nil {
22
+		t.Error("algorithm provider config should not be nil")
23
+	}
24
+	if len(p.FitPredicateKeys) == 0 {
25
+		t.Error("default algorithm provider shouldn't have 0 fit predicates")
26
+	}
27
+}
28
+
29
+func TestAlgorithmProviders(t *testing.T) {
30
+	for _, pn := range algorithmProviderNames {
31
+		p, err := factory.GetAlgorithmProvider(pn)
32
+		if err != nil {
33
+			t.Errorf("error retrieving '%s' provider: %v", pn, err)
34
+			break
35
+		}
36
+		if len(p.PriorityFunctionKeys) == 0 {
37
+			t.Errorf("%s algorithm provider shouldn't have 0 priority functions", pn)
38
+		}
39
+		for _, pf := range p.PriorityFunctionKeys.List() {
40
+			if !factory.IsPriorityFunctionRegistered(pf) {
41
+				t.Errorf("priority function %s is not registered but is used in the %s algorithm provider", pf, pn)
42
+			}
43
+		}
44
+		for _, fp := range p.FitPredicateKeys.List() {
45
+			if !factory.IsFitPredicateRegistered(fp) {
46
+				t.Errorf("fit predicate %s is not registered but is used in the %s algorithm provider", fp, pn)
47
+			}
48
+		}
49
+	}
50
+}
0 51
new file mode 100644
... ...
@@ -0,0 +1,48 @@
0
+package scheduler
1
+
2
+import (
3
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
4
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
5
+	kscheduler "github.com/GoogleCloudPlatform/kubernetes/pkg/scheduler"
6
+
7
+	"github.com/openshift/origin/pkg/project/cache"
8
+)
9
+
10
+const (
11
+	DefaultProvider = "OriginDefaultProvider"
12
+)
13
+
14
+func NewProjectSelectorMatchPredicate(info kscheduler.NodeInfo) kscheduler.FitPredicate {
15
+	selector := &projectNodeSelector{
16
+		info: info,
17
+	}
18
+	return selector.ProjectSelectorMatches
19
+}
20
+
21
+type projectNodeSelector struct {
22
+	info kscheduler.NodeInfo
23
+}
24
+
25
+func (p *projectNodeSelector) ProjectSelectorMatches(pod kapi.Pod, existingPods []kapi.Pod, node string) (bool, error) {
26
+	minion, err := p.info.GetNodeInfo(node)
27
+	if err != nil {
28
+		return false, err
29
+	}
30
+	return ProjectMatchesNodeLabels(&pod, minion)
31
+}
32
+
33
+func ProjectMatchesNodeLabels(pod *kapi.Pod, node *kapi.Node) (bool, error) {
34
+	projects, err := cache.GetProjectCache()
35
+	if err != nil {
36
+		return false, err
37
+	}
38
+	namespace, err := projects.GetNamespaceObject(pod.ObjectMeta.Namespace)
39
+	if err != nil {
40
+		return false, err
41
+	}
42
+	selector, err := projects.GetNodeSelectorObject(namespace)
43
+	if err != nil {
44
+		return false, err
45
+	}
46
+	return selector.Matches(labels.Set(node.Labels)), nil
47
+}
0 48
new file mode 100644
... ...
@@ -0,0 +1,90 @@
0
+package scheduler
1
+
2
+import (
3
+	"testing"
4
+
5
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
8
+
9
+	projectcache "github.com/openshift/origin/pkg/project/cache"
10
+)
11
+
12
+type FakeNodeInfo kapi.Node
13
+
14
+func (n FakeNodeInfo) GetNodeInfo(nodeName string) (*kapi.Node, error) {
15
+	node := kapi.Node(n)
16
+	return &node, nil
17
+}
18
+
19
+func TestPodFitsProjectSelector(t *testing.T) {
20
+	mockClient := &testclient.Fake{}
21
+	project := &kapi.Namespace{
22
+		ObjectMeta: kapi.ObjectMeta{
23
+			Name:      "testProject",
24
+			Namespace: "",
25
+		},
26
+	}
27
+	projectStore := cache.NewStore(cache.MetaNamespaceIndexFunc)
28
+	projectStore.Add(project)
29
+
30
+	pod := kapi.Pod{ObjectMeta: kapi.ObjectMeta{Name: "testPod"}}
31
+	node := kapi.Node{ObjectMeta: kapi.ObjectMeta{Name: "testNode"}}
32
+
33
+	tests := []struct {
34
+		defaultNodeSelector string
35
+		projectNodeSelector string
36
+		nodeLabels          map[string]string
37
+		fits                bool
38
+		testName            string
39
+	}{
40
+		{
41
+			defaultNodeSelector: "",
42
+			projectNodeSelector: "",
43
+			nodeLabels:          map[string]string{"infra": "false"},
44
+			fits:                true,
45
+			testName:            "No node selectors",
46
+		},
47
+		{
48
+			defaultNodeSelector: "infra=false",
49
+			projectNodeSelector: "",
50
+			nodeLabels:          map[string]string{"infra": "false"},
51
+			fits:                true,
52
+			testName:            "Matches default node selector",
53
+		},
54
+		{
55
+			defaultNodeSelector: "env=test",
56
+			projectNodeSelector: "",
57
+			nodeLabels:          map[string]string{"infra": "false"},
58
+			fits:                false,
59
+			testName:            "Doesn't match default node selector",
60
+		},
61
+		{
62
+			defaultNodeSelector: "",
63
+			projectNodeSelector: "infra=false",
64
+			nodeLabels:          map[string]string{"infra": "false"},
65
+			fits:                true,
66
+			testName:            "Matches project node selector",
67
+		},
68
+		{
69
+			defaultNodeSelector: "infra=false",
70
+			projectNodeSelector: "env=test",
71
+			nodeLabels:          map[string]string{"infra": "false"},
72
+			fits:                false,
73
+			testName:            "Doesn't match project node selector",
74
+		},
75
+	}
76
+	for _, test := range tests {
77
+		node.ObjectMeta.Labels = test.nodeLabels
78
+		projectcache.FakeProjectCache(mockClient, projectStore, test.defaultNodeSelector)
79
+		project.ObjectMeta.Annotations = map[string]string{"nodeSelector": test.projectNodeSelector}
80
+		predicate := projectNodeSelector{FakeNodeInfo(node)}
81
+		fits, err := predicate.ProjectSelectorMatches(pod, []kapi.Pod{}, "machine")
82
+		if err != nil {
83
+			t.Errorf("unexpected error: %v", err)
84
+		}
85
+		if fits != test.fits {
86
+			t.Errorf("%s: expected: %v got %v", test.testName, test.fits, fits)
87
+		}
88
+	}
89
+}
... ...
@@ -131,7 +131,8 @@ func TestProjectIsNamespace(t *testing.T) {
131 131
 		ObjectMeta: kapi.ObjectMeta{
132 132
 			Name: "new-project",
133 133
 			Annotations: map[string]string{
134
-				"displayName": "Hello World",
134
+				"displayName":  "Hello World",
135
+				"nodeSelector": "env=test",
135 136
 			},
136 137
 		},
137 138
 	}
... ...
@@ -151,7 +152,9 @@ func TestProjectIsNamespace(t *testing.T) {
151 151
 	if project.Annotations["displayName"] != namespace.Annotations["displayName"] {
152 152
 		t.Fatalf("Project display name did not match namespace annotation, project %v, namespace %v", project.Annotations["displayName"], namespace.Annotations["displayName"])
153 153
 	}
154
-
154
+	if project.Annotations["nodeSelector"] != namespace.Annotations["nodeSelector"] {
155
+		t.Fatalf("Project node selector did not match namespace node selector, project %v, namespace %v", project.Annotations["nodeSelector"], namespace.Annotations["displayname"])
156
+	}
155 157
 }
156 158
 
157 159
 // TestProjectMustExist verifies that content cannot be added in a project that does not exist
... ...
@@ -210,5 +213,4 @@ func TestProjectMustExist(t *testing.T) {
210 210
 	if err == nil {
211 211
 		t.Errorf("Expected an error on creation of a Origin resource because namespace does not exist")
212 212
 	}
213
-
214 213
 }