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.
... | ... |
@@ -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 |
} |
... | ... |
@@ -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 |
} |