diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 9d048ea4f250..322fe2fce890 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -215,6 +215,11 @@ "Comment": "v0.10.0-503-gc977a45", "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota", + "Comment": "v0.10.0-503-gc977a45", + "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime", "Comment": "v0.10.0-503-gc977a45", @@ -265,6 +270,26 @@ "Comment": "v0.10.0-503-gc977a45", "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger", + "Comment": "v0.10.0-503-gc977a45", + "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists", + "Comment": "v0.10.0-503-gc977a45", + "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults", + "Comment": "v0.10.0-503-gc977a45", + "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" + }, + { + "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota", + "Comment": "v0.10.0-503-gc977a45", + "Rev": "c977a458642b4dbd8c3ad9cfc9eecafc85fb6183" + }, { "ImportPath": "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile", "Comment": "v0.10.0-503-gc977a45", diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota/doc.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota/doc.go new file mode 100644 index 000000000000..2e31b0493327 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// resourcequota contains a controller that makes resource quota usage observations +package resourcequota diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota/resource_quota_controller.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota/resource_quota_controller.go new file mode 100644 index 000000000000..1b2d83f5d18e --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota/resource_quota_controller.go @@ -0,0 +1,194 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcequota + +import ( + "sync" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" +) + +// ResourceQuotaManager is responsible for tracking quota usage status in the system +type ResourceQuotaManager struct { + kubeClient client.Interface + syncTime <-chan time.Time + + // To allow injection of syncUsage for testing. + syncHandler func(quota api.ResourceQuota) error +} + +// NewResourceQuotaManager creates a new ResourceQuotaManager +func NewResourceQuotaManager(kubeClient client.Interface) *ResourceQuotaManager { + + rm := &ResourceQuotaManager{ + kubeClient: kubeClient, + } + + // set the synchronization handler + rm.syncHandler = rm.syncResourceQuota + return rm +} + +// Run begins watching and syncing. +func (rm *ResourceQuotaManager) Run(period time.Duration) { + rm.syncTime = time.Tick(period) + go util.Forever(func() { rm.synchronize() }, period) +} + +func (rm *ResourceQuotaManager) synchronize() { + var resourceQuotas []api.ResourceQuota + list, err := rm.kubeClient.ResourceQuotas(api.NamespaceAll).List(labels.Everything()) + if err != nil { + glog.Errorf("Synchronization error: %v (%#v)", err, err) + } + resourceQuotas = list.Items + wg := sync.WaitGroup{} + wg.Add(len(resourceQuotas)) + for ix := range resourceQuotas { + go func(ix int) { + defer wg.Done() + glog.V(4).Infof("periodic sync of %v/%v", resourceQuotas[ix].Namespace, resourceQuotas[ix].Name) + err := rm.syncHandler(resourceQuotas[ix]) + if err != nil { + glog.Errorf("Error synchronizing: %v", err) + } + }(ix) + } + wg.Wait() +} + +// syncResourceQuota runs a complete sync of current status +func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err error) { + + // dirty tracks if the usage status differs from the previous sync, + // if so, we send a new usage with latest status + // if this is our first sync, it will be dirty by default, since we need track usage + dirty := quota.Status.Hard == nil || quota.Status.Used == nil + + // Create a usage object that is based on the quota resource version + usage := api.ResourceQuotaUsage{ + ObjectMeta: api.ObjectMeta{ + Name: quota.Name, + Namespace: quota.Namespace, + ResourceVersion: quota.ResourceVersion}, + Status: api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + }, + } + // populate the usage with the current observed hard/used limits + usage.Status.Hard = quota.Spec.Hard + usage.Status.Used = quota.Status.Used + + set := map[api.ResourceName]bool{} + for k := range usage.Status.Hard { + set[k] = true + } + + pods := &api.PodList{} + if set[api.ResourcePods] || set[api.ResourceMemory] || set[api.ResourceCPU] { + pods, err = rm.kubeClient.Pods(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + } + + // iterate over each resource, and update observation + for k := range usage.Status.Hard { + + // look if there is a used value, if none, we are definitely dirty + prevQuantity, found := usage.Status.Used[k] + if !found { + dirty = true + } + + var value *resource.Quantity + + switch k { + case api.ResourcePods: + value = resource.NewQuantity(int64(len(pods.Items)), resource.DecimalSI) + case api.ResourceMemory: + val := int64(0) + for i := range pods.Items { + val = val + PodMemory(&pods.Items[i]).Value() + } + value = resource.NewQuantity(int64(val), resource.DecimalSI) + case api.ResourceCPU: + val := int64(0) + for i := range pods.Items { + val = val + PodCPU(&pods.Items[i]).MilliValue() + } + value = resource.NewMilliQuantity(int64(val), resource.DecimalSI) + case api.ResourceServices: + items, err := rm.kubeClient.Services(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + value = resource.NewQuantity(int64(len(items.Items)), resource.DecimalSI) + case api.ResourceReplicationControllers: + items, err := rm.kubeClient.ReplicationControllers(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + value = resource.NewQuantity(int64(len(items.Items)), resource.DecimalSI) + case api.ResourceQuotas: + items, err := rm.kubeClient.ResourceQuotas(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + value = resource.NewQuantity(int64(len(items.Items)), resource.DecimalSI) + } + + // ignore fields we do not understand (assume another controller is tracking it) + if value != nil { + // see if the value has changed + dirty = dirty || (value.Value() != prevQuantity.Value()) + // just update the value + usage.Status.Used[k] = *value + } + } + + // update the usage only if it changed + if dirty { + return rm.kubeClient.ResourceQuotaUsages(usage.Namespace).Create(&usage) + } + return nil +} + +// PodCPU computes total cpu usage of a pod +func PodCPU(pod *api.Pod) *resource.Quantity { + val := int64(0) + for j := range pod.Spec.Containers { + val = val + pod.Spec.Containers[j].Resources.Limits.Cpu().MilliValue() + } + return resource.NewMilliQuantity(int64(val), resource.DecimalSI) +} + +// PodMemory computes the memory usage of a pod +func PodMemory(pod *api.Pod) *resource.Quantity { + val := int64(0) + for j := range pod.Spec.Containers { + val = val + pod.Spec.Containers[j].Resources.Limits.Memory().Value() + } + return resource.NewQuantity(int64(val), resource.DecimalSI) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/admission.go new file mode 100644 index 000000000000..bf7f4f83a4c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/admission.go @@ -0,0 +1,175 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package limitranger + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func init() { + admission.RegisterPlugin("LimitRanger", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewLimitRanger(client, PodLimitFunc), nil + }) +} + +// limitRanger enforces usage limits on a per resource basis in the namespace +type limitRanger struct { + client client.Interface + limitFunc LimitFunc +} + +// Admit admits resources into cluster that do not violate any defined LimitRange in the namespace +func (l *limitRanger) Admit(a admission.Attributes) (err error) { + // ignore deletes + if a.GetOperation() == "DELETE" { + return nil + } + + // look for a limit range in current namespace that requires enforcement + // TODO: Move to cache when issue is resolved: https://github.com/GoogleCloudPlatform/kubernetes/issues/2294 + items, err := l.client.LimitRanges(a.GetNamespace()).List(labels.Everything()) + if err != nil { + return err + } + + // ensure it meets each prescribed min/max + for i := range items.Items { + limitRange := &items.Items[i] + err = l.limitFunc(limitRange, a.GetResource(), a.GetObject()) + if err != nil { + return err + } + } + return nil +} + +// NewLimitRanger returns an object that enforces limits based on the supplied limit function +func NewLimitRanger(client client.Interface, limitFunc LimitFunc) admission.Interface { + return &limitRanger{client: client, limitFunc: limitFunc} +} + +func Min(a int64, b int64) int64 { + if a < b { + return a + } + return b +} + +func Max(a int64, b int64) int64 { + if a > b { + return a + } + return b +} + +// PodLimitFunc enforces that a pod spec does not exceed any limits specified on the supplied limit range +func PodLimitFunc(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error { + if resourceName != "pods" { + return nil + } + + pod := obj.(*api.Pod) + + podCPU := int64(0) + podMem := int64(0) + + minContainerCPU := int64(0) + minContainerMem := int64(0) + maxContainerCPU := int64(0) + maxContainerMem := int64(0) + + for i := range pod.Spec.Containers { + container := pod.Spec.Containers[i] + containerCPU := container.Resources.Limits.Cpu().MilliValue() + containerMem := container.Resources.Limits.Memory().Value() + + if i == 0 { + minContainerCPU = containerCPU + minContainerMem = containerMem + maxContainerCPU = containerCPU + maxContainerMem = containerMem + } + + podCPU = podCPU + container.Resources.Limits.Cpu().MilliValue() + podMem = podMem + container.Resources.Limits.Memory().Value() + + minContainerCPU = Min(containerCPU, minContainerCPU) + minContainerMem = Min(containerMem, minContainerMem) + maxContainerCPU = Max(containerCPU, maxContainerCPU) + maxContainerMem = Max(containerMem, maxContainerMem) + } + + for i := range limitRange.Spec.Limits { + limit := limitRange.Spec.Limits[i] + for _, minOrMax := range []string{"Min", "Max"} { + var rl api.ResourceList + switch minOrMax { + case "Min": + rl = limit.Min + case "Max": + rl = limit.Max + } + for k, v := range rl { + observed := int64(0) + enforced := int64(0) + var err error + switch k { + case api.ResourceMemory: + enforced = v.Value() + switch limit.Type { + case api.LimitTypePod: + observed = podMem + err = fmt.Errorf("%simum memory usage per pod is %s", minOrMax, v.String()) + case api.LimitTypeContainer: + observed = maxContainerMem + err = fmt.Errorf("%simum memory usage per container is %s", minOrMax, v.String()) + } + case api.ResourceCPU: + enforced = v.MilliValue() + switch limit.Type { + case api.LimitTypePod: + observed = podCPU + err = fmt.Errorf("%simum CPU usage per pod is %s, but requested %s", minOrMax, v.String(), resource.NewMilliQuantity(observed, resource.DecimalSI)) + case api.LimitTypeContainer: + observed = maxContainerCPU + err = fmt.Errorf("%simum CPU usage per container is %s", minOrMax, v.String()) + } + } + switch minOrMax { + case "Min": + if observed < enforced { + return apierrors.NewForbidden(resourceName, pod.Name, err) + } + case "Max": + if observed > enforced { + return apierrors.NewForbidden(resourceName, pod.Name, err) + } + } + } + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/admission_test.go new file mode 100644 index 000000000000..d86e839334d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/admission_test.go @@ -0,0 +1,221 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package limitranger + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +func getResourceRequirements(cpu, memory string) api.ResourceRequirements { + res := api.ResourceRequirements{} + res.Limits = api.ResourceList{} + if cpu != "" { + res.Limits[api.ResourceCPU] = resource.MustParse(cpu) + } + if memory != "" { + res.Limits[api.ResourceMemory] = resource.MustParse(memory) + } + + return res +} + +func TestPodLimitFunc(t *testing.T) { + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: getResourceRequirements("200m", "4Gi").Limits, + Min: getResourceRequirements("50m", "2Mi").Limits, + }, + { + Type: api.LimitTypeContainer, + Max: getResourceRequirements("100m", "2Gi").Limits, + Min: getResourceRequirements("25m", "1Mi").Limits, + }, + }, + }, + } + + successCases := []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "foo:V1", + Resources: getResourceRequirements("100m", "2Gi"), + }, + { + Image: "boo:V1", + Resources: getResourceRequirements("100m", "2Gi"), + }, + }, + }, + }, + { + ObjectMeta: api.ObjectMeta{Name: "bar"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("100m", "2Gi"), + }, + }, + }, + }, + } + + errorCases := map[string]api.Pod{ + "min-container-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("25m", "2Gi"), + }, + }, + }, + }, + "max-container-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("110m", "1Gi"), + }, + }, + }, + }, + "min-container-mem": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("30m", "0"), + }, + }, + }, + }, + "max-container-mem": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("30m", "3Gi"), + }, + }, + }, + }, + "min-pod-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("40m", "2Gi"), + }, + }, + }, + }, + "max-pod-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("60m", "1Mi"), + }, + { + Image: "boo:V2", + Resources: getResourceRequirements("60m", "1Mi"), + }, + { + Image: "boo:V3", + Resources: getResourceRequirements("60m", "1Mi"), + }, + { + Image: "boo:V4", + Resources: getResourceRequirements("60m", "1Mi"), + }, + }, + }, + }, + "max-pod-memory": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("60m", "2Gi"), + }, + { + Image: "boo:V2", + Resources: getResourceRequirements("60m", "2Gi"), + }, + { + Image: "boo:V3", + Resources: getResourceRequirements("60m", "2Gi"), + }, + }, + }, + }, + "min-pod-memory": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + Resources: getResourceRequirements("60m", "0"), + }, + { + Image: "boo:V2", + Resources: getResourceRequirements("60m", "0"), + }, + { + Image: "boo:V3", + Resources: getResourceRequirements("60m", "0"), + }, + }, + }, + }, + } + + for i := range successCases { + err := PodLimitFunc(limitRange, "pods", &successCases[i]) + if err != nil { + t.Errorf("Unexpected error for valid pod: %v, %v", successCases[i].Name, err) + } + } + + for k, v := range errorCases { + err := PodLimitFunc(limitRange, "pods", &v) + if err == nil { + t.Errorf("Expected error for %s", k) + } + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/interfaces.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/interfaces.go new file mode 100644 index 000000000000..57d7c20bd8e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger/interfaces.go @@ -0,0 +1,25 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package limitranger + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// LimitFunc is a pluggable function to enforce limits on the object +type LimitFunc func(limitRange *api.LimitRange, kind string, obj runtime.Object) error diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists/admission.go new file mode 100644 index 000000000000..453b0860cd33 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists/admission.go @@ -0,0 +1,98 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exists + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +func init() { + admission.RegisterPlugin("NamespaceExists", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewExists(client), nil + }) +} + +// exists is an implementation of admission.Interface. +// It rejects all incoming requests in a namespace context if the namespace does not exist. +// It is useful in deployments that want to enforce pre-declaration of a Namespace resource. +type exists struct { + client client.Interface + store cache.Store +} + +func (e *exists) Admit(a admission.Attributes) (err error) { + defaultVersion, kind, err := latest.RESTMapper.VersionAndKindForResource(a.GetResource()) + if err != nil { + return err + } + mapping, err := latest.RESTMapper.RESTMapping(kind, defaultVersion) + if err != nil { + return err + } + if mapping.Scope.Name() != meta.RESTScopeNameNamespace { + return nil + } + namespace := &api.Namespace{ + ObjectMeta: api.ObjectMeta{ + Name: a.GetNamespace(), + Namespace: "", + }, + Status: api.NamespaceStatus{}, + } + _, exists, err := e.store.Get(namespace) + if err != nil { + return err + } + if exists { + return nil + } + obj := a.GetObject() + name := "Unknown" + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + return apierrors.NewForbidden(kind, name, fmt.Errorf("Namespace %s does not exist", a.GetNamespace())) +} + +func NewExists(c client.Interface) admission.Interface { + store := cache.NewStore(cache.MetaNamespaceKeyFunc) + // TODO: look into a list/watch that can work with client.Interface, maybe pass it a ListFunc and a WatchFunc + reflector := cache.NewReflector( + &cache.ListWatch{ + Client: c.(*client.Client), + FieldSelector: labels.Everything(), + Resource: "namespaces", + }, + &api.Namespace{}, + store, + ) + reflector.Run() + return &exists{ + client: c, + store: store, + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists/admission_test.go new file mode 100644 index 000000000000..b1d367a3e050 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists/admission_test.go @@ -0,0 +1,17 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exists diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/admission.go new file mode 100644 index 000000000000..3e3882b07a4a --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/admission.go @@ -0,0 +1,73 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcedefaults + +import ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func init() { + admission.RegisterPlugin("ResourceDefaults", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewResourceDefaults(), nil + }) +} + +const ( + defaultMemory string = "512Mi" + defaultCPU string = "1" +) + +// resourceDefaults is an implementation of admission.Interface which applies default resource limits (cpu/memory) +// It is useful for clusters that do not want to support unlimited usage constraints, but instead supply sensible defaults +type resourceDefaults struct{} + +func (resourceDefaults) Admit(a admission.Attributes) (err error) { + // ignore deletes, only process create and update + if a.GetOperation() == "DELETE" { + return nil + } + + // we only care about pods + if a.GetResource() != "pods" { + return nil + } + + // get the pod, so we can validate each of the containers within have default mem / cpu constraints + obj := a.GetObject() + pod := obj.(*api.Pod) + for index := range pod.Spec.Containers { + if pod.Spec.Containers[index].Resources.Limits == nil { + pod.Spec.Containers[index].Resources.Limits = api.ResourceList{} + } + if pod.Spec.Containers[index].Resources.Limits.Memory().Value() == 0 { + pod.Spec.Containers[index].Resources.Limits[api.ResourceMemory] = resource.MustParse(defaultMemory) + } + if pod.Spec.Containers[index].Resources.Limits.Cpu().Value() == 0 { + pod.Spec.Containers[index].Resources.Limits[api.ResourceCPU] = resource.MustParse(defaultCPU) + } + } + return nil +} + +func NewResourceDefaults() admission.Interface { + return new(resourceDefaults) +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/admission_test.go new file mode 100644 index 000000000000..b0a0ddd8039d --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/admission_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcedefaults + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +func TestAdmission(t *testing.T) { + namespace := "default" + + handler := NewResourceDefaults() + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: "ns"}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image"}}, + }, + } + + err := handler.Admit(admission.NewAttributesRecord(&pod, namespace, "pods", "CREATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } + + for i := range pod.Spec.Containers { + memory := pod.Spec.Containers[i].Resources.Limits.Memory().String() + cpu := pod.Spec.Containers[i].Resources.Limits.Cpu().String() + if memory != "512Mi" { + t.Errorf("Unexpected memory value %s", memory) + } + if cpu != "1" { + t.Errorf("Unexpected cpu value %s", cpu) + } + } +} + +func TestIgnoreAdmission(t *testing.T) { + namespace := "default" + + handler := NewResourceDefaults() + pod := api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: "ns"}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{ + { + Name: "ctr", + Image: "image", + Resources: api.ResourceRequirements{ + Limits: getResourceLimits("2", "750Mi"), + }, + }, + }, + }, + } + + err := handler.Admit(admission.NewAttributesRecord(&pod, namespace, "pods", "CREATE")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler") + } + + for i := range pod.Spec.Containers { + memory := pod.Spec.Containers[i].Resources.Limits.Memory().String() + cpu := pod.Spec.Containers[i].Resources.Limits.Cpu().String() + if memory != "750Mi" { + t.Errorf("Unexpected memory value %s", memory) + } + if cpu != "2" { + t.Errorf("Unexpected cpu value %s", cpu) + } + } +} + +func getResourceLimits(cpu, memory string) api.ResourceList { + res := api.ResourceList{} + res[api.ResourceCPU] = resource.MustParse(cpu) + res[api.ResourceMemory] = resource.MustParse(memory) + return res +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/doc.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/doc.go new file mode 100644 index 000000000000..38f10213b6fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults/doc.go @@ -0,0 +1,24 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package resourcedefaults contains an example plug-in that +// cluster operators can use to prevent unlimited CPU and +// Memory usage for a container. It intercepts all pod +// create and update requests and applies a default +// Memory and CPU quantity if none is supplied. +// This plug-in can be enhanced in the future to make the default value +// configurable via the admission control configuration file. +package resourcedefaults diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/admission.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/admission.go new file mode 100644 index 000000000000..bad1edbb750b --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/admission.go @@ -0,0 +1,177 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcequota + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" +) + +func init() { + admission.RegisterPlugin("ResourceQuota", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewResourceQuota(client), nil + }) +} + +type quota struct { + client client.Interface +} + +func NewResourceQuota(client client.Interface) admission.Interface { + return "a{client: client} +} + +var resourceToResourceName = map[string]api.ResourceName{ + "pods": api.ResourcePods, + "services": api.ResourceServices, + "replicationControllers": api.ResourceReplicationControllers, + "resourceQuotas": api.ResourceQuotas, +} + +func (q *quota) Admit(a admission.Attributes) (err error) { + if a.GetOperation() == "DELETE" { + return nil + } + + obj := a.GetObject() + resource := a.GetResource() + name := "Unknown" + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + + list, err := q.client.ResourceQuotas(a.GetNamespace()).List(labels.Everything()) + if err != nil { + return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), resource)) + } + + if len(list.Items) == 0 { + return nil + } + + for i := range list.Items { + quota := list.Items[i] + dirty, err := IncrementUsage(a, "a.Status, q.client) + if err != nil { + return err + } + + if dirty { + // construct a usage record + usage := api.ResourceQuotaUsage{ + ObjectMeta: api.ObjectMeta{ + Name: quota.Name, + Namespace: quota.Namespace, + ResourceVersion: quota.ResourceVersion}, + } + usage.Status = quota.Status + err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage) + if err != nil { + return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetResource())) + } + } + } + return nil +} + +// IncrementUsage updates the supplied ResourceQuotaStatus object based on the incoming operation +// Return true if the usage must be recorded prior to admitting the new resource +// Return an error if the operation should not pass admission control +func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) { + obj := a.GetObject() + resourceName := a.GetResource() + name := "Unknown" + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + dirty := false + set := map[api.ResourceName]bool{} + for k := range status.Hard { + set[k] = true + } + // handle max counts for each kind of resource (pods, services, replicationControllers, etc.) + if a.GetOperation() == "CREATE" { + resourceName := resourceToResourceName[a.GetResource()] + hard, hardFound := status.Hard[resourceName] + if hardFound { + used, usedFound := status.Used[resourceName] + if !usedFound { + return false, apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + } + if used.Value() >= hard.Value() { + return false, apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Limited to %s %s", hard.String(), a.GetResource())) + } else { + status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI) + dirty = true + } + } + } + // handle memory/cpu constraints, and any diff of usage based on memory/cpu on updates + if a.GetResource() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) { + pod := obj.(*api.Pod) + deltaCPU := resourcequota.PodCPU(pod) + deltaMemory := resourcequota.PodMemory(pod) + // if this is an update, we need to find the delta cpu/memory usage from previous state + if a.GetOperation() == "UPDATE" { + oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name) + if err != nil { + return false, apierrors.NewForbidden(resourceName, name, err) + } + oldCPU := resourcequota.PodCPU(oldPod) + oldMemory := resourcequota.PodMemory(oldPod) + deltaCPU = resource.NewMilliQuantity(deltaCPU.MilliValue()-oldCPU.MilliValue(), resource.DecimalSI) + deltaMemory = resource.NewQuantity(deltaMemory.Value()-oldMemory.Value(), resource.DecimalSI) + } + + hardMem, hardMemFound := status.Hard[api.ResourceMemory] + if hardMemFound { + used, usedFound := status.Used[api.ResourceMemory] + if !usedFound { + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + } + if used.Value()+deltaMemory.Value() > hardMem.Value() { + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Limited to %s memory", hardMem.String())) + } else { + status.Used[api.ResourceMemory] = *resource.NewQuantity(used.Value()+deltaMemory.Value(), resource.DecimalSI) + dirty = true + } + } + hardCPU, hardCPUFound := status.Hard[api.ResourceCPU] + if hardCPUFound { + used, usedFound := status.Used[api.ResourceCPU] + if !usedFound { + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + } + if used.MilliValue()+deltaCPU.MilliValue() > hardCPU.MilliValue() { + return false, apierrors.NewForbidden(resourceName, name, fmt.Errorf("Limited to %s CPU", hardCPU.String())) + } else { + status.Used[api.ResourceCPU] = *resource.NewMilliQuantity(used.MilliValue()+deltaCPU.MilliValue(), resource.DecimalSI) + dirty = true + } + } + } + return dirty, nil +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/admission_test.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/admission_test.go new file mode 100644 index 000000000000..c42af19538e2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/admission_test.go @@ -0,0 +1,377 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resourcequota + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func getResourceRequirements(cpu, memory string) api.ResourceRequirements { + res := api.ResourceRequirements{} + res.Limits = api.ResourceList{} + if cpu != "" { + res.Limits[api.ResourceCPU] = resource.MustParse(cpu) + } + if memory != "" { + res.Limits[api.ResourceMemory] = resource.MustParse(memory) + } + + return res +} + +func TestAdmissionIgnoresDelete(t *testing.T) { + namespace := "default" + handler := NewResourceQuota(&client.Fake{}) + err := handler.Admit(admission.NewAttributesRecord(nil, namespace, "pods", "DELETE")) + if err != nil { + t.Errorf("ResourceQuota should admit all deletes", err) + } +} + +func TestIncrementUsagePods(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourcePods + status.Hard[r] = resource.MustParse("2") + status.Used[r] = resource.MustParse("1") + dirty, err := IncrementUsage(admission.NewAttributesRecord(&api.Pod{}, namespace, "pods", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + quantity := status.Used[r] + if quantity.Value() != int64(2) { + t.Errorf("Expected new item count to be 2, but was %s", quantity.String()) + } +} + +func TestIncrementUsageMemory(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceMemory + status.Hard[r] = resource.MustParse("2Gi") + status.Used[r] = resource.MustParse("1Gi") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }} + dirty, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + expectedVal := resource.MustParse("2Gi") + quantity := status.Used[r] + if quantity.Value() != expectedVal.Value() { + t.Errorf("Expected %v was %v", expectedVal.Value(), quantity.Value()) + } +} + +func TestExceedUsageMemory(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceMemory + status.Hard[r] = resource.MustParse("2Gi") + status.Used[r] = resource.MustParse("1Gi") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "3Gi")}}, + }} + _, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected memory usage exceeded error") + } +} + +func TestIncrementUsageCPU(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceCPU + status.Hard[r] = resource.MustParse("200m") + status.Used[r] = resource.MustParse("100m") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }} + dirty, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + expectedVal := resource.MustParse("200m") + quantity := status.Used[r] + if quantity.Value() != expectedVal.Value() { + t.Errorf("Expected %v was %v", expectedVal.Value(), quantity.Value()) + } +} + +func TestExceedUsageCPU(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceCPU + status.Hard[r] = resource.MustParse("200m") + status.Used[r] = resource.MustParse("100m") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("500m", "1Gi")}}, + }} + _, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected CPU usage exceeded error") + } +} + +func TestExceedUsagePods(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourcePods + status.Hard[r] = resource.MustParse("1") + status.Used[r] = resource.MustParse("1") + _, err := IncrementUsage(admission.NewAttributesRecord(&api.Pod{}, namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected error because this would exceed your quota") + } +} + +func TestIncrementUsageServices(t *testing.T) { + namespace := "default" + client := &client.Fake{ + ServiceList: api.ServiceList{ + Items: []api.Service{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceServices + status.Hard[r] = resource.MustParse("2") + status.Used[r] = resource.MustParse("1") + dirty, err := IncrementUsage(admission.NewAttributesRecord(&api.Service{}, namespace, "services", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + quantity := status.Used[r] + if quantity.Value() != int64(2) { + t.Errorf("Expected new item count to be 2, but was %s", quantity.String()) + } +} + +func TestExceedUsageServices(t *testing.T) { + namespace := "default" + client := &client.Fake{ + ServiceList: api.ServiceList{ + Items: []api.Service{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceServices + status.Hard[r] = resource.MustParse("1") + status.Used[r] = resource.MustParse("1") + _, err := IncrementUsage(admission.NewAttributesRecord(&api.Service{}, namespace, "services", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected error because this would exceed usage") + } +} + +func TestIncrementUsageReplicationControllers(t *testing.T) { + namespace := "default" + client := &client.Fake{ + CtrlList: api.ReplicationControllerList{ + Items: []api.ReplicationController{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceReplicationControllers + status.Hard[r] = resource.MustParse("2") + status.Used[r] = resource.MustParse("1") + dirty, err := IncrementUsage(admission.NewAttributesRecord(&api.ReplicationController{}, namespace, "replicationControllers", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + quantity := status.Used[r] + if quantity.Value() != int64(2) { + t.Errorf("Expected new item count to be 2, but was %s", quantity.String()) + } +} + +func TestExceedUsageReplicationControllers(t *testing.T) { + namespace := "default" + client := &client.Fake{ + CtrlList: api.ReplicationControllerList{ + Items: []api.ReplicationController{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceReplicationControllers + status.Hard[r] = resource.MustParse("1") + status.Used[r] = resource.MustParse("1") + _, err := IncrementUsage(admission.NewAttributesRecord(&api.ReplicationController{}, namespace, "replicationControllers", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected error for exceeding hard limits") + } +} diff --git a/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/doc.go b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/doc.go new file mode 100644 index 000000000000..6e99dc3ab0b0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// resourcequota enforces all incoming requests against any applied quota +// in the namespace context of the request +package resourcequota diff --git a/Godeps/_workspace/src/golang.org/x/net/context/context.go b/Godeps/_workspace/src/golang.org/x/net/context/context.go index 66aff7cb4a04..490245de9e6f 100644 --- a/Godeps/_workspace/src/golang.org/x/net/context/context.go +++ b/Godeps/_workspace/src/golang.org/x/net/context/context.go @@ -34,7 +34,7 @@ // // See http://blog.golang.org/context for example code for a server that uses // Contexts. -package context +package context // import "golang.org/x/net/context" import ( "errors" diff --git a/Godeps/_workspace/src/golang.org/x/net/html/atom/atom.go b/Godeps/_workspace/src/golang.org/x/net/html/atom/atom.go index 227404bdafa4..cd0a8ac15451 100644 --- a/Godeps/_workspace/src/golang.org/x/net/html/atom/atom.go +++ b/Godeps/_workspace/src/golang.org/x/net/html/atom/atom.go @@ -15,7 +15,7 @@ // whether atom.H1 < atom.H2 may also change. The codes are not guaranteed to // be dense. The only guarantees are that e.g. looking up "div" will yield // atom.Div, calling atom.Div.String will return "div", and atom.Div != 0. -package atom +package atom // import "golang.org/x/net/html/atom" // Atom is an integer code for a string. The zero value maps to "". type Atom uint32 diff --git a/Godeps/_workspace/src/golang.org/x/net/html/charset/charset.go b/Godeps/_workspace/src/golang.org/x/net/html/charset/charset.go index b19f83b72760..2e5f9ba2c565 100644 --- a/Godeps/_workspace/src/golang.org/x/net/html/charset/charset.go +++ b/Godeps/_workspace/src/golang.org/x/net/html/charset/charset.go @@ -6,7 +6,7 @@ // // The mapping from encoding labels to encodings is defined at // http://encoding.spec.whatwg.org. -package charset +package charset // import "golang.org/x/net/html/charset" import ( "bytes" diff --git a/Godeps/_workspace/src/golang.org/x/net/html/doc.go b/Godeps/_workspace/src/golang.org/x/net/html/doc.go index fac0f54e78ad..32379a3246b2 100644 --- a/Godeps/_workspace/src/golang.org/x/net/html/doc.go +++ b/Godeps/_workspace/src/golang.org/x/net/html/doc.go @@ -93,7 +93,7 @@ The relevant specifications include: http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html and http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html */ -package html +package html // import "golang.org/x/net/html" // The tokenization algorithm implemented by this package is not a line-by-line // transliteration of the relatively verbose state-machine in the WHATWG diff --git a/Godeps/_workspace/src/golang.org/x/net/websocket/websocket.go b/Godeps/_workspace/src/golang.org/x/net/websocket/websocket.go index 0f4917bf7e62..b8d2e6d17442 100644 --- a/Godeps/_workspace/src/golang.org/x/net/websocket/websocket.go +++ b/Godeps/_workspace/src/golang.org/x/net/websocket/websocket.go @@ -4,7 +4,7 @@ // Package websocket implements a client and server for the WebSocket protocol // as specified in RFC 6455. -package websocket +package websocket // import "golang.org/x/net/websocket" import ( "bufio" diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/github/github.go b/Godeps/_workspace/src/golang.org/x/oauth2/github/github.go index 82ca623dd122..1648cb58daaf 100644 --- a/Godeps/_workspace/src/golang.org/x/oauth2/github/github.go +++ b/Godeps/_workspace/src/golang.org/x/oauth2/github/github.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package github provides constants for using OAuth2 to access Github. -package github +package github // import "golang.org/x/oauth2/github" import ( "golang.org/x/oauth2" diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/google/google.go b/Godeps/_workspace/src/golang.org/x/oauth2/google/google.go index 09206c7a8a1e..bbe6a386f4ee 100644 --- a/Godeps/_workspace/src/golang.org/x/oauth2/google/google.go +++ b/Godeps/_workspace/src/golang.org/x/oauth2/google/google.go @@ -11,7 +11,7 @@ // // For more information, please read // https://developers.google.com/accounts/docs/OAuth2. -package google +package google // import "golang.org/x/oauth2/google" import ( "encoding/json" diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go b/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go index 396b3fac827d..362323c4e745 100644 --- a/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go +++ b/Godeps/_workspace/src/golang.org/x/oauth2/jws/jws.go @@ -4,7 +4,7 @@ // Package jws provides encoding and decoding utilities for // signed JWS messages. -package jws +package jws // import "golang.org/x/oauth2/jws" import ( "bytes" diff --git a/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go b/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go index 53c755328cad..90f983bc3605 100644 --- a/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go +++ b/Godeps/_workspace/src/golang.org/x/oauth2/oauth2.go @@ -5,7 +5,7 @@ // Package oauth2 provides support for making // OAuth2 authorized and authenticated HTTP requests. // It can additionally grant authorization with Bearer JWT. -package oauth2 +package oauth2 // import "golang.org/x/oauth2" import ( "bytes" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/appengine.go b/Godeps/_workspace/src/google.golang.org/appengine/appengine.go index df263b59cff4..af12492929cb 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/appengine.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/appengine.go @@ -6,7 +6,7 @@ // // For more information on how to write Go apps for Google App Engine, see: // https://cloud.google.com/appengine/docs/go/ -package appengine +package appengine // import "google.golang.org/appengine" import ( "net/http" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/channel/channel.go b/Godeps/_workspace/src/google.golang.org/appengine/channel/channel.go index 6b7e77947b06..5782fffcfb40 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/channel/channel.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/channel/channel.go @@ -18,7 +18,7 @@ Send sends a message to the client over the channel identified by clientID. channel.Send(c, "player1", "Game over!") */ -package channel +package channel // import "google.golang.org/appengine/channel" import ( "encoding/json" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/datastore/doc.go b/Godeps/_workspace/src/google.golang.org/appengine/datastore/doc.go index d50fc2c36413..38164e936e6f 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/datastore/doc.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/datastore/doc.go @@ -313,4 +313,4 @@ Example code: fmt.Fprintf(w, "Count=%d", count) } */ -package datastore +package datastore // import "google.golang.org/appengine/datastore" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/delay/delay.go b/Godeps/_workspace/src/google.golang.org/appengine/delay/delay.go index 2b2e274b55f1..458be9d7aead 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/delay/delay.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/delay/delay.go @@ -41,7 +41,7 @@ reserved application path "/_ah/queue/go/delay". This path must not be marked as "login: required" in app.yaml; it must be marked as "login: admin" or have no access restriction. */ -package delay +package delay // import "google.golang.org/appengine/delay" import ( "bytes" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/image/image.go b/Godeps/_workspace/src/google.golang.org/appengine/image/image.go index 358fb0447404..e1558fe44f7f 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/image/image.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/image/image.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package image provides image services. -package image +package image // import "google.golang.org/appengine/image" import ( "fmt" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/log/log.go b/Godeps/_workspace/src/google.golang.org/appengine/log/log.go index 96f5bf80a7d9..b169cde6486f 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/log/log.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/log/log.go @@ -26,7 +26,7 @@ Example: c.Infof("Saw record %v", record) } */ -package log +package log // import "google.golang.org/appengine/log" import ( "errors" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/mail/mail.go b/Godeps/_workspace/src/google.golang.org/appengine/mail/mail.go index f9796ee064ee..5a26d595c0bd 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/mail/mail.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/mail/mail.go @@ -17,7 +17,7 @@ Example: c.Errorf("Alas, my user, the email failed to sendeth: %v", err) } */ -package mail +package mail // import "google.golang.org/appengine/mail" import ( "net/mail" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/memcache/memcache.go b/Godeps/_workspace/src/google.golang.org/appengine/memcache/memcache.go index 4324d35dff5a..5ed3dea9a1a5 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/memcache/memcache.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/memcache/memcache.go @@ -26,7 +26,7 @@ // if err := memcache.Set(c, item1); err != nil { // return err // } -package memcache +package memcache // import "google.golang.org/appengine/memcache" import ( "bytes" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/module/module.go b/Godeps/_workspace/src/google.golang.org/appengine/module/module.go index ecdc9ec42eec..d130b7dee8e6 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/module/module.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/module/module.go @@ -8,7 +8,7 @@ Package module provides functions for interacting with modules. The appengine package contains functions that report the identity of the app, including the module name. */ -package module +package module // import "google.golang.org/appengine/module" import ( "github.com/golang/protobuf/proto" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/remote_api/remote_api.go b/Godeps/_workspace/src/google.golang.org/appengine/remote_api/remote_api.go index 26d57d7a477b..68c13e8f8f7d 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/remote_api/remote_api.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/remote_api/remote_api.go @@ -6,7 +6,7 @@ Package remote_api implements the /_ah/remote_api endpoint. This endpoint is used by offline tools such as the bulk loader. */ -package remote_api +package remote_api // import "google.golang.org/appengine/remote_api" import ( "fmt" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/search/search.go b/Godeps/_workspace/src/google.golang.org/appengine/search/search.go index 2a53a967d375..2eefd41a58e9 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/search/search.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/search/search.go @@ -85,7 +85,7 @@ https://cloud.google.com/appengine/docs/go/search/query_strings Note that in Go, field names come from the struct field definition and begin with an upper case letter. */ -package search +package search // import "google.golang.org/appengine/search" // TODO: let Put specify the document language: "en", "fr", etc. Also: order_id?? storage?? // TODO: Index.GetAll (or Iterator.GetAll)? diff --git a/Godeps/_workspace/src/google.golang.org/appengine/taskqueue/taskqueue.go b/Godeps/_workspace/src/google.golang.org/appengine/taskqueue/taskqueue.go index 015c8af3bbfd..8068b0d48b04 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/taskqueue/taskqueue.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/taskqueue/taskqueue.go @@ -14,7 +14,7 @@ taskqueue operation is to add a single POST task, NewPOSTTask makes it easy. }) taskqueue.Add(c, t, "") // add t to the default queue */ -package taskqueue +package taskqueue // import "google.golang.org/appengine/taskqueue" import ( "errors" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/urlfetch/urlfetch.go b/Godeps/_workspace/src/google.golang.org/appengine/urlfetch/urlfetch.go index 2de61af9a744..a1cb12276055 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/urlfetch/urlfetch.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/urlfetch/urlfetch.go @@ -4,7 +4,7 @@ // Package urlfetch provides an http.RoundTripper implementation // for fetching URLs via App Engine's urlfetch service. -package urlfetch +package urlfetch // import "google.golang.org/appengine/urlfetch" import ( "errors" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/user/user.go b/Godeps/_workspace/src/google.golang.org/appengine/user/user.go index c53d00b705f6..16b0e7ec91be 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/user/user.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/user/user.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package user provides a client for App Engine's user authentication service. -package user +package user // import "google.golang.org/appengine/user" import ( "strings" diff --git a/Godeps/_workspace/src/google.golang.org/appengine/xmpp/xmpp.go b/Godeps/_workspace/src/google.golang.org/appengine/xmpp/xmpp.go index d005760b45cd..f32e77df8c54 100644 --- a/Godeps/_workspace/src/google.golang.org/appengine/xmpp/xmpp.go +++ b/Godeps/_workspace/src/google.golang.org/appengine/xmpp/xmpp.go @@ -22,7 +22,7 @@ To receive messages, // ... } */ -package xmpp +package xmpp // import "google.golang.org/appengine/xmpp" import ( "errors" diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 618f02bc60fb..3253f0b5bca3 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -82,6 +82,13 @@ import ( roleregistry "github.com/openshift/origin/pkg/authorization/registry/role" rolebindingregistry "github.com/openshift/origin/pkg/authorization/registry/rolebinding" subjectaccessreviewregistry "github.com/openshift/origin/pkg/authorization/registry/subjectaccessreview" + + // Admission control plugins + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" ) const (