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