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=<label_selector>' flag as command line argument
to openshift start or pass 'projectNodeSelector: <label_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{
'openshift.io/node-selector': <label_selector>
}
...
}
CLI: osadm new-project <name> --node-selector=<label_selector>
- Any project with no node selector will use global node selector if present.

Note: Current project node selector is limited to match exact labels. E.g.: "k1=v1, k2=v2"
we will support full selector(exists, in, notin operators) later.

Ravi Sankar Penta authored on 2015/05/01 07:31:20
Showing 24 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
 
... ...
@@ -56,6 +57,7 @@ func NewCmdNewProject(name, fullName string, f *clientcmd.Factory, out io.Writer
56 56
 	cmd.Flags().StringVar(&options.AdminUser, "admin", "", "project admin username")
57 57
 	cmd.Flags().StringVar(&options.DisplayName, "display-name", "", "project display name")
58 58
 	cmd.Flags().StringVar(&options.Description, "description", "", "project description")
59
+	cmd.Flags().StringVar(&options.NodeSelector, "node-selector", "", "Restrict pods onto nodes matching given label selector. Format: '<key1>=<value1>, <key2>=<value2>...'")
59 60
 
60 61
 	return cmd
61 62
 }
... ...
@@ -83,6 +85,7 @@ func (o *NewProjectOptions) Run() error {
83 83
 	project.Annotations = make(map[string]string)
84 84
 	project.Annotations["description"] = o.Description
85 85
 	project.Annotations["displayName"] = o.DisplayName
86
+	project.Annotations["openshift.io/node-selector"] = o.NodeSelector
86 87
 	project, err := o.Client.Projects().Create(project)
87 88
 	if err != nil {
88 89
 		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. Format: '<key1>=<value1>, <key2>=<value2>...'")
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["openshift.io/node-selector"] = o.NodeSelector
108 109
 
109 110
 	project, err := o.Client.ProjectRequests().Create(projectRequest)
110 111
 	if err != nil {
... ...
@@ -454,11 +454,18 @@ func (d *ProjectDescriber) Describe(namespace, name string) (string, error) {
454 454
 	if err != nil {
455 455
 		return "", err
456 456
 	}
457
+	nodeSelector := ""
458
+	if len(project.ObjectMeta.Annotations) > 0 {
459
+		if ns, ok := project.ObjectMeta.Annotations["openshift.io/node-selector"]; ok {
460
+			nodeSelector = ns
461
+		}
462
+	}
457 463
 
458 464
 	return tabbedString(func(out *tabwriter.Writer) error {
459 465
 		formatMeta(out, project.ObjectMeta)
460 466
 		formatString(out, "Display Name", project.Annotations["displayName"])
461 467
 		formatString(out, "Status", project.Status.Phase)
468
+		formatString(out, "Node Selector", nodeSelector)
462 469
 		return nil
463 470
 	})
464 471
 }
... ...
@@ -105,6 +105,8 @@ func GetMasterFileReferences(config *MasterConfig) []*string {
105 105
 
106 106
 	refs = append(refs, &config.PolicyConfig.BootstrapPolicyFile)
107 107
 
108
+	refs = append(refs, &config.ProjectNodeSelector)
109
+
108 110
 	return refs
109 111
 }
110 112
 
... ...
@@ -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
 }
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
10 10
 
11 11
 	"github.com/openshift/origin/pkg/cmd/server/api"
12
+	"github.com/openshift/origin/pkg/util/labelselector"
12 13
 )
13 14
 
14 15
 func ValidateMasterConfig(config *api.MasterConfig) fielderrors.ValidationErrorList {
... ...
@@ -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")...)
... ...
@@ -115,6 +118,19 @@ func ValidateEtcdStorageConfig(config api.EtcdStorageConfig) fielderrors.Validat
115 115
 	return allErrs
116 116
 }
117 117
 
118
+func ValidateProjectNodeSelector(nodeSelector string) fielderrors.ValidationErrorList {
119
+	allErrs := fielderrors.ValidationErrorList{}
120
+
121
+	if len(nodeSelector) > 0 {
122
+		_, err := labelselector.Parse(nodeSelector)
123
+		if err != nil {
124
+			allErrs = append(allErrs, fielderrors.NewFieldInvalid("projectNodeSelector", nodeSelector, "must be a valid label selector"))
125
+		}
126
+	}
127
+
128
+	return allErrs
129
+}
130
+
118 131
 func ValidateAssetConfig(config *api.AssetConfig) fielderrors.ValidationErrorList {
119 132
 	allErrs := fielderrors.ValidationErrorList{}
120 133
 
... ...
@@ -58,7 +58,8 @@ func BuildKubernetesMasterConfig(options configapi.MasterConfig, requestContextM
58 58
 	portalNet := net.IPNet(flagtypes.DefaultIPNet(options.KubernetesMasterConfig.ServicesSubnet))
59 59
 
60 60
 	// in-order list of plug-ins that should intercept admission decisions
61
-	admissionControlPluginNames := []string{"NamespaceExists", "NamespaceLifecycle", "LimitRanger", "ResourceQuota"}
61
+	// TODO: Push node environment support to upstream in future
62
+	admissionControlPluginNames := []string{"NamespaceExists", "NamespaceLifecycle", "OriginPodNodeEnvironment", "LimitRanger", "ResourceQuota"}
62 63
 	admissionController := admission.NewFromPlugins(kubeClient, admissionControlPluginNames, "")
63 64
 
64 65
 	_, portString, err := net.SplitHostPort(options.ServingInfo.BindAddress)
... ...
@@ -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"
... ...
@@ -768,6 +769,12 @@ func (c *MasterConfig) RunDNSServer() {
768 768
 	glog.Infof("OpenShift DNS listening at %s", c.Options.DNSConfig.BindAddress)
769 769
 }
770 770
 
771
+// RunProjectCache populates project cache, used by scheduler and project admission controller.
772
+func (c *MasterConfig) RunProjectCache() {
773
+	glog.Infof("Using default project node label selector: %s", c.Options.ProjectNodeSelector)
774
+	projectcache.RunProjectCache(c.PrivilegedLoopbackKubernetesClient, c.Options.ProjectNodeSelector)
775
+}
776
+
771 777
 // RunBuildController starts the build sync loop for builds and buildConfig processing.
772 778
 func (c *MasterConfig) RunBuildController() {
773 779
 	// 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. Format: '<key1>=<value1>, <key2>=<value2...'")
63 64
 }
64 65
 
65 66
 // NewDefaultMasterArgs creates MasterArgs with sub-objects created and default values set.
... ...
@@ -197,6 +199,10 @@ func (args MasterArgs) BuildSerializeableMasterConfig() (*configapi.MasterConfig
197 197
 		},
198 198
 	}
199 199
 
200
+	if len(args.ProjectNodeSelector) > 0 {
201
+		config.ProjectNodeSelector = args.ProjectNodeSelector
202
+	}
203
+
200 204
 	if args.ListenArg.UseTLS() {
201 205
 		config.ServingInfo.ServerCert = admin.DefaultMasterServingCertInfo(args.ConfigDir.Value())
202 206
 		config.ServingInfo.ClientCA = admin.DefaultAPIClientCAFile(args.ConfigDir.Value())
... ...
@@ -8,5 +8,6 @@ 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
+	_ "github.com/openshift/origin/pkg/project/admission/nodeenv"
12 13
 )
... ...
@@ -311,8 +311,9 @@ func StartMaster(openshiftMasterConfig *configapi.MasterConfig) error {
311 311
 	if err != nil {
312 312
 		return err
313 313
 	}
314
-	//	 must start policy caching immediately
314
+	// Must start policy caching immediately
315 315
 	openshiftConfig.RunPolicyCache()
316
+	openshiftConfig.RunProjectCache()
316 317
 
317 318
 	unprotectedInstallers := []origin.APIInstaller{}
318 319
 
319 320
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, "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, "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, "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, "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, "Build", 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, "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, "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, "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, "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, "Build", namespaceObj.Namespace, "builds", "DELETE"))
107
+	if err != nil {
108
+		t.Errorf("Unexpected error returned from admission handler: %v", err)
109
+	}
110
+
111
+}
0 112
new file mode 100644
... ...
@@ -0,0 +1,74 @@
0
+package admission
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+
6
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
7
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
8
+	apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
9
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
10
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
11
+
12
+	projectcache "github.com/openshift/origin/pkg/project/cache"
13
+	"github.com/openshift/origin/pkg/util/labelselector"
14
+)
15
+
16
+func init() {
17
+	admission.RegisterPlugin("OriginPodNodeEnvironment", func(client client.Interface, config io.Reader) (admission.Interface, error) {
18
+		return NewPodNodeEnvironment(client)
19
+	})
20
+}
21
+
22
+// podNodeEnvironment is an implementation of admission.Interface.
23
+type podNodeEnvironment struct {
24
+	client client.Interface
25
+}
26
+
27
+// Admit enforces that pod and its project node label selectors matches at least a node in the cluster.
28
+func (p *podNodeEnvironment) Admit(a admission.Attributes) (err error) {
29
+	// ignore deletes
30
+	if a.GetOperation() == "DELETE" {
31
+		return nil
32
+	}
33
+
34
+	resource := a.GetResource()
35
+	if resource != "pods" {
36
+		return nil
37
+	}
38
+
39
+	obj := a.GetObject()
40
+	name := "Unknown"
41
+	if obj != nil {
42
+		name, _ = meta.NewAccessor().Name(obj)
43
+	}
44
+	pod := obj.(*kapi.Pod)
45
+
46
+	projects, err := projectcache.GetProjectCache()
47
+	if err != nil {
48
+		return err
49
+	}
50
+	namespace, err := projects.GetNamespaceObject(a.GetNamespace())
51
+	if err != nil {
52
+		return apierrors.NewForbidden(resource, name, err)
53
+	}
54
+	projectNodeSelector, err := projects.GetNodeSelectorMap(namespace)
55
+	if err != nil {
56
+		return err
57
+	}
58
+
59
+	if labelselector.Conflicts(projectNodeSelector, pod.Spec.NodeSelector) {
60
+		return apierrors.NewForbidden(resource, name, fmt.Errorf("Pod node label selector conflicts with its project node label selector"))
61
+	}
62
+
63
+	// modify pod node selector = project node selector + current pod node selector
64
+	pod.Spec.NodeSelector = labelselector.Merge(projectNodeSelector, pod.Spec.NodeSelector)
65
+
66
+	return nil
67
+}
68
+
69
+func NewPodNodeEnvironment(client client.Interface) (admission.Interface, error) {
70
+	return &podNodeEnvironment{
71
+		client: client,
72
+	}, nil
73
+}
0 74
new file mode 100644
... ...
@@ -0,0 +1,113 @@
0
+package admission
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
6
+	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
9
+
10
+	projectcache "github.com/openshift/origin/pkg/project/cache"
11
+	"github.com/openshift/origin/pkg/util/labelselector"
12
+)
13
+
14
+// TestPodAdmission verifies various scenarios involving pod/project/global node label selectors
15
+func TestPodAdmission(t *testing.T) {
16
+	mockClient := &testclient.Fake{}
17
+	project := &kapi.Namespace{
18
+		ObjectMeta: kapi.ObjectMeta{
19
+			Name:      "testProject",
20
+			Namespace: "",
21
+		},
22
+	}
23
+	projectStore := cache.NewStore(cache.MetaNamespaceIndexFunc)
24
+	projectStore.Add(project)
25
+
26
+	handler := &podNodeEnvironment{client: mockClient}
27
+	pod := &kapi.Pod{
28
+		ObjectMeta: kapi.ObjectMeta{Name: "testPod"},
29
+	}
30
+
31
+	tests := []struct {
32
+		defaultNodeSelector string
33
+		projectNodeSelector string
34
+		podNodeSelector     map[string]string
35
+		mergedNodeSelector  map[string]string
36
+		admit               bool
37
+		testName            string
38
+	}{
39
+		{
40
+			defaultNodeSelector: "",
41
+			projectNodeSelector: "",
42
+			podNodeSelector:     map[string]string{},
43
+			mergedNodeSelector:  map[string]string{},
44
+			admit:               true,
45
+			testName:            "No node selectors",
46
+		},
47
+		{
48
+			defaultNodeSelector: "infra = false",
49
+			projectNodeSelector: "",
50
+			podNodeSelector:     map[string]string{},
51
+			mergedNodeSelector:  map[string]string{"infra": "false"},
52
+			admit:               true,
53
+			testName:            "Default node selector and no conflicts",
54
+		},
55
+		{
56
+			defaultNodeSelector: "",
57
+			projectNodeSelector: "infra = false",
58
+			podNodeSelector:     map[string]string{},
59
+			mergedNodeSelector:  map[string]string{"infra": "false"},
60
+			admit:               true,
61
+			testName:            "Project node selector and no conflicts",
62
+		},
63
+		{
64
+			defaultNodeSelector: "infra = false",
65
+			projectNodeSelector: "infra=true",
66
+			podNodeSelector:     map[string]string{},
67
+			mergedNodeSelector:  map[string]string{"infra": "true"},
68
+			admit:               true,
69
+			testName:            "Default and project node selector, no conflicts",
70
+		},
71
+		{
72
+			defaultNodeSelector: "infra = false",
73
+			projectNodeSelector: "infra=true",
74
+			podNodeSelector:     map[string]string{"env": "test"},
75
+			mergedNodeSelector:  map[string]string{"infra": "true", "env": "test"},
76
+			admit:               true,
77
+			testName:            "Project and pod node selector, no conflicts",
78
+		},
79
+		{
80
+			defaultNodeSelector: "env = test",
81
+			projectNodeSelector: "infra=true",
82
+			podNodeSelector:     map[string]string{"infra": "false"},
83
+			mergedNodeSelector:  map[string]string{"infra": "false"},
84
+			admit:               false,
85
+			testName:            "Conflicting pod and project node selector, one label",
86
+		},
87
+		{
88
+			defaultNodeSelector: "env=dev",
89
+			projectNodeSelector: "infra=false, env = test",
90
+			podNodeSelector:     map[string]string{"env": "dev", "color": "blue"},
91
+			mergedNodeSelector:  map[string]string{"env": "dev", "color": "blue"},
92
+			admit:               false,
93
+			testName:            "Conflicting pod and project node selector, multiple labels",
94
+		},
95
+	}
96
+	for _, test := range tests {
97
+		projectcache.FakeProjectCache(mockClient, projectStore, test.defaultNodeSelector)
98
+		project.ObjectMeta.Annotations = map[string]string{"openshift.io/node-selector": test.projectNodeSelector}
99
+		pod.Spec = kapi.PodSpec{NodeSelector: test.podNodeSelector}
100
+
101
+		err := handler.Admit(admission.NewAttributesRecord(pod, "Pod", project.ObjectMeta.Name, "pods", "CREATE"))
102
+		if test.admit && err != nil {
103
+			t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
104
+		} else if !test.admit && err == nil {
105
+			t.Errorf("Test: %s, expected an error", test.testName)
106
+		}
107
+
108
+		if !labelselector.Equals(test.mergedNodeSelector, pod.Spec.NodeSelector) {
109
+			t.Errorf("Test: %s, expected: %s but got: %s", test.testName, test.mergedNodeSelector, pod.Spec.NodeSelector)
110
+		}
111
+	}
112
+}
... ...
@@ -6,7 +6,9 @@ import (
6 6
 	kvalidation "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
7 7
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
8 8
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
9
+
9 10
 	"github.com/openshift/origin/pkg/project/api"
11
+	"github.com/openshift/origin/pkg/util/labelselector"
10 12
 )
11 13
 
12 14
 // ValidateProject tests required fields for a Project.
... ...
@@ -23,6 +25,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 +38,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 +50,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["openshift.io/node-selector"]; ok {
55
+			if _, err := labelselector.Parse(selector); err != nil {
56
+				allErrs = append(allErrs, fielderrors.NewFieldInvalid("nodeSelector", p.Annotations["openshift.io/node-selector"], "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
+						"openshift.io/node-selector": "infra=true, env = test",
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
+						"openshift.io/node-selector": "infra, env = $test",
121
+					},
122
+				},
123
+			},
124
+			// Should fail because infra and $test doesn't satisfy the format
125
+			numErrs: 1,
126
+		},
100 127
 	}
101 128
 
102 129
 	for _, tc := range testCases {
103 130
new file mode 100644
... ...
@@ -0,0 +1,115 @@
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
+	"github.com/openshift/origin/pkg/util/labelselector"
14
+)
15
+
16
+type ProjectCache struct {
17
+	Client              client.Interface
18
+	Store               cache.Store
19
+	DefaultNodeSelector string
20
+}
21
+
22
+var pcache *ProjectCache
23
+
24
+func (p *ProjectCache) GetNamespaceObject(name string) (*kapi.Namespace, error) {
25
+	// check for namespace in the cache
26
+	namespaceObj, exists, err := p.Store.Get(&kapi.Namespace{
27
+		ObjectMeta: kapi.ObjectMeta{
28
+			Name:      name,
29
+			Namespace: "",
30
+		},
31
+		Status: kapi.NamespaceStatus{},
32
+	})
33
+	if err != nil {
34
+		return nil, err
35
+	}
36
+
37
+	var namespace *kapi.Namespace
38
+	if exists {
39
+		namespace = namespaceObj.(*kapi.Namespace)
40
+	} else {
41
+		// Our watch maybe latent, so we make a best effort to get the object, and only fail if not found
42
+		namespace, err = p.Client.Namespaces().Get(name)
43
+		// the namespace does not exist, so prevent create and update in that namespace
44
+		if err != nil {
45
+			return nil, fmt.Errorf("Namespace %s does not exist", name)
46
+		}
47
+	}
48
+	return namespace, nil
49
+}
50
+
51
+func (p *ProjectCache) GetNodeSelector(namespace *kapi.Namespace) string {
52
+	selector := ""
53
+	if len(namespace.ObjectMeta.Annotations) > 0 {
54
+		if ns, ok := namespace.ObjectMeta.Annotations["openshift.io/node-selector"]; ok {
55
+			selector = ns
56
+		}
57
+	}
58
+	if len(selector) == 0 {
59
+		selector = p.DefaultNodeSelector
60
+	}
61
+	return selector
62
+}
63
+
64
+func (p *ProjectCache) GetNodeSelectorMap(namespace *kapi.Namespace) (map[string]string, error) {
65
+	selector := p.GetNodeSelector(namespace)
66
+	labelsMap, err := labelselector.Parse(selector)
67
+	if err != nil {
68
+		return map[string]string{}, err
69
+	}
70
+	return labelsMap, nil
71
+}
72
+
73
+func GetProjectCache() (*ProjectCache, error) {
74
+	if pcache == nil {
75
+		return nil, fmt.Errorf("project cache not initialized")
76
+	}
77
+	return pcache, nil
78
+}
79
+
80
+func RunProjectCache(c client.Interface, defaultNodeSelector string) {
81
+	if pcache != nil {
82
+		return
83
+	}
84
+
85
+	store := cache.NewStore(cache.MetaNamespaceKeyFunc)
86
+	reflector := cache.NewReflector(
87
+		&cache.ListWatch{
88
+			ListFunc: func() (runtime.Object, error) {
89
+				return c.Namespaces().List(labels.Everything(), fields.Everything())
90
+			},
91
+			WatchFunc: func(resourceVersion string) (watch.Interface, error) {
92
+				return c.Namespaces().Watch(labels.Everything(), fields.Everything(), resourceVersion)
93
+			},
94
+		},
95
+		&kapi.Namespace{},
96
+		store,
97
+		0,
98
+	)
99
+	reflector.Run()
100
+	pcache = &ProjectCache{
101
+		Client:              c,
102
+		Store:               store,
103
+		DefaultNodeSelector: defaultNodeSelector,
104
+	}
105
+}
106
+
107
+// Used for testing purpose only
108
+func FakeProjectCache(c client.Interface, store cache.Store, defaultNodeSelector string) {
109
+	pcache = &ProjectCache{
110
+		Client:              c,
111
+		Store:               store,
112
+		DefaultNodeSelector: defaultNodeSelector,
113
+	}
114
+}
0 115
new file mode 100644
... ...
@@ -0,0 +1,374 @@
0
+/*
1
+Copyright 2014 The Kubernetes Authors 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
+// labelselector is trim down version of k8s/pkg/labels/selector.go
17
+// It only accepts exact label matches
18
+// Example: "k1=v1, k2 = v2"
19
+
20
+package labelselector
21
+
22
+import (
23
+	"fmt"
24
+
25
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
26
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
27
+)
28
+
29
+// constants definition for lexer token
30
+type Token int
31
+
32
+const (
33
+	ErrorToken Token = iota
34
+	EndOfStringToken
35
+	CommaToken
36
+	EqualsToken
37
+	IdentifierToken // to represent keys and values
38
+)
39
+
40
+// string2token contains the mapping between lexer Token and token literal
41
+// (except IdentifierToken, EndOfStringToken and ErrorToken since it makes no sense)
42
+var string2token = map[string]Token{
43
+	",": CommaToken,
44
+	"=": EqualsToken,
45
+}
46
+
47
+// The item produced by the lexer. It contains the Token and the literal.
48
+type ScannedItem struct {
49
+	tok     Token
50
+	literal string
51
+}
52
+
53
+// isWhitespace returns true if the rune is a space, tab, or newline.
54
+func isWhitespace(ch byte) bool {
55
+	return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
56
+}
57
+
58
+// isSpecialSymbol detect if the character ch can be an operator
59
+func isSpecialSymbol(ch byte) bool {
60
+	switch ch {
61
+	case '=', ',':
62
+		return true
63
+	}
64
+	return false
65
+}
66
+
67
+// Lexer represents the Lexer struct for label selector.
68
+// It contains necessary informationt to tokenize the input string
69
+type Lexer struct {
70
+	// s stores the string to be tokenized
71
+	s string
72
+	// pos is the position currently tokenized
73
+	pos int
74
+}
75
+
76
+// read return the character currently lexed
77
+// increment the position and check the buffer overflow
78
+func (l *Lexer) read() (b byte) {
79
+	b = 0
80
+	if l.pos < len(l.s) {
81
+		b = l.s[l.pos]
82
+		l.pos++
83
+	}
84
+	return b
85
+}
86
+
87
+// unread 'undoes' the last read character
88
+func (l *Lexer) unread() {
89
+	l.pos--
90
+}
91
+
92
+// scanIdOrKeyword scans string to recognize literal token or an identifier.
93
+func (l *Lexer) scanIdOrKeyword() (tok Token, lit string) {
94
+	var buffer []byte
95
+IdentifierLoop:
96
+	for {
97
+		switch ch := l.read(); {
98
+		case ch == 0:
99
+			break IdentifierLoop
100
+		case isSpecialSymbol(ch) || isWhitespace(ch):
101
+			l.unread()
102
+			break IdentifierLoop
103
+		default:
104
+			buffer = append(buffer, ch)
105
+		}
106
+	}
107
+	s := string(buffer)
108
+	if val, ok := string2token[s]; ok { // is a literal token
109
+		return val, s
110
+	}
111
+	return IdentifierToken, s // otherwise is an identifier
112
+}
113
+
114
+// scanSpecialSymbol scans string starting with special symbol.
115
+// special symbol identify non literal operators: "="
116
+func (l *Lexer) scanSpecialSymbol() (Token, string) {
117
+	lastScannedItem := ScannedItem{}
118
+	var buffer []byte
119
+SpecialSymbolLoop:
120
+	for {
121
+		switch ch := l.read(); {
122
+		case ch == 0:
123
+			break SpecialSymbolLoop
124
+		case isSpecialSymbol(ch):
125
+			buffer = append(buffer, ch)
126
+			if token, ok := string2token[string(buffer)]; ok {
127
+				lastScannedItem = ScannedItem{tok: token, literal: string(buffer)}
128
+			} else if lastScannedItem.tok != 0 {
129
+				l.unread()
130
+				break SpecialSymbolLoop
131
+			}
132
+		default:
133
+			l.unread()
134
+			break SpecialSymbolLoop
135
+		}
136
+	}
137
+	if lastScannedItem.tok == 0 {
138
+		return ErrorToken, fmt.Sprintf("error expected: keyword found '%s'", buffer)
139
+	}
140
+	return lastScannedItem.tok, lastScannedItem.literal
141
+}
142
+
143
+// skipWhiteSpaces consumes all blank characters
144
+// returning the first non blank character
145
+func (l *Lexer) skipWhiteSpaces(ch byte) byte {
146
+	for {
147
+		if !isWhitespace(ch) {
148
+			return ch
149
+		}
150
+		ch = l.read()
151
+	}
152
+}
153
+
154
+// Lex returns a pair of Token and the literal
155
+// literal is meaningfull only for IdentifierToken token
156
+func (l *Lexer) Lex() (tok Token, lit string) {
157
+	switch ch := l.skipWhiteSpaces(l.read()); {
158
+	case ch == 0:
159
+		return EndOfStringToken, ""
160
+	case isSpecialSymbol(ch):
161
+		l.unread()
162
+		return l.scanSpecialSymbol()
163
+	default:
164
+		l.unread()
165
+		return l.scanIdOrKeyword()
166
+	}
167
+}
168
+
169
+// Parser data structure contains the label selector parser data strucutre
170
+type Parser struct {
171
+	l            *Lexer
172
+	scannedItems []ScannedItem
173
+	position     int
174
+}
175
+
176
+// lookahead func returns the current token and string. No increment of current position
177
+func (p *Parser) lookahead() (Token, string) {
178
+	tok, lit := p.scannedItems[p.position].tok, p.scannedItems[p.position].literal
179
+	return tok, lit
180
+}
181
+
182
+// consume returns current token and string. Increments the the position
183
+func (p *Parser) consume() (Token, string) {
184
+	p.position++
185
+	tok, lit := p.scannedItems[p.position-1].tok, p.scannedItems[p.position-1].literal
186
+	return tok, lit
187
+}
188
+
189
+// scan runs through the input string and stores the ScannedItem in an array
190
+// Parser can now lookahead and consume the tokens
191
+func (p *Parser) scan() {
192
+	for {
193
+		token, literal := p.l.Lex()
194
+		p.scannedItems = append(p.scannedItems, ScannedItem{token, literal})
195
+		if token == EndOfStringToken {
196
+			break
197
+		}
198
+	}
199
+}
200
+
201
+// parse runs the left recursive descending algorithm
202
+// on input string. It returns a list of map[key]value.
203
+func (p *Parser) parse() (map[string]string, error) {
204
+	p.scan() // init scannedItems
205
+
206
+	labelsMap := map[string]string{}
207
+	for {
208
+		tok, lit := p.lookahead()
209
+		switch tok {
210
+		case IdentifierToken:
211
+			key, value, err := p.parseLabel()
212
+			if err != nil {
213
+				return nil, fmt.Errorf("unable to parse requiremnt: %v", err)
214
+			}
215
+			labelsMap[key] = value
216
+			t, l := p.consume()
217
+			switch t {
218
+			case EndOfStringToken:
219
+				return labelsMap, nil
220
+			case CommaToken:
221
+				t2, l2 := p.lookahead()
222
+				if t2 != IdentifierToken {
223
+					return nil, fmt.Errorf("found '%s', expected: identifier after ','", l2)
224
+				}
225
+			default:
226
+				return nil, fmt.Errorf("found '%s', expected: ',' or 'end of string'", l)
227
+			}
228
+		case EndOfStringToken:
229
+			return labelsMap, nil
230
+		default:
231
+			return nil, fmt.Errorf("found '%s', expected: identifier or 'end of string'", lit)
232
+		}
233
+	}
234
+	return labelsMap, nil
235
+}
236
+
237
+func (p *Parser) parseLabel() (string, string, error) {
238
+	key, err := p.parseKey()
239
+	if err != nil {
240
+		return "", "", err
241
+	}
242
+	op, err := p.parseOperator()
243
+	if err != nil {
244
+		return "", "", err
245
+	}
246
+	if op != "=" {
247
+		return "", "", fmt.Errorf("Invalid operator: %s, expected: '='", op)
248
+	}
249
+	value, err := p.parseExactValue()
250
+	if err != nil {
251
+		return "", "", err
252
+	}
253
+	return key, value, nil
254
+}
255
+
256
+// parseKey parse literals.
257
+func (p *Parser) parseKey() (string, error) {
258
+	tok, literal := p.consume()
259
+	if tok != IdentifierToken {
260
+		err := fmt.Errorf("found '%s', expected: identifier", literal)
261
+		return "", err
262
+	}
263
+	if err := validateLabelKey(literal); err != nil {
264
+		return "", err
265
+	}
266
+	return literal, nil
267
+}
268
+
269
+// parseOperator returns operator
270
+func (p *Parser) parseOperator() (op string, err error) {
271
+	tok, lit := p.consume()
272
+	switch tok {
273
+	case EqualsToken:
274
+		op = "="
275
+	default:
276
+		return "", fmt.Errorf("found '%s', expected: '='", lit)
277
+	}
278
+	return op, nil
279
+}
280
+
281
+// parseExactValue parses the only value for exact match style
282
+func (p *Parser) parseExactValue() (string, error) {
283
+	tok, lit := p.consume()
284
+	if tok != IdentifierToken {
285
+		return "", fmt.Errorf("found '%s', expected: identifier", lit)
286
+	}
287
+	if err := validateLabelValue(lit); err != nil {
288
+		return "", err
289
+	}
290
+	return lit, nil
291
+}
292
+
293
+// Parse takes a string representing a selector and returns
294
+// map[key]value, or an error.
295
+// The input will cause an error if it does not follow this form:
296
+//
297
+// <selector-syntax> ::= [ <requirement> | <requirement> "," <selector-syntax> ]
298
+// <requirement> ::= KEY "=" VALUE
299
+// KEY is a sequence of one or more characters following [ DNS_SUBDOMAIN "/" ] DNS_LABEL
300
+// VALUE is a sequence of zero or more characters "([A-Za-z0-9_-\.])". Max length is 64 character.
301
+// Delimiter is white space: (' ', '\t')
302
+//
303
+//
304
+func Parse(selector string) (map[string]string, error) {
305
+	p := &Parser{l: &Lexer{s: selector, pos: 0}}
306
+	labels, error := p.parse()
307
+	if error != nil {
308
+		return map[string]string{}, error
309
+	}
310
+	return labels, nil
311
+}
312
+
313
+// Conflicts takes 2 maps
314
+// returns true if there a key match between the maps but the value doesn't match
315
+// returns false in other cases
316
+func Conflicts(labels1, labels2 map[string]string) bool {
317
+	for k, v := range labels1 {
318
+		if val, match := labels2[k]; match {
319
+			if val != v {
320
+				return true
321
+			}
322
+		}
323
+	}
324
+	return false
325
+}
326
+
327
+// Merge combines given maps
328
+// Note: It doesn't not check for any conflicts between the maps
329
+func Merge(labels1, labels2 map[string]string) map[string]string {
330
+	mergedMap := map[string]string{}
331
+
332
+	for k, v := range labels1 {
333
+		mergedMap[k] = v
334
+	}
335
+	for k, v := range labels2 {
336
+		mergedMap[k] = v
337
+	}
338
+	return mergedMap
339
+}
340
+
341
+// Equals returns true if the given maps are equal
342
+func Equals(labels1, labels2 map[string]string) bool {
343
+	if len(labels1) != len(labels2) {
344
+		return false
345
+	}
346
+
347
+	for k, v := range labels1 {
348
+		value, ok := labels2[k]
349
+		if !ok {
350
+			return false
351
+		}
352
+		if value != v {
353
+			return false
354
+		}
355
+	}
356
+	return true
357
+}
358
+
359
+const qualifiedNameErrorMsg string = "must match regex [" + util.DNS1123SubdomainFmt + " / ] " + util.DNS1123LabelFmt
360
+
361
+func validateLabelKey(k string) error {
362
+	if !util.IsQualifiedName(k) {
363
+		return fielderrors.NewFieldInvalid("label key", k, qualifiedNameErrorMsg)
364
+	}
365
+	return nil
366
+}
367
+
368
+func validateLabelValue(v string) error {
369
+	if !util.IsValidLabelValue(v) {
370
+		return fielderrors.NewFieldInvalid("label value", v, qualifiedNameErrorMsg)
371
+	}
372
+	return nil
373
+}
0 374
new file mode 100644
... ...
@@ -0,0 +1,192 @@
0
+/*
1
+Copyright 2014 The Kubernetes Authors 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 labelselector
17
+
18
+import (
19
+	"testing"
20
+)
21
+
22
+func TestLabelSelectorParse(t *testing.T) {
23
+	tests := []struct {
24
+		selector string
25
+		labels   map[string]string
26
+		valid    bool
27
+	}{
28
+		{
29
+			selector: "",
30
+			labels:   map[string]string{},
31
+			valid:    true,
32
+		},
33
+		{
34
+			selector: "   ",
35
+			labels:   map[string]string{},
36
+			valid:    true,
37
+		},
38
+		{
39
+			selector: "x=a",
40
+			labels:   map[string]string{"x": "a"},
41
+			valid:    true,
42
+		},
43
+		{
44
+			selector: "x=a,y=b,z=c",
45
+			labels:   map[string]string{"x": "a", "y": "b", "z": "c"},
46
+			valid:    true,
47
+		},
48
+		{
49
+			selector: "x = a, y=b ,z  = c  ",
50
+			labels:   map[string]string{"x": "a", "y": "b", "z": "c"},
51
+			valid:    true,
52
+		},
53
+		{
54
+			selector: "color=green, env = test ,service= front ",
55
+			labels:   map[string]string{"color": "green", "env": "test", "service": "front"},
56
+			valid:    true,
57
+		},
58
+		{
59
+			selector: ",",
60
+			labels:   map[string]string{},
61
+			valid:    false,
62
+		},
63
+		{
64
+			selector: "x",
65
+			labels:   map[string]string{},
66
+			valid:    false,
67
+		},
68
+		{
69
+			selector: "x,y",
70
+			labels:   map[string]string{},
71
+			valid:    false,
72
+		},
73
+		{
74
+			selector: "x=$y",
75
+			labels:   map[string]string{},
76
+			valid:    false,
77
+		},
78
+		{
79
+			selector: "x!=y",
80
+			labels:   map[string]string{},
81
+			valid:    false,
82
+		},
83
+		{
84
+			selector: "x==y",
85
+			labels:   map[string]string{},
86
+			valid:    false,
87
+		},
88
+		{
89
+			selector: "x=a||y=b",
90
+			labels:   map[string]string{},
91
+			valid:    false,
92
+		},
93
+		{
94
+			selector: "x in (y)",
95
+			labels:   map[string]string{},
96
+			valid:    false,
97
+		},
98
+		{
99
+			selector: "x notin (y)",
100
+			labels:   map[string]string{},
101
+			valid:    false,
102
+		},
103
+		{
104
+			selector: "x y",
105
+			labels:   map[string]string{},
106
+			valid:    false,
107
+		},
108
+	}
109
+	for _, test := range tests {
110
+		labels, err := Parse(test.selector)
111
+		if test.valid && err != nil {
112
+			t.Errorf("selector: %s, expected no error but got: %s", test.selector, err)
113
+		} else if !test.valid && err == nil {
114
+			t.Errorf("selector: %s, expected an error", test.selector)
115
+		}
116
+
117
+		if !Equals(labels, test.labels) {
118
+			t.Errorf("expected: %s but got: %s", test.labels, labels)
119
+		}
120
+	}
121
+}
122
+
123
+func TestLabelConflict(t *testing.T) {
124
+	tests := []struct {
125
+		labels1  map[string]string
126
+		labels2  map[string]string
127
+		conflict bool
128
+	}{
129
+		{
130
+			labels1:  map[string]string{},
131
+			labels2:  map[string]string{},
132
+			conflict: false,
133
+		},
134
+		{
135
+			labels1:  map[string]string{"env": "test"},
136
+			labels2:  map[string]string{"infra": "true"},
137
+			conflict: false,
138
+		},
139
+		{
140
+			labels1:  map[string]string{"env": "test"},
141
+			labels2:  map[string]string{"infra": "true", "env": "test"},
142
+			conflict: false,
143
+		},
144
+		{
145
+			labels1:  map[string]string{"env": "test"},
146
+			labels2:  map[string]string{"env": "dev"},
147
+			conflict: true,
148
+		},
149
+		{
150
+			labels1:  map[string]string{"env": "test", "infra": "false"},
151
+			labels2:  map[string]string{"infra": "true", "color": "blue"},
152
+			conflict: true,
153
+		},
154
+	}
155
+	for _, test := range tests {
156
+		conflict := Conflicts(test.labels1, test.labels2)
157
+		if conflict != test.conflict {
158
+			t.Errorf("expected: %v but got: %v", test.conflict, conflict)
159
+		}
160
+	}
161
+}
162
+
163
+func TestLabelMerge(t *testing.T) {
164
+	tests := []struct {
165
+		labels1      map[string]string
166
+		labels2      map[string]string
167
+		mergedLabels map[string]string
168
+	}{
169
+		{
170
+			labels1:      map[string]string{},
171
+			labels2:      map[string]string{},
172
+			mergedLabels: map[string]string{},
173
+		},
174
+		{
175
+			labels1:      map[string]string{"infra": "true"},
176
+			labels2:      map[string]string{},
177
+			mergedLabels: map[string]string{"infra": "true"},
178
+		},
179
+		{
180
+			labels1:      map[string]string{"infra": "true"},
181
+			labels2:      map[string]string{"env": "test", "color": "blue"},
182
+			mergedLabels: map[string]string{"infra": "true", "env": "test", "color": "blue"},
183
+		},
184
+	}
185
+	for _, test := range tests {
186
+		mergedLabels := Merge(test.labels1, test.labels2)
187
+		if !Equals(mergedLabels, test.mergedLabels) {
188
+			t.Errorf("expected: %v but got: %v", test.mergedLabels, mergedLabels)
189
+		}
190
+	}
191
+}
... ...
@@ -132,7 +132,8 @@ func TestProjectIsNamespace(t *testing.T) {
132 132
 		ObjectMeta: kapi.ObjectMeta{
133 133
 			Name: "new-project",
134 134
 			Annotations: map[string]string{
135
-				"displayName": "Hello World",
135
+				"displayName":                "Hello World",
136
+				"openshift.io/node-selector": "env=test",
136 137
 			},
137 138
 		},
138 139
 	}
... ...
@@ -152,7 +153,9 @@ func TestProjectIsNamespace(t *testing.T) {
152 152
 	if project.Annotations["displayName"] != namespace.Annotations["displayName"] {
153 153
 		t.Fatalf("Project display name did not match namespace annotation, project %v, namespace %v", project.Annotations["displayName"], namespace.Annotations["displayName"])
154 154
 	}
155
-
155
+	if project.Annotations["openshift.io/node-selector"] != namespace.Annotations["openshift.io/node-selector"] {
156
+		t.Fatalf("Project node selector did not match namespace node selector, project %v, namespace %v", project.Annotations["openshift.io/node-selector"], namespace.Annotations["openshift.io/node-selector"])
157
+	}
156 158
 }
157 159
 
158 160
 // TestProjectMustExist verifies that content cannot be added in a project that does not exist
... ...
@@ -211,5 +214,4 @@ func TestProjectMustExist(t *testing.T) {
211 211
 	if err == nil {
212 212
 		t.Errorf("Expected an error on creation of a Origin resource because namespace does not exist")
213 213
 	}
214
-
215 214
 }