vendor update for CSI 0.3.0

This commit is contained in:
gman
2018-07-18 16:47:22 +02:00
parent 6f484f92fc
commit 8ea659f0d5
6810 changed files with 438061 additions and 193861 deletions

View File

@ -24,7 +24,6 @@ go_library(
importpath = "k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//pkg/scheduler/algorithm:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",

View File

@ -24,7 +24,6 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper"
"k8s.io/kubernetes/pkg/scheduler/algorithm"
)
@ -39,6 +38,20 @@ var (
defaultUnreachableTolerationSeconds = flag.Int64("default-unreachable-toleration-seconds", 300,
"Indicates the tolerationSeconds of the toleration for unreachable:NoExecute"+
" that is added by default to every pod that does not already have such a toleration.")
notReadyToleration = api.Toleration{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: defaultNotReadyTolerationSeconds,
}
unreachableToleration = api.Toleration{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: defaultUnreachableTolerationSeconds,
}
)
// Register registers a plugin
@ -99,27 +112,13 @@ func (p *Plugin) Admit(attributes admission.Attributes) (err error) {
}
}
// no change is required, return immediately
if toleratesNodeNotReady && toleratesNodeUnreachable {
return nil
}
if !toleratesNodeNotReady {
helper.AddOrUpdateTolerationInPod(pod, &api.Toleration{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: defaultNotReadyTolerationSeconds,
})
pod.Spec.Tolerations = append(pod.Spec.Tolerations, notReadyToleration)
}
if !toleratesNodeUnreachable {
helper.AddOrUpdateTolerationInPod(pod, &api.Toleration{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: defaultUnreachableTolerationSeconds,
})
pod.Spec.Tolerations = append(pod.Spec.Tolerations, unreachableToleration)
}
return nil
}

View File

@ -45,8 +45,6 @@ go_library(
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation:go_default_library",
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",

View File

@ -12,9 +12,8 @@ go_library(
deps = [
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
],
)

View File

@ -19,25 +19,15 @@ limitations under the License.
package install
import (
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
internalapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
versionedapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1"
)
// Install registers the API group and adds types to a scheme
func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) {
if err := announced.NewGroupMetaFactory(
&announced.GroupMetaFactoryArgs{
GroupName: internalapi.GroupName,
VersionPreferenceOrder: []string{versionedapi.SchemeGroupVersion.Version},
AddInternalObjectsToScheme: internalapi.AddToScheme,
},
announced.VersionToSchemeFunc{
versionedapi.SchemeGroupVersion.Version: versionedapi.AddToScheme,
},
).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil {
panic(err)
}
func Install(scheme *runtime.Scheme) {
utilruntime.Must(internalapi.AddToScheme(scheme))
utilruntime.Must(versionedapi.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(versionedapi.SchemeGroupVersion))
}

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -20,10 +20,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
@ -32,14 +29,12 @@ import (
)
var (
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
func init() {
install.Install(groupFactoryRegistry, registry, scheme)
install.Install(scheme)
}
// LoadConfiguration loads the provided configuration.

View File

@ -30,6 +30,7 @@ go_test(
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta/testrestmapper:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",

View File

@ -95,21 +95,26 @@ func (a *gcPermissionsEnforcement) Validate(attributes admission.Attributes) (er
return nil
}
deleteAttributes := authorizer.AttributesRecord{
User: attributes.GetUserInfo(),
Verb: "delete",
Namespace: attributes.GetNamespace(),
APIGroup: attributes.GetResource().Group,
APIVersion: attributes.GetResource().Version,
Resource: attributes.GetResource().Resource,
Subresource: attributes.GetSubresource(),
Name: attributes.GetName(),
ResourceRequest: true,
Path: "",
}
decision, reason, err := a.authorizer.Authorize(deleteAttributes)
if decision != authorizer.DecisionAllow {
return admission.NewForbidden(attributes, fmt.Errorf("cannot set an ownerRef on a resource you can't delete: %v, %v", reason, err))
// if you are creating a thing, you should always be allowed to set an owner ref since you logically had the power
// to never create it. We still need to check block owner deletion below, because the power to delete does not
// imply the power to prevent deletion on other resources.
if attributes.GetOperation() != admission.Create {
deleteAttributes := authorizer.AttributesRecord{
User: attributes.GetUserInfo(),
Verb: "delete",
Namespace: attributes.GetNamespace(),
APIGroup: attributes.GetResource().Group,
APIVersion: attributes.GetResource().Version,
Resource: attributes.GetResource().Resource,
Subresource: attributes.GetSubresource(),
Name: attributes.GetName(),
ResourceRequest: true,
Path: "",
}
decision, reason, err := a.authorizer.Authorize(deleteAttributes)
if decision != authorizer.DecisionAllow {
return admission.NewForbidden(attributes, fmt.Errorf("cannot set an ownerRef on a resource you can't delete: %v, %v", reason, err))
}
}
// Further check if the user is setting ownerReference.blockOwnerDeletion to
@ -119,7 +124,7 @@ func (a *gcPermissionsEnforcement) Validate(attributes admission.Attributes) (er
for _, ref := range newBlockingRefs {
records, err := a.ownerRefToDeleteAttributeRecords(ref, attributes)
if err != nil {
return admission.NewForbidden(attributes, fmt.Errorf("cannot set blockOwnerDeletion in this case because cannot find RESTMapping for APIVersion %s Kind %s: %v, %v", ref.APIVersion, ref.Kind, reason, err))
return admission.NewForbidden(attributes, fmt.Errorf("cannot set blockOwnerDeletion in this case because cannot find RESTMapping for APIVersion %s Kind %s: %v", ref.APIVersion, ref.Kind, err))
}
// Multiple records are returned if ref.Kind could map to multiple
// resources. User needs to have delete permission on all the
@ -186,9 +191,9 @@ func (a *gcPermissionsEnforcement) ownerRefToDeleteAttributeRecords(ref metav1.O
Verb: "update",
// ownerReference can only refer to an object in the same namespace, so attributes.GetNamespace() equals to the owner's namespace
Namespace: attributes.GetNamespace(),
APIGroup: groupVersion.Group,
APIVersion: groupVersion.Version,
Resource: mapping.Resource,
APIGroup: mapping.Resource.Group,
APIVersion: mapping.Resource.Version,
Resource: mapping.Resource.Resource,
Subresource: "finalizers",
Name: ref.Name,
ResourceRequest: true,

View File

@ -20,6 +20,7 @@ import (
"strings"
"testing"
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -87,7 +88,7 @@ func newGCPermissionsEnforcement() (*gcPermissionsEnforcement, error) {
}
genericPluginInitializer := initializer.New(nil, nil, fakeAuthorizer{}, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(nil, nil, nil, legacyscheme.Registry.RESTMapper(), nil)
pluginInitializer := kubeadmission.NewPluginInitializer(nil, nil, nil, testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme), nil)
initializersChain := admission.PluginInitializers{}
initializersChain = append(initializersChain, genericPluginInitializer)
initializersChain = append(initializersChain, pluginInitializer)
@ -101,6 +102,9 @@ func TestGCAdmission(t *testing.T) {
return err == nil
}
expectCantSetOwnerRefError := func(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "cannot set an ownerRef on a resource you can't delete")
}
tests := []struct {
@ -139,7 +143,7 @@ func TestGCAdmission(t *testing.T) {
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectCantSetOwnerRefError,
checkError: expectNoError,
},
{
name: "non-pod-deleter, create, no objectref change",
@ -153,7 +157,7 @@ func TestGCAdmission(t *testing.T) {
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectCantSetOwnerRefError,
checkError: expectNoError,
},
{
name: "non-pod-deleter, create, objectref change, but not a pod",
@ -253,32 +257,34 @@ func TestGCAdmission(t *testing.T) {
checkError: expectNoError,
},
}
gcAdmit, err := newGCPermissionsEnforcement()
if err != nil {
t.Error(err)
}
for _, tc := range tests {
operation := admission.Create
if tc.oldObj != nil {
operation = admission.Update
}
user := &user.DefaultInfo{Name: tc.username}
attributes := admission.NewAttributesRecord(tc.newObj, tc.oldObj, schema.GroupVersionKind{}, metav1.NamespaceDefault, "foo", tc.resource, tc.subresource, operation, user)
t.Run(tc.name, func(t *testing.T) {
gcAdmit, err := newGCPermissionsEnforcement()
if err != nil {
t.Error(err)
}
err := gcAdmit.Validate(attributes)
if !tc.checkError(err) {
t.Errorf("%v: unexpected err: %v", tc.name, err)
}
operation := admission.Create
if tc.oldObj != nil {
operation = admission.Update
}
user := &user.DefaultInfo{Name: tc.username}
attributes := admission.NewAttributesRecord(tc.newObj, tc.oldObj, schema.GroupVersionKind{}, metav1.NamespaceDefault, "foo", tc.resource, tc.subresource, operation, user)
err = gcAdmit.Validate(attributes)
if !tc.checkError(err) {
t.Errorf("unexpected err: %v", err)
}
})
}
}
func TestBlockOwnerDeletionAdmission(t *testing.T) {
podWithOwnerRefs := func(refs ...metav1.OwnerReference) *api.Pod {
var refSlice []metav1.OwnerReference
for _, ref := range refs {
refSlice = append(refSlice, ref)
}
refSlice = append(refSlice, refs...)
return &api.Pod{
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: refSlice,

View File

@ -239,7 +239,7 @@ func NewImagePolicyWebhook(configFile io.Reader) (*Plugin, error) {
return nil, err
}
gw, err := webhook.NewGenericWebhook(legacyscheme.Registry, legacyscheme.Codecs, whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
gw, err := webhook.NewGenericWebhook(legacyscheme.Scheme, legacyscheme.Codecs, whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
if err != nil {
return nil, err
}

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright 2016 The Kubernetes Authors.
#

View File

@ -1,70 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"data_source.go",
"gcm.go",
"hawkular.go",
"influxdb.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/initialresources",
deps = [
"//pkg/apis/core:go_default_library",
"//vendor/cloud.google.com/go/compute/metadata:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/hawkular/hawkular-client-go/metrics:go_default_library",
"//vendor/github.com/influxdata/influxdb/client:go_default_library",
"//vendor/golang.org/x/oauth2:go_default_library",
"//vendor/golang.org/x/oauth2/google:go_default_library",
"//vendor/google.golang.org/api/cloudmonitoring/v2beta2:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"data_source_test.go",
"gcm_test.go",
"hawkular_test.go",
"influxdb_test.go",
],
embed = [":go_default_library"],
deps = [
"//pkg/apis/core:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/golang.org/x/oauth2:go_default_library",
"//vendor/golang.org/x/oauth2/google:go_default_library",
"//vendor/google.golang.org/api/cloudmonitoring/v2beta2:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -1,220 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
"flag"
"fmt"
"io"
"sort"
"strings"
"time"
"github.com/golang/glog"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
var (
source = flag.String("ir-data-source", "influxdb", "Data source used by InitialResources. Supported options: influxdb, gcm.")
percentile = flag.Int64("ir-percentile", 90, "Which percentile of samples should InitialResources use when estimating resources. For experiment purposes.")
nsOnly = flag.Bool("ir-namespace-only", false, "Whether the estimation should be made only based on data from the same namespace.")
)
const (
initialResourcesAnnotation = "kubernetes.io/initial-resources"
samplesThreshold = 30
week = 7 * 24 * time.Hour
month = 30 * 24 * time.Hour
// PluginName indicates name of admission plugin.
PluginName = "InitialResources"
)
// Register registers a plugin
// WARNING: this feature is experimental and will definitely change.
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
// TODO: remove the usage of flags in favor of reading versioned configuration
s, err := newDataSource(*source)
if err != nil {
return nil, err
}
return newInitialResources(s, *percentile, *nsOnly), nil
})
}
type InitialResources struct {
*admission.Handler
source dataSource
percentile int64
nsOnly bool
}
var _ admission.MutationInterface = &InitialResources{}
func newInitialResources(source dataSource, percentile int64, nsOnly bool) *InitialResources {
return &InitialResources{
Handler: admission.NewHandler(admission.Create),
source: source,
percentile: percentile,
nsOnly: nsOnly,
}
}
// Admit makes an admission decision based on the request attributes
func (ir InitialResources) Admit(a admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
if a.GetSubresource() != "" || a.GetResource().GroupResource() != api.Resource("pods") {
return nil
}
pod, ok := a.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
ir.estimateAndFillResourcesIfNotSet(pod)
return nil
}
// The method veryfies whether resources should be set for the given pod and
// if there is estimation available the method fills Request field.
func (ir InitialResources) estimateAndFillResourcesIfNotSet(pod *api.Pod) {
var annotations []string
for i := range pod.Spec.InitContainers {
annotations = append(annotations, ir.estimateContainer(pod, &pod.Spec.InitContainers[i], "init container")...)
}
for i := range pod.Spec.Containers {
annotations = append(annotations, ir.estimateContainer(pod, &pod.Spec.Containers[i], "container")...)
}
if len(annotations) > 0 {
if pod.ObjectMeta.Annotations == nil {
pod.ObjectMeta.Annotations = make(map[string]string)
}
val := "Initial Resources plugin set: " + strings.Join(annotations, "; ")
pod.ObjectMeta.Annotations[initialResourcesAnnotation] = val
}
}
func (ir InitialResources) estimateContainer(pod *api.Pod, c *api.Container, message string) []string {
var annotations []string
req := c.Resources.Requests
cpu := ir.getEstimationIfNeeded(api.ResourceCPU, c, pod.ObjectMeta.Namespace)
mem := ir.getEstimationIfNeeded(api.ResourceMemory, c, pod.ObjectMeta.Namespace)
// If Requests doesn't exits and an estimation was made, create Requests.
if req == nil && (cpu != nil || mem != nil) {
c.Resources.Requests = api.ResourceList{}
req = c.Resources.Requests
}
setRes := []string{}
if cpu != nil {
glog.Infof("CPU estimation for %s %v in pod %v/%v is %v", message, c.Name, pod.ObjectMeta.Namespace, pod.ObjectMeta.Name, cpu.String())
setRes = append(setRes, string(api.ResourceCPU))
req[api.ResourceCPU] = *cpu
}
if mem != nil {
glog.Infof("Memory estimation for %s %v in pod %v/%v is %v", message, c.Name, pod.ObjectMeta.Namespace, pod.ObjectMeta.Name, mem.String())
setRes = append(setRes, string(api.ResourceMemory))
req[api.ResourceMemory] = *mem
}
if len(setRes) > 0 {
sort.Strings(setRes)
a := strings.Join(setRes, ", ") + fmt.Sprintf(" request for %s %s", message, c.Name)
annotations = append(annotations, a)
}
return annotations
}
// getEstimationIfNeeded estimates compute resource for container if its corresponding
// Request(min amount) and Limit(max amount) both are not specified.
func (ir InitialResources) getEstimationIfNeeded(kind api.ResourceName, c *api.Container, ns string) *resource.Quantity {
requests := c.Resources.Requests
limits := c.Resources.Limits
var quantity *resource.Quantity
var err error
if _, requestFound := requests[kind]; !requestFound {
if _, limitFound := limits[kind]; !limitFound {
quantity, err = ir.getEstimation(kind, c, ns)
if err != nil {
glog.Errorf("Error while trying to estimate resources: %v", err)
}
}
}
return quantity
}
func (ir InitialResources) getEstimation(kind api.ResourceName, c *api.Container, ns string) (*resource.Quantity, error) {
end := time.Now()
start := end.Add(-week)
var usage, samples int64
var err error
// Historical data from last 7 days for the same image:tag within the same namespace.
if usage, samples, err = ir.source.GetUsagePercentile(kind, ir.percentile, c.Image, ns, true, start, end); err != nil {
return nil, err
}
if samples < samplesThreshold {
// Historical data from last 30 days for the same image:tag within the same namespace.
start := end.Add(-month)
if usage, samples, err = ir.source.GetUsagePercentile(kind, ir.percentile, c.Image, ns, true, start, end); err != nil {
return nil, err
}
}
// If we are allowed to estimate only based on data from the same namespace.
if ir.nsOnly {
if samples < samplesThreshold {
// Historical data from last 30 days for the same image within the same namespace.
start := end.Add(-month)
image := strings.Split(c.Image, ":")[0]
if usage, samples, err = ir.source.GetUsagePercentile(kind, ir.percentile, image, ns, false, start, end); err != nil {
return nil, err
}
}
} else {
if samples < samplesThreshold {
// Historical data from last 7 days for the same image:tag within all namespaces.
start := end.Add(-week)
if usage, samples, err = ir.source.GetUsagePercentile(kind, ir.percentile, c.Image, "", true, start, end); err != nil {
return nil, err
}
}
if samples < samplesThreshold {
// Historical data from last 30 days for the same image:tag within all namespaces.
start := end.Add(-month)
if usage, samples, err = ir.source.GetUsagePercentile(kind, ir.percentile, c.Image, "", true, start, end); err != nil {
return nil, err
}
}
if samples < samplesThreshold {
// Historical data from last 30 days for the same image within all namespaces.
start := end.Add(-month)
image := strings.Split(c.Image, ":")[0]
if usage, samples, err = ir.source.GetUsagePercentile(kind, ir.percentile, image, "", false, start, end); err != nil {
return nil, err
}
}
}
if samples > 0 && kind == api.ResourceCPU {
return resource.NewMilliQuantity(usage, resource.DecimalSI), nil
}
if samples > 0 && kind == api.ResourceMemory {
return resource.NewQuantity(usage, resource.DecimalSI), nil
}
return nil, nil
}

View File

@ -1,300 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
"errors"
"testing"
"time"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
type fakeSource struct {
f func(kind api.ResourceName, perc int64, image, namespace string, exactMatch bool, start, end time.Time) (int64, int64, error)
}
func (s *fakeSource) GetUsagePercentile(kind api.ResourceName, perc int64, image, namespace string, exactMatch bool, start, end time.Time) (usage int64, samples int64, err error) {
return s.f(kind, perc, image, namespace, exactMatch, start, end)
}
func parseReq(cpu, mem string) api.ResourceList {
if cpu == "" && mem == "" {
return nil
}
req := api.ResourceList{}
if cpu != "" {
req[api.ResourceCPU] = resource.MustParse(cpu)
}
if mem != "" {
req[api.ResourceMemory] = resource.MustParse(mem)
}
return req
}
func addContainer(pod *api.Pod, name, image string, request api.ResourceList) {
pod.Spec.Containers = append(pod.Spec.Containers, api.Container{
Name: name,
Image: image,
Resources: api.ResourceRequirements{Requests: request},
})
}
func createPod(name string, image string, request api.ResourceList) *api.Pod {
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test-ns"},
Spec: api.PodSpec{},
}
pod.Spec.Containers = []api.Container{}
addContainer(pod, "i0", image, request)
pod.Spec.InitContainers = pod.Spec.Containers
pod.Spec.Containers = []api.Container{}
addContainer(pod, "c0", image, request)
return pod
}
func getPods() []*api.Pod {
return []*api.Pod{
createPod("p0", "image:v0", parseReq("", "")),
createPod("p1", "image:v1", parseReq("", "300")),
createPod("p2", "image:v2", parseReq("300m", "")),
createPod("p3", "image:v3", parseReq("300m", "300")),
}
}
func verifyContainer(t *testing.T, c *api.Container, cpu, mem int64) {
req := c.Resources.Requests
if req.Cpu().MilliValue() != cpu {
t.Errorf("Wrong CPU request for container %v. Expected %v, got %v.", c.Name, cpu, req.Cpu().MilliValue())
}
if req.Memory().Value() != mem {
t.Errorf("Wrong memory request for container %v. Expected %v, got %v.", c.Name, mem, req.Memory().Value())
}
}
func verifyPod(t *testing.T, pod *api.Pod, cpu, mem int64) {
verifyContainer(t, &pod.Spec.Containers[0], cpu, mem)
verifyContainer(t, &pod.Spec.InitContainers[0], cpu, mem)
}
func verifyAnnotation(t *testing.T, pod *api.Pod, expected string) {
a, ok := pod.ObjectMeta.Annotations[initialResourcesAnnotation]
if !ok {
t.Errorf("No annotation but expected %v", expected)
}
if a != expected {
t.Errorf("Wrong annotation set by Initial Resources: got %v, expected %v", a, expected)
}
}
func expectNoAnnotation(t *testing.T, pod *api.Pod) {
if a, ok := pod.ObjectMeta.Annotations[initialResourcesAnnotation]; ok {
t.Errorf("Expected no annotation but got %v", a)
}
}
func admit(t *testing.T, ir admission.MutationInterface, pods []*api.Pod) {
for i := range pods {
p := pods[i]
podKind := api.Kind("Pod").WithVersion("version")
podRes := api.Resource("pods").WithVersion("version")
attrs := admission.NewAttributesRecord(p, nil, podKind, "test", p.ObjectMeta.Name, podRes, "", admission.Create, nil)
if err := ir.Admit(attrs); err != nil {
t.Error(err)
}
}
}
func testAdminScenarios(t *testing.T, ir admission.MutationInterface, p *api.Pod) {
podKind := api.Kind("Pod").WithVersion("version")
podRes := api.Resource("pods").WithVersion("version")
var tests = []struct {
attrs admission.Attributes
expectError bool
}{
{
admission.NewAttributesRecord(p, nil, podKind, "test", p.ObjectMeta.Name, podRes, "foo", admission.Create, nil),
false,
},
{
admission.NewAttributesRecord(&api.ReplicationController{}, nil, podKind, "test", "", podRes, "", admission.Create, nil),
true,
},
}
for _, test := range tests {
err := ir.Admit(test.attrs)
if err != nil && test.expectError == false {
t.Error(err)
} else if err == nil && test.expectError == true {
t.Error("Error expected for Admit but received none")
}
}
}
func performTest(t *testing.T, ir admission.MutationInterface) {
pods := getPods()
admit(t, ir, pods)
testAdminScenarios(t, ir, pods[0])
verifyPod(t, pods[0], 100, 100)
verifyPod(t, pods[1], 100, 300)
verifyPod(t, pods[2], 300, 100)
verifyPod(t, pods[3], 300, 300)
verifyAnnotation(t, pods[0], "Initial Resources plugin set: cpu, memory request for init container i0; cpu, memory request for container c0")
verifyAnnotation(t, pods[1], "Initial Resources plugin set: cpu request for init container i0")
verifyAnnotation(t, pods[2], "Initial Resources plugin set: memory request for init container i0")
expectNoAnnotation(t, pods[3])
}
func TestEstimateReturnsErrorFromSource(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, start, end time.Time) (int64, int64, error) {
return 0, 0, errors.New("Example error")
}
ir := newInitialResources(&fakeSource{f: f}, 90, false)
admit(t, ir, getPods())
}
func TestEstimationBasedOnTheSameImageSameNamespace7d(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, start, end time.Time) (int64, int64, error) {
if exactMatch && end.Sub(start) == week && ns == "test-ns" {
return 100, 120, nil
}
return 200, 120, nil
}
performTest(t, newInitialResources(&fakeSource{f: f}, 90, false))
}
func TestEstimationBasedOnTheSameImageSameNamespace30d(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, start, end time.Time) (int64, int64, error) {
if exactMatch && end.Sub(start) == week && ns == "test-ns" {
return 200, 20, nil
}
if exactMatch && end.Sub(start) == month && ns == "test-ns" {
return 100, 120, nil
}
return 200, 120, nil
}
performTest(t, newInitialResources(&fakeSource{f: f}, 90, false))
}
func TestEstimationBasedOnTheSameImageAllNamespaces7d(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, start, end time.Time) (int64, int64, error) {
if exactMatch && ns == "test-ns" {
return 200, 20, nil
}
if exactMatch && end.Sub(start) == week && ns == "" {
return 100, 120, nil
}
return 200, 120, nil
}
performTest(t, newInitialResources(&fakeSource{f: f}, 90, false))
}
func TestEstimationBasedOnTheSameImageAllNamespaces30d(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, start, end time.Time) (int64, int64, error) {
if exactMatch && ns == "test-ns" {
return 200, 20, nil
}
if exactMatch && end.Sub(start) == week && ns == "" {
return 200, 20, nil
}
if exactMatch && end.Sub(start) == month && ns == "" {
return 100, 120, nil
}
return 200, 120, nil
}
performTest(t, newInitialResources(&fakeSource{f: f}, 90, false))
}
func TestEstimationBasedOnOtherImages(t *testing.T) {
f := func(_ api.ResourceName, _ int64, image, ns string, exactMatch bool, _, _ time.Time) (int64, int64, error) {
if image == "image" && !exactMatch && ns == "" {
return 100, 5, nil
}
return 200, 20, nil
}
performTest(t, newInitialResources(&fakeSource{f: f}, 90, false))
}
func TestNoData(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, _ bool, _, _ time.Time) (int64, int64, error) {
return 200, 0, nil
}
ir := newInitialResources(&fakeSource{f: f}, 90, false)
pods := []*api.Pod{
createPod("p0", "image:v0", parseReq("", "")),
}
admit(t, ir, pods)
if pods[0].Spec.Containers[0].Resources.Requests != nil {
t.Errorf("Unexpected resource estimation")
}
expectNoAnnotation(t, pods[0])
}
func TestManyContainers(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, _, _ time.Time) (int64, int64, error) {
if exactMatch {
return 100, 120, nil
}
return 200, 30, nil
}
ir := newInitialResources(&fakeSource{f: f}, 90, false)
pod := createPod("p", "image:v0", parseReq("", ""))
addContainer(pod, "c1", "image:v1", parseReq("", "300"))
addContainer(pod, "c2", "image:v2", parseReq("300m", ""))
addContainer(pod, "c3", "image:v3", parseReq("300m", "300"))
admit(t, ir, []*api.Pod{pod})
verifyContainer(t, &pod.Spec.Containers[0], 100, 100)
verifyContainer(t, &pod.Spec.Containers[1], 100, 300)
verifyContainer(t, &pod.Spec.Containers[2], 300, 100)
verifyContainer(t, &pod.Spec.Containers[3], 300, 300)
verifyAnnotation(t, pod, "Initial Resources plugin set: cpu, memory request for init container i0; cpu, memory request for container c0; cpu request for container c1; memory request for container c2")
}
func TestNamespaceAware(t *testing.T) {
f := func(_ api.ResourceName, _ int64, _, ns string, exactMatch bool, start, end time.Time) (int64, int64, error) {
if ns == "test-ns" {
return 200, 0, nil
}
return 200, 120, nil
}
ir := newInitialResources(&fakeSource{f: f}, 90, true)
pods := []*api.Pod{
createPod("p0", "image:v0", parseReq("", "")),
}
admit(t, ir, pods)
if pods[0].Spec.Containers[0].Resources.Requests != nil {
t.Errorf("Unexpected resource estimation")
}
expectNoAnnotation(t, pods[0])
}

View File

@ -1,56 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
"flag"
"fmt"
api "k8s.io/kubernetes/pkg/apis/core"
"time"
)
var (
influxdbHost = flag.String("ir-influxdb-host", "localhost:8080/api/v1/namespaces/kube-system/services/monitoring-influxdb:api/proxy", "Address of InfluxDB which contains metrics required by InitialResources")
user = flag.String("ir-user", "root", "User used for connecting to InfluxDB")
// TODO: figure out how to better pass password here
password = flag.String("ir-password", "root", "Password used for connecting to InfluxDB")
db = flag.String("ir-dbname", "k8s", "InfluxDB database name which contains metrics required by InitialResources")
hawkularConfig = flag.String("ir-hawkular", "", "Hawkular configuration URL")
)
// WARNING: If you are planning to add another implementation of dataSource interface please bear in mind,
// that dataSource will be moved to Heapster some time in the future and possibly rewritten.
type dataSource interface {
// Returns <perc>th of sample values which represent usage of <kind> for containers running <image>,
// within time range (start, end), number of samples considered and error if occurred.
// If <exactMatch> then take only samples that concern the same image (both name and take are the same),
// otherwise consider also samples with the same image a possibly different tag.
GetUsagePercentile(kind api.ResourceName, perc int64, image, namespace string, exactMatch bool, start, end time.Time) (usage int64, samples int64, err error)
}
func newDataSource(kind string) (dataSource, error) {
if kind == "influxdb" {
return newInfluxdbSource(*influxdbHost, *user, *password, *db)
}
if kind == "gcm" {
return newGcmSource()
}
if kind == "hawkular" {
return newHawkularSource(*hawkularConfig)
}
return nil, fmt.Errorf("unknown data source %v", kind)
}

View File

@ -1,45 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 initialresources
import "testing"
func TestInfluxDBDataSource(t *testing.T) {
ds, _ := newDataSource("influxdb")
if _, ok := ds.(*influxdbSource); !ok {
t.Errorf("newDataSource did not return valid InfluxDB type")
}
}
func TestGCMDataSource(t *testing.T) {
// No ProjectID set
newDataSource("gcm")
}
func TestHawkularDataSource(t *testing.T) {
ds, _ := newDataSource("hawkular")
if _, ok := ds.(*hawkularSource); !ok {
t.Errorf("newDataSource did not return valid hawkularSource type")
}
}
func TestNoDataSourceFound(t *testing.T) {
ds, err := newDataSource("")
if ds != nil || err == nil {
t.Errorf("newDataSource found for empty input")
}
}

View File

@ -1,132 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
api "k8s.io/kubernetes/pkg/apis/core"
"math"
"sort"
"time"
gce "cloud.google.com/go/compute/metadata"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
gcm "google.golang.org/api/cloudmonitoring/v2beta2"
)
const (
kubePrefix = "custom.cloudmonitoring.googleapis.com/kubernetes.io/"
cpuMetricName = kubePrefix + "cpu/usage_rate"
memMetricName = kubePrefix + "memory/usage"
labelImage = kubePrefix + "label/container_base_image"
labelNs = kubePrefix + "label/pod_namespace"
)
type gcmSource struct {
project string
gcmService *gcm.Service
}
func newGcmSource() (dataSource, error) {
// Detect project ID
projectId, err := gce.ProjectID()
if err != nil {
return nil, err
}
// Create Google Cloud Monitoring service.
client := oauth2.NewClient(oauth2.NoContext, google.ComputeTokenSource(""))
s, err := gcm.New(client)
if err != nil {
return nil, err
}
return &gcmSource{
project: projectId,
gcmService: s,
}, nil
}
func (s *gcmSource) query(metric, oldest, youngest string, labels []string, pageToken string) (*gcm.ListTimeseriesResponse, error) {
req := s.gcmService.Timeseries.List(s.project, metric, youngest, nil).
Oldest(oldest).
Aggregator("mean").
Window("1m")
for _, l := range labels {
req = req.Labels(l)
}
if pageToken != "" {
req = req.PageToken(pageToken)
}
return req.Do()
}
func retrieveRawSamples(res *gcm.ListTimeseriesResponse, output *[]int) {
for _, ts := range res.Timeseries {
for _, p := range ts.Points {
*output = append(*output, int(*p.DoubleValue))
}
}
}
func (s *gcmSource) GetUsagePercentile(kind api.ResourceName, perc int64, image, namespace string, exactMatch bool, start, end time.Time) (int64, int64, error) {
var metric string
if kind == api.ResourceCPU {
metric = cpuMetricName
} else if kind == api.ResourceMemory {
metric = memMetricName
}
var labels []string
if exactMatch {
labels = append(labels, labelImage+"=="+image)
} else {
labels = append(labels, labelImage+"=~"+image+".*")
}
if namespace != "" {
labels = append(labels, labelNs+"=="+namespace)
}
oldest := start.Format(time.RFC3339)
youngest := end.Format(time.RFC3339)
rawSamples := make([]int, 0)
pageToken := ""
for {
res, err := s.query(metric, oldest, youngest, labels, pageToken)
if err != nil {
return 0, 0, err
}
retrieveRawSamples(res, &rawSamples)
pageToken = res.NextPageToken
if pageToken == "" {
break
}
}
count := len(rawSamples)
if count == 0 {
return 0, 0, nil
}
sort.Ints(rawSamples)
usageIndex := int64(math.Ceil(float64(count)*9/10)) - 1
usage := rawSamples[usageIndex]
return int64(usage), int64(count), nil
}

View File

@ -1,46 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 initialresources
import (
"testing"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
gcm "google.golang.org/api/cloudmonitoring/v2beta2"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestGCMReturnsErrorIfClientCannotConnect(t *testing.T) {
client := oauth2.NewClient(oauth2.NoContext, google.ComputeTokenSource(""))
service, _ := gcm.New(client)
source := &gcmSource{
project: "",
gcmService: service,
}
_, _, err := source.GetUsagePercentile(api.ResourceCPU, 90, "", "", true, time.Now(), time.Now())
if err == nil {
t.Errorf("Expected error from GCM")
}
_, _, err = source.GetUsagePercentile(api.ResourceMemory, 90, "", "foo", false, time.Now(), time.Now())
if err == nil {
t.Errorf("Expected error from GCM")
}
}

View File

@ -1,223 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/golang/glog"
"github.com/hawkular/hawkular-client-go/metrics"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
api "k8s.io/kubernetes/pkg/apis/core"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
type hawkularSource struct {
client *metrics.Client
uri *url.URL
useNamespace bool
modifiers []metrics.Modifier
}
const (
containerImageTag string = "container_base_image"
descriptorTag string = "descriptor_name"
separator string = "/"
defaultServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
)
// heapsterName gets the equivalent MetricDescriptor.Name used in the Heapster
func heapsterName(kind api.ResourceName) string {
switch kind {
case api.ResourceCPU:
return "cpu/usage"
case api.ResourceMemory:
return "memory/usage"
default:
return ""
}
}
// tagQuery creates tagFilter query for Hawkular
func tagQuery(kind api.ResourceName, image string, exactMatch bool) map[string]string {
q := make(map[string]string)
// Add here the descriptor_tag..
q[descriptorTag] = heapsterName(kind)
if exactMatch {
q[containerImageTag] = image
} else {
split := strings.Index(image, "@")
if split < 0 {
split = strings.Index(image, ":")
}
q[containerImageTag] = fmt.Sprintf("%s:*", image[:split])
}
return q
}
// dataSource API
func (hs *hawkularSource) GetUsagePercentile(kind api.ResourceName, perc int64, image, namespace string, exactMatch bool, start, end time.Time) (int64, int64, error) {
q := tagQuery(kind, image, exactMatch)
m := make([]metrics.Modifier, len(hs.modifiers), 2+len(hs.modifiers))
copy(m, hs.modifiers)
if namespace != metav1.NamespaceAll {
m = append(m, metrics.Tenant(namespace))
}
p := float64(perc)
m = append(m, metrics.Filters(metrics.TagsFilter(q), metrics.BucketsFilter(1), metrics.StartTimeFilter(start), metrics.EndTimeFilter(end), metrics.PercentilesFilter([]float64{p})))
bp, err := hs.client.ReadBuckets(metrics.Counter, m...)
if err != nil {
return 0, 0, err
}
if len(bp) > 0 && len(bp[0].Percentiles) > 0 {
return int64(bp[0].Percentiles[0].Value), int64(bp[0].Samples), nil
}
return 0, 0, nil
}
// newHawkularSource creates a new Hawkular Source. The uri follows the scheme from Heapster
func newHawkularSource(uri string) (dataSource, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
d := &hawkularSource{
uri: u,
}
if err = d.init(); err != nil {
return nil, err
}
return d, nil
}
// init initializes the Hawkular dataSource. Almost equal to the Heapster initialization
func (hs *hawkularSource) init() error {
hs.modifiers = make([]metrics.Modifier, 0)
p := metrics.Parameters{
Tenant: "heapster", // This data is stored by the heapster - for no-namespace hits
Url: hs.uri.String(),
}
opts := hs.uri.Query()
if v, found := opts["tenant"]; found {
p.Tenant = v[0]
}
if v, found := opts["useServiceAccount"]; found {
if b, _ := strconv.ParseBool(v[0]); b {
accountFile := defaultServiceAccountFile
if file, f := opts["serviceAccountFile"]; f {
accountFile = file[0]
}
// If a readable service account token exists, then use it
if contents, err := ioutil.ReadFile(accountFile); err == nil {
p.Token = string(contents)
} else {
glog.Errorf("Could not read contents of %s, no token authentication is used\n", defaultServiceAccountFile)
}
}
}
// Authentication / Authorization parameters
tC := &tls.Config{}
if v, found := opts["auth"]; found {
if _, f := opts["caCert"]; f {
return fmt.Errorf("both auth and caCert files provided, combination is not supported")
}
if len(v[0]) > 0 {
// Authfile
kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&clientcmd.ClientConfigLoadingRules{
ExplicitPath: v[0]},
&clientcmd.ConfigOverrides{}).ClientConfig()
if err != nil {
return err
}
tC, err = restclient.TLSConfigFor(kubeConfig)
if err != nil {
return err
}
}
}
if u, found := opts["user"]; found {
if _, wrong := opts["useServiceAccount"]; wrong {
return fmt.Errorf("if user and password are used, serviceAccount cannot be used")
}
if p, f := opts["pass"]; f {
hs.modifiers = append(hs.modifiers, func(req *http.Request) error {
req.SetBasicAuth(u[0], p[0])
return nil
})
}
}
if v, found := opts["caCert"]; found {
caCert, err := ioutil.ReadFile(v[0])
if err != nil {
return err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tC.RootCAs = caCertPool
}
if v, found := opts["insecure"]; found {
insecure, err := strconv.ParseBool(v[0])
if err != nil {
return err
}
tC.InsecureSkipVerify = insecure
}
p.TLSConfig = tC
c, err := metrics.NewHawkularClient(p)
if err != nil {
return err
}
hs.client = c
glog.Infof("Initialised Hawkular Source with parameters %v", p)
return nil
}

View File

@ -1,142 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
"fmt"
api "k8s.io/kubernetes/pkg/apis/core"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
assert "github.com/stretchr/testify/require"
)
const (
testImageName string = "hawkular/hawkular-metrics"
testImageVersion string = "latest"
testImageSHA string = "b727ece3780cdd30e9a86226e520f26bcc396071ed7a86b7ef6684bb93a9f717"
testPartialMatch string = "hawkular/hawkular-metrics:*"
)
func testImageWithVersion() string {
return fmt.Sprintf("%s:%s", testImageName, testImageVersion)
}
func testImageWithReference() string {
return fmt.Sprintf("%s@sha256:%s", testImageName, testImageSHA)
}
func TestTaqQuery(t *testing.T) {
kind := api.ResourceCPU
tQ := tagQuery(kind, testImageWithVersion(), false)
assert.Equal(t, 2, len(tQ))
assert.Equal(t, testPartialMatch, tQ[containerImageTag])
assert.Equal(t, "cpu/usage", tQ[descriptorTag])
tQe := tagQuery(kind, testImageWithVersion(), true)
assert.Equal(t, 2, len(tQe))
assert.Equal(t, testImageWithVersion(), tQe[containerImageTag])
assert.Equal(t, "cpu/usage", tQe[descriptorTag])
tQr := tagQuery(kind, testImageWithReference(), false)
assert.Equal(t, 2, len(tQe))
assert.Equal(t, testPartialMatch, tQr[containerImageTag])
assert.Equal(t, "cpu/usage", tQr[descriptorTag])
tQre := tagQuery(kind, testImageWithReference(), true)
assert.Equal(t, 2, len(tQe))
assert.Equal(t, testImageWithReference(), tQre[containerImageTag])
assert.Equal(t, "cpu/usage", tQre[descriptorTag])
kind = api.ResourceMemory
tQ = tagQuery(kind, testImageWithReference(), true)
assert.Equal(t, "memory/usage", tQ[descriptorTag])
kind = api.ResourceStorage
tQ = tagQuery(kind, testImageWithReference(), true)
assert.Equal(t, "", tQ[descriptorTag])
}
func newSource(t *testing.T) (map[string]string, dataSource) {
tenant := "16a8884e4c155457ee38a8901df6b536"
reqs := make(map[string]string)
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, tenant, r.Header.Get("Hawkular-Tenant"))
assert.Equal(t, "Basic", r.Header.Get("Authorization")[:5])
if strings.Contains(r.RequestURI, "counters/data") {
assert.True(t, strings.Contains(r.RequestURI, url.QueryEscape(testImageWithVersion())))
assert.True(t, strings.Contains(r.RequestURI, "cpu%2Fusage"))
assert.True(t, strings.Contains(r.RequestURI, "percentiles=90"))
reqs["counters/data"] = r.RequestURI
fmt.Fprintf(w, ` [{"start":1444620095882,"end":1444648895882,"min":1.45,"avg":1.45,"median":1.45,"max":1.45,"percentile95th":1.45,"samples":123456,"percentiles":[{"value":7896.54,"quantile":0.9},{"value":1.45,"quantile":0.99}],"empty":false}]`)
} else {
reqs["unknown"] = r.RequestURI
}
}))
paramUri := fmt.Sprintf("%s?user=test&pass=yep&tenant=foo&insecure=true", s.URL)
hSource, err := newHawkularSource(paramUri)
assert.NoError(t, err)
return reqs, hSource
}
func TestInsecureMustBeBool(t *testing.T) {
paramUri := fmt.Sprintf("localhost?user=test&pass=yep&insecure=foo")
_, err := newHawkularSource(paramUri)
if err == nil {
t.Errorf("Expected error from newHawkularSource")
}
}
func TestCAFileMustExist(t *testing.T) {
paramUri := fmt.Sprintf("localhost?user=test&pass=yep&caCert=foo")
_, err := newHawkularSource(paramUri)
if err == nil {
t.Errorf("Expected error from newHawkularSource")
}
}
func TestServiceAccountIsMutuallyExclusiveWithAuth(t *testing.T) {
paramUri := fmt.Sprintf("localhost?user=test&pass=yep&useServiceAccount=true")
_, err := newHawkularSource(paramUri)
if err == nil {
t.Errorf("Expected error from newHawkularSource")
}
}
func TestGetUsagePercentile(t *testing.T) {
reqs, hSource := newSource(t)
usage, samples, err := hSource.GetUsagePercentile(api.ResourceCPU, 90, testImageWithVersion(), "16a8884e4c155457ee38a8901df6b536", true, time.Now(), time.Now())
assert.NoError(t, err)
assert.Equal(t, 1, len(reqs))
assert.Equal(t, "", reqs["unknown"])
assert.Equal(t, int64(123456), int64(samples))
assert.Equal(t, int64(7896), usage) // float64 -> int64
}

View File

@ -1,73 +0,0 @@
/*
Copyright 2015 The Kubernetes Authors.
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 initialresources
import (
"fmt"
"strings"
"time"
influxdb "github.com/influxdata/influxdb/client"
api "k8s.io/kubernetes/pkg/apis/core"
)
const (
cpuSeriesName = "autoscaling.cpu.usage.2m"
memSeriesName = "autoscaling.memory.usage.2m"
cpuContinuousQuery = "select derivative(value) as value from \"cpu/usage_ns_cumulative\" where pod_id <> '' group by pod_id, pod_namespace, container_name, container_base_image, time(2m) into " + cpuSeriesName
memContinuousQuery = "select mean(value) as value from \"memory/usage_bytes_gauge\" where pod_id <> '' group by pod_id, pod_namespace, container_name, container_base_image, time(2m) into " + memSeriesName
timeFormat = "2006-01-02 15:04:05"
)
// TODO(piosz): rewrite this once we will migrate into InfluxDB v0.9.
type influxdbSource struct{}
func newInfluxdbSource(host, user, password, db string) (dataSource, error) {
return &influxdbSource{}, nil
}
func (s *influxdbSource) query(query string) ([]*influxdb.Response, error) {
// TODO(piosz): add support again
return nil, fmt.Errorf("temporary not supported; see #18826 for more details")
}
func (s *influxdbSource) GetUsagePercentile(kind api.ResourceName, perc int64, image, namespace string, exactMatch bool, start, end time.Time) (int64, int64, error) {
var series string
if kind == api.ResourceCPU {
series = cpuSeriesName
} else if kind == api.ResourceMemory {
series = memSeriesName
}
var imgPattern string
if exactMatch {
imgPattern = "='" + image + "'"
} else {
// Escape character "/" in image pattern.
imgPattern = "=~/^" + strings.Replace(image, "/", "\\/", -1) + "/"
}
var namespaceCond string
if namespace != "" {
namespaceCond = " and pod_namespace='" + namespace + "'"
}
query := fmt.Sprintf("select percentile(value, %v), count(pod_id) from %v where container_base_image%v%v and time > '%v' and time < '%v'", perc, series, imgPattern, namespaceCond, start.UTC().Format(timeFormat), end.UTC().Format(timeFormat))
if _, err := s.query(query); err != nil {
return 0, 0, fmt.Errorf("error while trying to query InfluxDB: %v", err)
}
return 0, 0, nil
}

View File

@ -1,40 +0,0 @@
/*
Copyright 2017 The Kubernetes Authors.
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 initialresources
import (
"testing"
"time"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestInfluxDBGetUsagePercentileCPU(t *testing.T) {
source, _ := newInfluxdbSource("", "", "", "")
_, _, err := source.GetUsagePercentile(api.ResourceCPU, 90, "", "", true, time.Now(), time.Now())
if err == nil {
t.Errorf("Expected error because InfluxDB is temporarily disabled")
}
}
func TestInfluxDBGetUsagePercentileMemory(t *testing.T) {
source, _ := newInfluxdbSource("", "", "", "")
_, _, err := source.GetUsagePercentile(api.ResourceMemory, 90, "", "foo", false, time.Now(), time.Now())
if err == nil {
t.Errorf("Expected error because InfluxDB is temporarily disabled")
}
}

View File

@ -16,13 +16,12 @@ go_library(
"//pkg/apis/core:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/features:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
@ -38,14 +37,14 @@ go_test(
"//pkg/apis/core:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library",
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/features:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
)

View File

@ -2,7 +2,9 @@ approvers:
- deads2k
- liggitt
- tallclair
- mikedanese
reviewers:
- deads2k
- liggitt
- tallclair
- mikedanese

View File

@ -22,7 +22,6 @@ import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apiserver/pkg/admission"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -31,8 +30,8 @@ import (
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
internalversion "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
"k8s.io/kubernetes/pkg/features"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
@ -62,18 +61,18 @@ func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *nodePlugin {
type nodePlugin struct {
*admission.Handler
nodeIdentifier nodeidentifier.NodeIdentifier
podsGetter coreinternalversion.PodsGetter
podsGetter internalversion.PodLister
// allows overriding for testing
features utilfeature.FeatureGate
}
var (
_ = admission.Interface(&nodePlugin{})
_ = kubeapiserveradmission.WantsInternalKubeClientSet(&nodePlugin{})
_ = kubeapiserveradmission.WantsInternalKubeInformerFactory(&nodePlugin{})
)
func (p *nodePlugin) SetInternalKubeClientSet(f internalclientset.Interface) {
p.podsGetter = f.Core()
func (p *nodePlugin) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
p.podsGetter = f.Core().InternalVersion().Pods().Lister()
}
func (p *nodePlugin) ValidateInitialization() error {
@ -183,14 +182,10 @@ func (c *nodePlugin) admitPod(nodeName string, a admission.Attributes) error {
return nil
case admission.Delete:
// get the existing pod from the server cache
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{ResourceVersion: "0"})
// get the existing pod
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName())
if errors.IsNotFound(err) {
// wasn't found in the server cache, do a live lookup before forbidding
existingPod, err = c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{})
if errors.IsNotFound(err) {
return err
}
return err
}
if err != nil {
return admission.NewForbidden(a, err)
@ -241,14 +236,10 @@ func (c *nodePlugin) admitPodEviction(nodeName string, a admission.Attributes) e
}
podName = eviction.Name
}
// get the existing pod from the server cache
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(podName, v1.GetOptions{ResourceVersion: "0"})
// get the existing pod
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(podName)
if errors.IsNotFound(err) {
// wasn't found in the server cache, do a live lookup before forbidding
existingPod, err = c.podsGetter.Pods(a.GetNamespace()).Get(podName, v1.GetOptions{})
if errors.IsNotFound(err) {
return err
}
return err
}
if err != nil {
return admission.NewForbidden(a, err)
@ -347,6 +338,12 @@ func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error {
if node.Spec.ConfigSource != nil && !apiequality.Semantic.DeepEqual(node.Spec.ConfigSource, oldNode.Spec.ConfigSource) {
return admission.NewForbidden(a, fmt.Errorf("cannot update configSource to a new non-nil configSource"))
}
// Don't allow a node to update its own taints. This would allow a node to remove or modify its
// taints in a way that would let it steer disallowed workloads to itself.
if !apiequality.Semantic.DeepEqual(node.Spec.Taints, oldNode.Spec.Taints) {
return admission.NewForbidden(a, fmt.Errorf("cannot modify taints"))
}
}
return nil
@ -376,7 +373,7 @@ func (c *nodePlugin) admitServiceAccount(nodeName string, a admission.Attributes
if ref.UID == "" {
return admission.NewForbidden(a, fmt.Errorf("node requested token with a pod binding without a uid"))
}
pod, err := c.podsGetter.Pods(a.GetNamespace()).Get(ref.Name, v1.GetOptions{})
pod, err := c.podsGetter.Pods(a.GetNamespace()).Get(ref.Name)
if errors.IsNotFound(err) {
return err
}

View File

@ -25,12 +25,12 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/cache"
authenticationapi "k8s.io/kubernetes/pkg/apis/authentication"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/kubernetes/pkg/client/listers/core/internalversion"
"k8s.io/kubernetes/pkg/features"
)
@ -63,6 +63,7 @@ func makeTestPod(namespace, name, node string, mirror bool) *api.Pod {
func makeTestPodEviction(name string) *policy.Eviction {
eviction := &policy.Eviction{}
eviction.Name = name
eviction.Namespace = "ns"
return eviction
}
@ -91,10 +92,22 @@ func Test_nodePlugin_Admit(t *testing.T) {
mynodeObjMeta = metav1.ObjectMeta{Name: "mynode"}
mynodeObj = &api.Node{ObjectMeta: mynodeObjMeta}
mynodeObjConfigA = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{ConfigSource: &api.NodeConfigSource{
ConfigMapRef: &api.ObjectReference{Name: "foo", Namespace: "bar", UID: "fooUID"}}}}
ConfigMap: &api.ConfigMapNodeConfigSource{
Name: "foo",
Namespace: "bar",
UID: "fooUID",
KubeletConfigKey: "kubelet",
}}}}
mynodeObjConfigB = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{ConfigSource: &api.NodeConfigSource{
ConfigMapRef: &api.ObjectReference{Name: "qux", Namespace: "bar", UID: "quxUID"}}}}
othernodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "othernode"}}
ConfigMap: &api.ConfigMapNodeConfigSource{
Name: "qux",
Namespace: "bar",
UID: "quxUID",
KubeletConfigKey: "kubelet",
}}}}
mynodeObjTaintA = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{Taints: []api.Taint{{Key: "mykey", Value: "A"}}}}
mynodeObjTaintB = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{Taints: []api.Taint{{Key: "mykey", Value: "B"}}}}
othernodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "othernode"}}
mymirrorpod = makeTestPod("ns", "mymirrorpod", "mynode", true)
othermirrorpod = makeTestPod("ns", "othermirrorpod", "othernode", true)
@ -125,10 +138,20 @@ func Test_nodePlugin_Admit(t *testing.T) {
svcacctResource = api.Resource("serviceaccounts").WithVersion("v1")
tokenrequestKind = api.Kind("TokenRequest").WithVersion("v1")
noExistingPods = fake.NewSimpleClientset().Core()
existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core()
noExistingPodsIndex = cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil)
noExistingPods = internalversion.NewPodLister(noExistingPodsIndex)
existingPodsIndex = cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil)
existingPods = internalversion.NewPodLister(existingPodsIndex)
)
existingPodsIndex.Add(mymirrorpod)
existingPodsIndex.Add(othermirrorpod)
existingPodsIndex.Add(unboundmirrorpod)
existingPodsIndex.Add(mypod)
existingPodsIndex.Add(otherpod)
existingPodsIndex.Add(unboundpod)
sapod := makeTestPod("ns", "mysapod", "mynode", true)
sapod.Spec.ServiceAccountName = "foo"
@ -143,7 +166,7 @@ func Test_nodePlugin_Admit(t *testing.T) {
tests := []struct {
name string
podsGetter coreinternalversion.PodsGetter
podsGetter internalversion.PodLister
attributes admission.Attributes
features utilfeature.FeatureGate
err string
@ -446,7 +469,7 @@ func Test_nodePlugin_Admit(t *testing.T) {
err: "forbidden: unexpected operation",
},
{
name: "forbid create of eviction for normal pod bound to another",
name: "forbid create of unnamed eviction for normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, otherpod.Namespace, otherpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
@ -612,6 +635,12 @@ func Test_nodePlugin_Admit(t *testing.T) {
attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, "", nodeResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow create of my node with taints",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mynodeObjTaintA, nil, nodeKind, mynodeObj.Namespace, "", nodeResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow update of my node",
podsGetter: existingPods,
@ -660,6 +689,30 @@ func Test_nodePlugin_Admit(t *testing.T) {
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "",
},
{
name: "allow update of my node: no change to taints",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObjTaintA, mynodeObjTaintA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "",
},
{
name: "forbid update of my node: add taints",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObjTaintA, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "cannot modify taints",
},
{
name: "forbid update of my node: remove taints",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObjTaintA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "cannot modify taints",
},
{
name: "forbid update of my node: change taints",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObjTaintA, mynodeObjTaintB, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "cannot modify taints",
},
// Other node object
{

View File

@ -51,8 +51,6 @@ go_library(
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",

View File

@ -37,21 +37,6 @@ import (
// TestPodAdmission verifies various scenarios involving pod/namespace tolerations
func TestPodAdmission(t *testing.T) {
namespace := &api.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "testNamespace",
Namespace: "",
},
}
mockClient := &fake.Clientset{}
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
stopCh := make(chan struct{})
defer close(stopCh)
informerFactory.Start(stopCh)
CPU1000m := resource.MustParse("1000m")
CPU500m := resource.MustParse("500m")
@ -230,57 +215,74 @@ func TestPodAdmission(t *testing.T) {
},
}
for _, test := range tests {
if test.namespaceTolerations != nil {
tolerationStr, err := json.Marshal(test.namespaceTolerations)
if err != nil {
t.Errorf("error in marshalling namespace tolerations %v", test.namespaceTolerations)
t.Run(test.testName, func(t *testing.T) {
namespace := &api.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "testNamespace",
Namespace: "",
Annotations: map[string]string{},
},
}
namespace.Annotations = map[string]string{NSDefaultTolerations: string(tolerationStr)}
}
if test.whitelist != nil {
tolerationStr, err := json.Marshal(test.whitelist)
if err != nil {
t.Errorf("error in marshalling namespace whitelist %v", test.whitelist)
if test.namespaceTolerations != nil {
tolerationStr, err := json.Marshal(test.namespaceTolerations)
if err != nil {
t.Errorf("error in marshalling namespace tolerations %v", test.namespaceTolerations)
}
namespace.Annotations = map[string]string{NSDefaultTolerations: string(tolerationStr)}
}
namespace.Annotations[NSWLTolerations] = string(tolerationStr)
}
informerFactory.Core().InternalVersion().Namespaces().Informer().GetStore().Update(namespace)
if test.whitelist != nil {
tolerationStr, err := json.Marshal(test.whitelist)
if err != nil {
t.Errorf("error in marshalling namespace whitelist %v", test.whitelist)
}
namespace.Annotations[NSWLTolerations] = string(tolerationStr)
}
handler.pluginConfig = &pluginapi.Configuration{Default: test.defaultClusterTolerations, Whitelist: test.clusterWhitelist}
pod := test.pod
pod.Spec.Tolerations = test.podTolerations
mockClient := fake.NewSimpleClientset(namespace)
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Fatalf("unexpected error initializing handler: %v", err)
}
stopCh := make(chan struct{})
defer close(stopCh)
informerFactory.Start(stopCh)
// copy the original pod for tests of uninitialized pod updates.
oldPod := *pod
oldPod.Initializers = &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init"}}}
oldPod.Spec.Tolerations = []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}}
handler.pluginConfig = &pluginapi.Configuration{Default: test.defaultClusterTolerations, Whitelist: test.clusterWhitelist}
pod := test.pod
pod.Spec.Tolerations = test.podTolerations
err := handler.Admit(admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if test.admit && err != nil {
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
} else if !test.admit && err == nil {
t.Errorf("Test: %s, expected an error", test.testName)
}
// copy the original pod for tests of uninitialized pod updates.
oldPod := *pod
oldPod.Initializers = &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init"}}}
oldPod.Spec.Tolerations = []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}}
updatedPodTolerations := pod.Spec.Tolerations
if test.admit && !tolerations.EqualTolerations(updatedPodTolerations, test.mergedTolerations) {
t.Errorf("Test: %s, expected: %#v but got: %#v", test.testName, test.mergedTolerations, updatedPodTolerations)
}
err = handler.Admit(admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if test.admit && err != nil {
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
} else if !test.admit && err == nil {
t.Errorf("Test: %s, expected an error", test.testName)
}
// handles update of uninitialized pod like it's newly created.
err = handler.Admit(admission.NewAttributesRecord(pod, &oldPod, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if test.admit && err != nil {
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
} else if !test.admit && err == nil {
t.Errorf("Test: %s, expected an error", test.testName)
}
updatedPodTolerations := pod.Spec.Tolerations
if test.admit && !tolerations.EqualTolerations(updatedPodTolerations, test.mergedTolerations) {
t.Errorf("Test: %s, expected: %#v but got: %#v", test.testName, test.mergedTolerations, updatedPodTolerations)
}
updatedPodTolerations = pod.Spec.Tolerations
if test.admit && !tolerations.EqualTolerations(updatedPodTolerations, test.mergedTolerations) {
t.Errorf("Test: %s, expected: %#v but got: %#v", test.testName, test.mergedTolerations, updatedPodTolerations)
}
// handles update of uninitialized pod like it's newly created.
err = handler.Admit(admission.NewAttributesRecord(pod, &oldPod, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if test.admit && err != nil {
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
} else if !test.admit && err == nil {
t.Errorf("Test: %s, expected an error", test.testName)
}
updatedPodTolerations = pod.Spec.Tolerations
if test.admit && !tolerations.EqualTolerations(updatedPodTolerations, test.mergedTolerations) {
t.Errorf("Test: %s, expected: %#v but got: %#v", test.testName, test.mergedTolerations, updatedPodTolerations)
}
})
}
}

View File

@ -12,9 +12,8 @@ go_library(
deps = [
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
],
)

View File

@ -19,25 +19,15 @@ limitations under the License.
package install
import (
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
internalapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
versionedapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1"
)
// Install registers the API group and adds types to a scheme
func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) {
if err := announced.NewGroupMetaFactory(
&announced.GroupMetaFactoryArgs{
GroupName: internalapi.GroupName,
VersionPreferenceOrder: []string{versionedapi.SchemeGroupVersion.Version},
AddInternalObjectsToScheme: internalapi.AddToScheme,
},
announced.VersionToSchemeFunc{
versionedapi.SchemeGroupVersion.Version: versionedapi.AddToScheme,
},
).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil {
panic(err)
}
func Install(scheme *runtime.Scheme) {
utilruntime.Must(internalapi.AddToScheme(scheme))
utilruntime.Must(versionedapi.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(versionedapi.SchemeGroupVersion))
}

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -20,10 +20,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
internalapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
@ -33,14 +30,12 @@ import (
)
var (
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
func init() {
install.Install(groupFactoryRegistry, registry, scheme)
install.Install(scheme)
}
// LoadConfiguration loads the provided configuration.

View File

@ -16,10 +16,10 @@ go_test(
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/features:go_default_library",
"//pkg/scheduler/api:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
],
)
@ -37,8 +37,8 @@ go_library(
"//pkg/features:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//pkg/kubelet/types:go_default_library",
"//pkg/scheduler/api:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",

View File

@ -21,6 +21,7 @@ import (
"io"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/admission"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -32,7 +33,6 @@ import (
"k8s.io/kubernetes/pkg/features"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
schedulerapi "k8s.io/kubernetes/pkg/scheduler/api"
)
const (
@ -97,6 +97,10 @@ var (
// Admit checks Pods and admits or rejects them. It also resolves the priority of pods based on their PriorityClass.
// Note that pod validation mechanism prevents update of a pod priority.
func (p *priorityPlugin) Admit(a admission.Attributes) error {
if !utilfeature.DefaultFeatureGate.Enabled(features.PodPriority) {
return nil
}
operation := a.GetOperation()
// Ignore all calls to subresources
if len(a.GetSubresource()) != 0 {
@ -105,7 +109,7 @@ func (p *priorityPlugin) Admit(a admission.Attributes) error {
switch a.GetResource().GroupResource() {
case podResource:
if operation == admission.Create {
if operation == admission.Create || operation == admission.Update {
return p.admitPod(a)
}
return nil
@ -135,6 +139,20 @@ func (p *priorityPlugin) Validate(a admission.Attributes) error {
}
}
// priorityClassPermittedInNamespace returns true if we allow the given priority class name in the
// given namespace. It currently checks that system priorities are created only in the system namespace.
func priorityClassPermittedInNamespace(priorityClassName string, namespace string) bool {
// Only allow system priorities in the system namespace. This is to prevent abuse or incorrect
// usage of these priorities. Pods created at these priorities could preempt system critical
// components.
for _, spc := range scheduling.SystemPriorityClasses() {
if spc.Name == priorityClassName && namespace != metav1.NamespaceSystem {
return false
}
}
return true
}
// admitPod makes sure a new pod does not set spec.Priority field. It also makes sure that the PriorityClassName exists if it is provided and resolves the pod priority from the PriorityClassName.
func (p *priorityPlugin) admitPod(a admission.Attributes) error {
operation := a.GetOperation()
@ -143,18 +161,29 @@ func (p *priorityPlugin) admitPod(a admission.Attributes) error {
return errors.NewBadRequest("resource was marked with kind Pod but was unable to be converted")
}
// Make sure that the client has not set `priority` at the time of pod creation.
if operation == admission.Create && pod.Spec.Priority != nil {
return admission.NewForbidden(a, fmt.Errorf("the integer value of priority must not be provided in pod spec. Priority admission controller populates the value from the given PriorityClass name"))
if operation == admission.Update {
oldPod, ok := a.GetOldObject().(*api.Pod)
if !ok {
return errors.NewBadRequest("resource was marked with kind Pod but was unable to be converted")
}
// This admission plugin set pod.Spec.Priority on create.
// Ensure the existing priority is preserved on update.
// API validation prevents mutations to Priority and PriorityClassName, so any other changes will fail update validation and not be persisted.
if pod.Spec.Priority == nil && oldPod.Spec.Priority != nil {
pod.Spec.Priority = oldPod.Spec.Priority
}
return nil
}
if utilfeature.DefaultFeatureGate.Enabled(features.PodPriority) {
if operation == admission.Create {
var priority int32
// TODO: @ravig - This is for backwards compatibility to ensure that critical pods with annotations just work fine.
// Remove when no longer needed.
if len(pod.Spec.PriorityClassName) == 0 &&
utilfeature.DefaultFeatureGate.Enabled(features.ExperimentalCriticalPodAnnotation) &&
kubelettypes.IsCritical(a.GetNamespace(), pod.Annotations) {
pod.Spec.PriorityClassName = schedulerapi.SystemClusterCritical
pod.Spec.PriorityClassName = scheduling.SystemClusterCritical
}
if len(pod.Spec.PriorityClassName) == 0 {
var err error
@ -163,22 +192,26 @@ func (p *priorityPlugin) admitPod(a admission.Attributes) error {
return fmt.Errorf("failed to get default priority class: %v", err)
}
} else {
// First try to resolve by system priority classes.
priority, ok = schedulerapi.SystemPriorityClasses[pod.Spec.PriorityClassName]
if !ok {
// Now that we didn't find any system priority, try resolving by user defined priority classes.
pc, err := p.lister.Get(pod.Spec.PriorityClassName)
pcName := pod.Spec.PriorityClassName
if !priorityClassPermittedInNamespace(pcName, a.GetNamespace()) {
return admission.NewForbidden(a, fmt.Errorf("pods with %v priorityClass is not permitted in %v namespace", pcName, a.GetNamespace()))
}
if err != nil {
if errors.IsNotFound(err) {
return admission.NewForbidden(a, fmt.Errorf("no PriorityClass with name %v was found", pod.Spec.PriorityClassName))
}
return fmt.Errorf("failed to get PriorityClass with name %s: %v", pod.Spec.PriorityClassName, err)
// Try resolving the priority class name.
pc, err := p.lister.Get(pod.Spec.PriorityClassName)
if err != nil {
if errors.IsNotFound(err) {
return admission.NewForbidden(a, fmt.Errorf("no PriorityClass with name %v was found", pod.Spec.PriorityClassName))
}
priority = pc.Value
return fmt.Errorf("failed to get PriorityClass with name %s: %v", pod.Spec.PriorityClassName, err)
}
priority = pc.Value
}
// if the pod contained a priority that differs from the one computed from the priority class, error
if pod.Spec.Priority != nil && *pod.Spec.Priority != priority {
return admission.NewForbidden(a, fmt.Errorf("the integer value of priority (%d) must not be provided in pod spec; priority admission controller computed %d from the given PriorityClass name", *pod.Spec.Priority, priority))
}
pod.Spec.Priority = &priority
}
@ -192,12 +225,6 @@ func (p *priorityPlugin) validatePriorityClass(a admission.Attributes) error {
if !ok {
return errors.NewBadRequest("resource was marked with kind PriorityClass but was unable to be converted")
}
if pc.Value > schedulerapi.HighestUserDefinablePriority {
return admission.NewForbidden(a, fmt.Errorf("maximum allowed value of a user defined priority is %v", schedulerapi.HighestUserDefinablePriority))
}
if _, ok := schedulerapi.SystemPriorityClasses[pc.Name]; ok {
return admission.NewForbidden(a, fmt.Errorf("the name of the priority class is a reserved name for system use only: %v", pc.Name))
}
// If the new PriorityClass tries to be the default priority, make sure that no other priority class is marked as default.
if pc.GlobalDefault {
dpc, err := p.getDefaultPriorityClass()

View File

@ -24,13 +24,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/scheduling"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/features"
schedulerapi "k8s.io/kubernetes/pkg/scheduler/api"
)
func addPriorityClasses(ctrl *priorityPlugin, priorityClasses []*scheduling.PriorityClass) {
@ -75,58 +75,58 @@ var nondefaultClass1 = &scheduling.PriorityClass{
Description: "Just a test priority class",
}
func TestPriorityClassAdmission(t *testing.T) {
var tooHighPriorityClass = &scheduling.PriorityClass{
TypeMeta: metav1.TypeMeta{
Kind: "PriorityClass",
},
ObjectMeta: metav1.ObjectMeta{
Name: "toohighclass",
},
Value: schedulerapi.HighestUserDefinablePriority + 1,
Description: "Just a test priority class",
}
var systemClusterCritical = &scheduling.PriorityClass{
TypeMeta: metav1.TypeMeta{
Kind: "PriorityClass",
},
ObjectMeta: metav1.ObjectMeta{
Name: scheduling.SystemClusterCritical,
},
Value: scheduling.SystemCriticalPriority,
GlobalDefault: true,
}
func TestPriorityClassAdmission(t *testing.T) {
var systemClass = &scheduling.PriorityClass{
TypeMeta: metav1.TypeMeta{
Kind: "PriorityClass",
},
ObjectMeta: metav1.ObjectMeta{
Name: schedulerapi.SystemClusterCritical,
Name: scheduling.SystemPriorityClassPrefix + "test",
},
Value: schedulerapi.HighestUserDefinablePriority + 1,
Description: "Name conflicts with system priority class names",
Value: scheduling.HighestUserDefinablePriority + 1,
Description: "Name has system critical prefix",
}
tests := []struct {
name string
existingClasses []*scheduling.PriorityClass
newClass *scheduling.PriorityClass
userInfo user.Info
expectError bool
}{
{
"one default class",
[]*scheduling.PriorityClass{},
defaultClass1,
nil,
false,
},
{
"more than one default classes",
[]*scheduling.PriorityClass{defaultClass1},
defaultClass2,
nil,
true,
},
{
"too high PriorityClass value",
[]*scheduling.PriorityClass{},
tooHighPriorityClass,
true,
},
{
"system name conflict",
"system name and value are allowed by admission controller",
[]*scheduling.PriorityClass{},
systemClass,
true,
&user.DefaultInfo{
Name: user.APIServerUser,
},
false,
},
}
@ -146,7 +146,7 @@ func TestPriorityClassAdmission(t *testing.T) {
scheduling.Resource("priorityclasses").WithVersion("version"),
"",
admission.Create,
nil,
test.userInfo,
)
err := ctrl.Validate(attrs)
glog.Infof("Got %v", err)
@ -244,6 +244,7 @@ func TestDefaultPriority(t *testing.T) {
}
}
var zeroPriority = int32(0)
var intPriority = int32(1000)
func TestPodAdmission(t *testing.T) {
@ -314,7 +315,7 @@ func TestPodAdmission(t *testing.T) {
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-w-system-priority",
Namespace: "namespace",
Namespace: metav1.NamespaceSystem,
},
Spec: api.PodSpec{
Containers: []api.Container{
@ -322,14 +323,14 @@ func TestPodAdmission(t *testing.T) {
Name: containerName,
},
},
PriorityClassName: schedulerapi.SystemClusterCritical,
PriorityClassName: scheduling.SystemClusterCritical,
},
},
// pod[5]: mirror Pod with a system priority class name
{
ObjectMeta: metav1.ObjectMeta{
Name: "mirror-pod-w-system-priority",
Namespace: "namespace",
Namespace: metav1.NamespaceSystem,
Annotations: map[string]string{api.MirrorPodAnnotationKey: ""},
},
Spec: api.PodSpec{
@ -374,6 +375,67 @@ func TestPodAdmission(t *testing.T) {
},
},
},
// pod[8]: Pod with a system priority class name in non-system namespace
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-w-system-priority-in-nonsystem-namespace",
Namespace: "non-system-namespace",
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: containerName,
},
},
PriorityClassName: scheduling.SystemClusterCritical,
},
},
// pod[9]: Pod with a priority value that matches the resolved priority
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-w-zero-priority-in-nonsystem-namespace",
Namespace: "non-system-namespace",
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: containerName,
},
},
Priority: &zeroPriority,
},
},
// pod[10]: Pod with a priority value that matches the resolved default priority
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-w-priority-matching-default-priority",
Namespace: "non-system-namespace",
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: containerName,
},
},
Priority: &defaultClass2.Value,
},
},
// pod[11]: Pod with a priority value that matches the resolved priority
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-w-priority-matching-resolved-default-priority",
Namespace: metav1.NamespaceSystem,
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: containerName,
},
},
PriorityClassName: systemClusterCritical.Name,
Priority: &systemClusterCritical.Value,
},
},
}
// Enable PodPriority feature gate.
utilfeature.DefaultFeatureGate.Set(fmt.Sprintf("%s=true", features.PodPriority))
@ -419,9 +481,9 @@ func TestPodAdmission(t *testing.T) {
},
{
"pod with a system priority class",
[]*scheduling.PriorityClass{},
[]*scheduling.PriorityClass{systemClusterCritical},
*pods[4],
schedulerapi.SystemCriticalPriority,
scheduling.SystemCriticalPriority,
false,
},
{
@ -440,9 +502,9 @@ func TestPodAdmission(t *testing.T) {
},
{
"mirror pod with system priority class",
[]*scheduling.PriorityClass{},
[]*scheduling.PriorityClass{systemClusterCritical},
*pods[5],
schedulerapi.SystemCriticalPriority,
scheduling.SystemCriticalPriority,
false,
},
{
@ -454,9 +516,37 @@ func TestPodAdmission(t *testing.T) {
},
{
"pod with critical pod annotation",
[]*scheduling.PriorityClass{},
[]*scheduling.PriorityClass{systemClusterCritical},
*pods[7],
schedulerapi.SystemCriticalPriority,
scheduling.SystemCriticalPriority,
false,
},
{
"pod with system critical priority in non-system namespace",
[]*scheduling.PriorityClass{systemClusterCritical},
*pods[8],
scheduling.SystemCriticalPriority,
true,
},
{
"pod with priority that matches computed priority",
[]*scheduling.PriorityClass{nondefaultClass1},
*pods[9],
0,
false,
},
{
"pod with priority that matches default priority",
[]*scheduling.PriorityClass{defaultClass2},
*pods[10],
defaultClass2.Value,
false,
},
{
"pod with priority that matches resolved priority",
[]*scheduling.PriorityClass{systemClusterCritical},
*pods[11],
systemClusterCritical.Value,
false,
},
}
@ -485,8 +575,7 @@ func TestPodAdmission(t *testing.T) {
if !test.expectError {
if err != nil {
t.Errorf("Test %q: unexpected error received: %v", test.name, err)
}
if *test.pod.Spec.Priority != test.expectedPriority {
} else if *test.pod.Spec.Priority != test.expectedPriority {
t.Errorf("Test %q: expected priority is %d, but got %d.", test.name, test.expectedPriority, *test.pod.Spec.Priority)
}
}

View File

@ -32,9 +32,8 @@ go_library(
"//plugin/pkg/admission/resourcequota/apis/resourcequota/validation:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",

View File

@ -17,6 +17,7 @@ limitations under the License.
package resourcequota
import (
"fmt"
"strconv"
"strings"
"testing"
@ -73,6 +74,14 @@ func validPod(name string, numContainers int, resources api.ResourceRequirements
return pod
}
func validPodWithPriority(name string, numContainers int, resources api.ResourceRequirements, priorityClass string) *api.Pod {
pod := validPod(name, numContainers, resources)
if priorityClass != "" {
pod.Spec.PriorityClassName = priorityClass
}
return pod
}
func validPersistentVolumeClaim(name string, resources api.ResourceRequirements) *api.PersistentVolumeClaim {
return &api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
@ -1445,3 +1454,672 @@ func TestAdmitLimitedResourceWithQuotaThatDoesNotCover(t *testing.T) {
t.Fatalf("Expected an error since the quota did not cover cpu")
}
}
// TestAdmitLimitedScopeWithQuota verifies if a limited scope is configured the quota must cover the resource.
func TestAdmitLimitedScopeWithCoverQuota(t *testing.T) {
testCases := []struct {
description string
testPod *api.Pod
quota *api.ResourceQuota
anotherQuota *api.ResourceQuota
config *resourcequotaapi.Configuration
expErr string
}{
{
description: "Covering quota exists for configured limited scope PriorityClassNameExists.",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists,
},
},
},
},
},
expErr: "",
},
{
description: "configured limited scope PriorityClassNameExists and limited cpu resource. No covering quota for cpu and pod admit fails.",
testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists,
},
},
MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
},
},
},
expErr: "insufficient quota to consume: requests.cpu",
},
{
description: "Covering quota does not exist for configured limited scope PriorityClassNameExists.",
testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists,
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{PriorityClass Exists []}]",
},
{
description: "Covering quota does not exist for configured limited scope resourceQuotaBestEffort",
testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
},
{
description: "Covering quota exist for configured limited scope resourceQuotaBestEffort",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort},
},
Status: api.ResourceQuotaStatus{
Hard: api.ResourceList{
api.ResourcePods: resource.MustParse("5"),
},
Used: api.ResourceList{
api.ResourcePods: resource.MustParse("3"),
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
},
},
expErr: "",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Neither matches pod. Pod allowed",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Only BestEffort scope matches pod. Pod admit fails because covering quota is missing for BestEffort scope",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Only PriorityClass scope matches pod. Pod admit fails because covering quota is missing for PriorityClass scope",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("100m", "1Gi"), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{PriorityClass In [cluster-services]}]",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Pod admit fails because covering quota is missing for PriorityClass scope and BestEffort scope",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{BestEffort Exists []} {PriorityClass In [cluster-services]}]",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Quota available only for BestEffort scope. Pod admit fails because covering quota is missing for PriorityClass scope",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort},
},
Status: api.ResourceQuotaStatus{
Hard: api.ResourceList{
api.ResourcePods: resource.MustParse("5"),
},
Used: api.ResourceList{
api.ResourcePods: resource.MustParse("3"),
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{PriorityClass In [cluster-services]}]",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Quota available only for PriorityClass scope. Pod admit fails because covering quota is missing for BestEffort scope",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{BestEffort Exists []}]",
},
{
description: "Two scopes,BestEffort and PriorityClassIN, in two LimitedResources. Both the scopes matches pod. Quota available only for both the scopes. Pod admit success. No Error",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota-besteffort", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
Scopes: []api.ResourceQuotaScope{api.ResourceQuotaScopeBestEffort},
},
Status: api.ResourceQuotaStatus{
Hard: api.ResourceList{
api.ResourcePods: resource.MustParse("5"),
},
Used: api.ResourceList{
api.ResourcePods: resource.MustParse("3"),
},
},
},
anotherQuota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopeBestEffort,
Operator: api.ScopeSelectorOpExists,
},
},
},
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "",
},
{
description: "Pod allowed with priorityclass if limited scope PriorityClassNameExists not configured.",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{},
expErr: "",
},
{
description: "quota fails, though covering quota for configured limited scope PriorityClassNameExists exists.",
testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "20Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists},
},
},
},
Status: api.ResourceQuotaStatus{
Hard: api.ResourceList{
api.ResourceMemory: resource.MustParse("10Gi"),
},
Used: api.ResourceList{
api.ResourceMemory: resource.MustParse("1Gi"),
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists,
},
},
},
},
},
expErr: "forbidden: exceeded quota: quota, requested: memory=20Gi, used: memory=1Gi, limited: memory=10Gi",
},
{
description: "Pod has different priorityclass than configured limited. Covering quota exists for configured limited scope PriorityClassIn.",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "fake-priority"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
expErr: "",
},
{
description: "Pod has limited priorityclass. Covering quota exists for configured limited scope PriorityClassIn.",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"cluster-services"},
},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name", "cluster-services"},
},
},
},
},
},
expErr: "",
},
{
description: "Pod has limited priorityclass. Covering quota does not exist for configured limited scope PriorityClassIn.",
testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name"},
},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name", "cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{PriorityClass In [another-priorityclass-name cluster-services]}]",
},
{
description: "From the above test case, just changing pod priority from cluster-services to another-priorityclass-name. expecting no error",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "another-priorityclass-name"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name"},
},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name", "cluster-services"},
},
},
},
},
},
expErr: "",
},
{
description: "Pod has limited priorityclass. Covering quota does NOT exists for configured limited scope PriorityClassIn.",
testPod: validPodWithPriority("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name", "cluster-services"},
},
},
},
},
},
expErr: "insufficient quota to match these scopes: [{PriorityClass In [another-priorityclass-name cluster-services]}]",
},
{
description: "Pod has limited priorityclass. Covering quota exists for configured limited scope PriorityClassIn through PriorityClassNameExists",
testPod: validPodWithPriority("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")), "cluster-services"),
quota: &api.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
Spec: api.ResourceQuotaSpec{
ScopeSelector: &api.ScopeSelector{
MatchExpressions: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpExists},
},
},
},
},
config: &resourcequotaapi.Configuration{
LimitedResources: []resourcequotaapi.LimitedResource{
{
Resource: "pods",
MatchScopes: []api.ScopedResourceSelectorRequirement{
{
ScopeName: api.ResourceQuotaScopePriorityClass,
Operator: api.ScopeSelectorOpIn,
Values: []string{"another-priorityclass-name", "cluster-services"},
},
},
},
},
},
expErr: "",
},
}
for _, testCase := range testCases {
newPod := testCase.testPod
config := testCase.config
resourceQuota := testCase.quota
kubeClient := fake.NewSimpleClientset(resourceQuota)
if testCase.anotherQuota != nil {
kubeClient = fake.NewSimpleClientset(resourceQuota, testCase.anotherQuota)
}
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
stopCh := make(chan struct{})
defer close(stopCh)
informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
quotaAccessor, _ := newQuotaAccessor()
quotaAccessor.client = kubeClient
quotaAccessor.lister = informerFactory.Core().InternalVersion().ResourceQuotas().Lister()
quotaConfiguration := install.NewQuotaConfigurationForAdmission()
evaluator := NewQuotaEvaluator(quotaAccessor, quotaConfiguration.IgnoredResources(), generic.NewRegistry(quotaConfiguration.Evaluators()), nil, config, 5, stopCh)
handler := &QuotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update),
evaluator: evaluator,
}
indexer.Add(resourceQuota)
if testCase.anotherQuota != nil {
indexer.Add(testCase.anotherQuota)
}
err := handler.Validate(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if testCase.expErr == "" {
if err != nil {
t.Fatalf("Testcase, %v, failed with unexpected error: %v. ExpErr: %v", testCase.description, err, testCase.expErr)
}
} else {
if !strings.Contains(fmt.Sprintf("%v", err), testCase.expErr) {
t.Fatalf("Testcase, %v, failed with unexpected error: %v. ExpErr: %v", testCase.description, err, testCase.expErr)
}
}
}
}

View File

@ -15,6 +15,7 @@ go_library(
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota",
deps = [
"//pkg/apis/core:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",

View File

@ -12,9 +12,8 @@ go_library(
deps = [
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
],
)

View File

@ -19,25 +19,15 @@ limitations under the License.
package install
import (
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
resourcequotav1alpha1 "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1"
)
// Install registers the API group and adds types to a scheme
func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) {
if err := announced.NewGroupMetaFactory(
&announced.GroupMetaFactoryArgs{
GroupName: resourcequotaapi.GroupName,
VersionPreferenceOrder: []string{resourcequotav1alpha1.SchemeGroupVersion.Version},
AddInternalObjectsToScheme: resourcequotaapi.AddToScheme,
},
announced.VersionToSchemeFunc{
resourcequotav1alpha1.SchemeGroupVersion.Version: resourcequotav1alpha1.AddToScheme,
},
).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil {
panic(err)
}
func Install(scheme *runtime.Scheme) {
utilruntime.Must(resourcequotaapi.AddToScheme(scheme))
utilruntime.Must(resourcequotav1alpha1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(resourcequotav1alpha1.SchemeGroupVersion))
}

View File

@ -16,7 +16,10 @@ limitations under the License.
package resourcequota
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apis/core"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -54,4 +57,16 @@ type LimitedResource struct {
// with any storage class, the list would include
// ".storageclass.storage.k8s.io/requests.storage"
MatchContains []string
// For each intercepted request, the quota system will figure out if the input object
// satisfies a scope which is present in this listing, then
// quota system will ensure that there is a covering quota. In the
// absence of a covering quota, the quota system will deny the request.
// For example, if an administrator wants to globally enforce that
// a quota must exist to create a pod with "cluster-services" priorityclass
// the list would include
// "PriorityClassNameIn=cluster-services"
// +optional
// MatchScopes []string `json:"matchScopes,omitempty"`
MatchScopes []core.ScopedResourceSelectorRequirement `json:"matchScopes,omitempty"`
}

View File

@ -18,7 +18,9 @@ go_library(
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1",
deps = [
"//pkg/apis/core:go_default_library",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",

View File

@ -16,7 +16,10 @@ limitations under the License.
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -54,4 +57,13 @@ type LimitedResource struct {
// with any storage class, the list would include
// ".storageclass.storage.k8s.io/requests.storage"
MatchContains []string `json:"matchContains,omitempty"`
// For each intercepted request, the quota system will figure out if the input object
// satisfies a scope which is present in this listing, then
// quota system will ensure that there is a covering quota. In the
// absence of a covering quota, the quota system will deny the request.
// For example, if an administrator wants to globally enforce that
// a quota must exist to create a pod with "cluster-services" priorityclass
// the list would include "scopeName=PriorityClass, Operator=In, Value=cluster-services"
// +optional
MatchScopes []v1.ScopedResourceSelectorRequirement `json:"matchScopes,omitempty"`
}

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,8 +23,10 @@ package v1alpha1
import (
unsafe "unsafe"
v1 "k8s.io/api/core/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
core "k8s.io/kubernetes/pkg/apis/core"
resourcequota "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
)
@ -67,6 +69,7 @@ func autoConvert_v1alpha1_LimitedResource_To_resourcequota_LimitedResource(in *L
out.APIGroup = in.APIGroup
out.Resource = in.Resource
out.MatchContains = *(*[]string)(unsafe.Pointer(&in.MatchContains))
out.MatchScopes = *(*[]core.ScopedResourceSelectorRequirement)(unsafe.Pointer(&in.MatchScopes))
return nil
}
@ -79,6 +82,7 @@ func autoConvert_resourcequota_LimitedResource_To_v1alpha1_LimitedResource(in *r
out.APIGroup = in.APIGroup
out.Resource = in.Resource
out.MatchContains = *(*[]string)(unsafe.Pointer(&in.MatchContains))
out.MatchScopes = *(*[]v1.ScopedResourceSelectorRequirement)(unsafe.Pointer(&in.MatchScopes))
return nil
}

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ limitations under the License.
package v1alpha1
import (
v1 "k8s.io/api/core/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -64,6 +65,13 @@ func (in *LimitedResource) DeepCopyInto(out *LimitedResource) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.MatchScopes != nil {
in, out := &in.MatchScopes, &out.MatchScopes
*out = make([]v1.ScopedResourceSelectorRequirement, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
// +build !ignore_autogenerated
/*
Copyright 2018 The Kubernetes Authors.
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ package resourcequota
import (
runtime "k8s.io/apimachinery/pkg/runtime"
core "k8s.io/kubernetes/pkg/apis/core"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -64,6 +65,13 @@ func (in *LimitedResource) DeepCopyInto(out *LimitedResource) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.MatchScopes != nil {
in, out := &in.MatchScopes, &out.MatchScopes
*out = make([]core.ScopedResourceSelectorRequirement, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}

View File

@ -20,10 +20,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
@ -32,14 +29,12 @@ import (
)
var (
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
func init() {
install.Install(groupFactoryRegistry, registry, scheme)
install.Install(scheme)
}
// LoadConfiguration loads the provided configuration.

View File

@ -25,7 +25,9 @@ import (
"github.com/golang/glog"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
@ -366,6 +368,21 @@ func limitedByDefault(usage api.ResourceList, limitedResources []resourcequotaap
return result
}
func getMatchedLimitedScopes(evaluator quota.Evaluator, inputObject runtime.Object, limitedResources []resourcequotaapi.LimitedResource) ([]api.ScopedResourceSelectorRequirement, error) {
scopes := []api.ScopedResourceSelectorRequirement{}
for _, limitedResource := range limitedResources {
matched, err := evaluator.MatchingScopes(inputObject, limitedResource.MatchScopes)
if err != nil {
glog.Errorf("Error while matching limited Scopes: %v", err)
return []api.ScopedResourceSelectorRequirement{}, err
}
for _, scope := range matched {
scopes = append(scopes, scope)
}
}
return scopes, nil
}
// checkRequest verifies that the request does not exceed any quota constraint. it returns a copy of quotas not yet persisted
// that capture what the usage would be if the request succeeded. It return an error if there is insufficient quota to satisfy the request
func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.Attributes) ([]api.ResourceQuota, error) {
@ -382,6 +399,12 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
// if we have limited resources enabled for this resource, always calculate usage
inputObject := a.GetObject()
// Check if object matches AdmissionConfiguration matchScopes
limitedScopes, err := getMatchedLimitedScopes(evaluator, inputObject, e.config.LimitedResources)
if err != nil {
return quotas, nil
}
// determine the set of resource names that must exist in a covering quota
limitedResourceNames := []api.ResourceName{}
limitedResources := filterLimitedResourcesByGroupResource(e.config.LimitedResources, a.GetResource().GroupResource())
@ -403,10 +426,21 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
// this is needed to know if we have satisfied any constraints where consumption
// was limited by default.
restrictedResourcesSet := sets.String{}
restrictedScopes := []api.ScopedResourceSelectorRequirement{}
for i := range quotas {
resourceQuota := quotas[i]
scopeSelectors := getScopeSelectorsFromQuota(resourceQuota)
localRestrictedScopes, err := evaluator.MatchingScopes(inputObject, scopeSelectors)
if err != nil {
return nil, fmt.Errorf("error matching scopes of quota %s, err: %v", resourceQuota.Name, err)
}
for _, scope := range localRestrictedScopes {
restrictedScopes = append(restrictedScopes, scope)
}
match, err := evaluator.Matches(&resourceQuota, inputObject)
if err != nil {
glog.Errorf("Error occurred while matching resource quota, %v, against input object. Err: %v", resourceQuota, err)
return quotas, err
}
if !match {
@ -431,7 +465,18 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
// if not, we reject the request.
hasNoCoveringQuota := limitedResourceNamesSet.Difference(restrictedResourcesSet)
if len(hasNoCoveringQuota) > 0 {
return quotas, fmt.Errorf("insufficient quota to consume: %v", strings.Join(hasNoCoveringQuota.List(), ","))
return quotas, admission.NewForbidden(a, fmt.Errorf("insufficient quota to consume: %v", strings.Join(hasNoCoveringQuota.List(), ",")))
}
// verify that for every scope that had limited access enabled
// that there was a corresponding quota that covered it.
// if not, we reject the request.
scopesHasNoCoveringQuota, err := evaluator.UncoveredQuotaScopes(limitedScopes, restrictedScopes)
if err != nil {
return quotas, err
}
if len(scopesHasNoCoveringQuota) > 0 {
return quotas, fmt.Errorf("insufficient quota to match these scopes: %v", scopesHasNoCoveringQuota)
}
if len(interestingQuotaIndexes) == 0 {
@ -515,6 +560,21 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
return outQuotas, nil
}
func getScopeSelectorsFromQuota(quota api.ResourceQuota) []api.ScopedResourceSelectorRequirement {
selectors := []api.ScopedResourceSelectorRequirement{}
for _, scope := range quota.Spec.Scopes {
selectors = append(selectors, api.ScopedResourceSelectorRequirement{
ScopeName: scope,
Operator: api.ScopeSelectorOpExists})
}
if quota.Spec.ScopeSelector != nil {
for _, scopeSelector := range quota.Spec.ScopeSelector.MatchExpressions {
selectors = append(selectors, scopeSelector)
}
}
return selectors
}
func (e *quotaEvaluator) Evaluate(a admission.Attributes) error {
e.init.Do(func() {
go e.run()
@ -531,7 +591,7 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error {
evaluator := e.registry.Get(gr)
if evaluator == nil {
// create an object count evaluator if no evaluator previously registered
// note, we do not need aggregate usage here, so we pass a nil infomer func
// note, we do not need aggregate usage here, so we pass a nil informer func
evaluator = generic.NewObjectCountEvaluator(false, gr, nil, "")
e.registry.Add(evaluator)
glog.Infof("quota admission added evaluator for: %s", gr)
@ -549,7 +609,7 @@ func (e *quotaEvaluator) Evaluate(a admission.Attributes) error {
select {
case <-waiter.finished:
case <-time.After(10 * time.Second):
return fmt.Errorf("timeout")
return apierrors.NewInternalError(fmt.Errorf("resource quota evaluates timeout"))
}
return waiter.result
@ -582,6 +642,11 @@ func (e *quotaEvaluator) completeWork(ns string) {
e.inProgress.Delete(ns)
}
// getWork returns a namespace, a list of work items in that
// namespace, and a shutdown boolean. If not shutdown then the return
// must eventually be followed by a call on completeWork for the
// returned namespace (regardless of whether the work item list is
// empty).
func (e *quotaEvaluator) getWork() (string, []*admissionWaiter, bool) {
uncastNS, shutdown := e.queue.Get()
if shutdown {
@ -598,15 +663,8 @@ func (e *quotaEvaluator) getWork() (string, []*admissionWaiter, bool) {
work := e.work[ns]
delete(e.work, ns)
delete(e.dirtyWork, ns)
if len(work) != 0 {
e.inProgress.Insert(ns)
return ns, work, false
}
e.queue.Done(ns)
e.inProgress.Delete(ns)
return ns, []*admissionWaiter{}, false
e.inProgress.Insert(ns)
return ns, work, false
}
// prettyPrint formats a resource list for usage in errors

View File

@ -15,7 +15,7 @@ go_library(
"//pkg/apis/extensions:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/extensions/internalversion:go_default_library",
"//pkg/client/listers/policy/internalversion:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//pkg/registry/rbac:go_default_library",
"//pkg/security/podsecuritypolicy:go_default_library",
@ -39,8 +39,6 @@ go_test(
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//pkg/apis/extensions:go_default_library",
"//pkg/apis/policy:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/controller:go_default_library",
@ -48,6 +46,7 @@ go_test(
"//pkg/security/podsecuritypolicy:go_default_library",
"//pkg/security/podsecuritypolicy/seccomp:go_default_library",
"//pkg/security/podsecuritypolicy/util:go_default_library",
"//pkg/util/pointer:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",

View File

@ -35,7 +35,7 @@ import (
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/apis/policy"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
extensionslisters "k8s.io/kubernetes/pkg/client/listers/extensions/internalversion"
policylisters "k8s.io/kubernetes/pkg/client/listers/policy/internalversion"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
psp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
@ -61,7 +61,7 @@ type PodSecurityPolicyPlugin struct {
strategyFactory psp.StrategyFactory
failOnNoPolicies bool
authz authorizer.Authorizer
lister extensionslisters.PodSecurityPolicyLister
lister policylisters.PodSecurityPolicyLister
}
// SetAuthorizer sets the authorizer.
@ -84,6 +84,7 @@ var _ admission.MutationInterface = &PodSecurityPolicyPlugin{}
var _ admission.ValidationInterface = &PodSecurityPolicyPlugin{}
var _ genericadmissioninit.WantsAuthorizer = &PodSecurityPolicyPlugin{}
var _ kubeapiserveradmission.WantsInternalKubeInformerFactory = &PodSecurityPolicyPlugin{}
var auditKeyPrefix = strings.ToLower(PluginName) + "." + policy.GroupName + ".k8s.io"
// newPlugin creates a new PSP admission plugin.
func newPlugin(strategyFactory psp.StrategyFactory, failOnNoPolicies bool) *PodSecurityPolicyPlugin {
@ -95,7 +96,7 @@ func newPlugin(strategyFactory psp.StrategyFactory, failOnNoPolicies bool) *PodS
}
func (a *PodSecurityPolicyPlugin) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
podSecurityPolicyInformer := f.Extensions().InternalVersion().PodSecurityPolicies()
podSecurityPolicyInformer := f.Policy().InternalVersion().PodSecurityPolicies()
a.lister = podSecurityPolicyInformer.Lister()
a.SetReadyFunc(podSecurityPolicyInformer.Informer().HasSynced)
}
@ -136,6 +137,10 @@ func (c *PodSecurityPolicyPlugin) Admit(a admission.Attributes) error {
pod.ObjectMeta.Annotations = map[string]string{}
}
pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation] = pspName
key := auditKeyPrefix + "/" + "admit-policy"
if err := a.AddAnnotation(key, pspName); err != nil {
glog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
}
return nil
}
@ -154,11 +159,15 @@ func (c *PodSecurityPolicyPlugin) Validate(a admission.Attributes) error {
pod := a.GetObject().(*api.Pod)
// compute the context. Mutation is not allowed. ValidatedPSPAnnotation is used as a hint to gain same speed-up.
allowedPod, _, validationErrs, err := c.computeSecurityContext(a, pod, false, pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation])
allowedPod, pspName, validationErrs, err := c.computeSecurityContext(a, pod, false, pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation])
if err != nil {
return admission.NewForbidden(a, err)
}
if apiequality.Semantic.DeepEqual(pod, allowedPod) {
key := auditKeyPrefix + "/" + "validate-policy"
if err := a.AddAnnotation(key, pspName); err != nil {
glog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
}
return nil
}
@ -329,7 +338,7 @@ func assignSecurityContext(provider psp.Provider, pod *api.Pod, fldPath *field.P
}
// createProvidersFromPolicies creates providers from the constraints supplied.
func (c *PodSecurityPolicyPlugin) createProvidersFromPolicies(psps []*extensions.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) {
func (c *PodSecurityPolicyPlugin) createProvidersFromPolicies(psps []*policy.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) {
var (
// collected providers
providers []psp.Provider

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
approvers:
- liggitt
- deads2k
- mikedanese
reviewers:
- liggitt
- deads2k
- mikedanese
- enj

View File

@ -208,6 +208,15 @@ func (s *serviceAccount) Validate(a admission.Attributes) (err error) {
if hasSecrets {
return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference secrets"))
}
for _, v := range pod.Spec.Volumes {
if proj := v.Projected; proj != nil {
for _, projSource := range proj.Sources {
if projSource.ServiceAccountToken != nil {
return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ServiceAccountToken volume projections"))
}
}
}
}
return nil
}

View File

@ -138,6 +138,31 @@ func TestRejectsMirrorPodWithSecretVolumes(t *testing.T) {
}
}
func TestRejectsMirrorPodWithServiceAccountTokenVolumeProjections(t *testing.T) {
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
kubelet.ConfigMirrorAnnotationKey: "true",
},
},
Spec: api.PodSpec{
Volumes: []api.Volume{
{VolumeSource: api.VolumeSource{
Projected: &api.ProjectedVolumeSource{
Sources: []api.VolumeProjection{{ServiceAccountToken: &api.ServiceAccountTokenProjection{}}},
},
},
},
},
},
}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "myns", "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, nil)
err := NewServiceAccount().Admit(attrs)
if err == nil {
t.Errorf("Expected a mirror pod to be prevented from referencing a ServiceAccountToken volume projection")
}
}
func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) {
ns := "myns"

View File

@ -12,7 +12,7 @@ go_library(
"admission.go",
"doc.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label",
importpath = "k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/label",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/cloudprovider:go_default_library",

View File

@ -33,12 +33,15 @@ import (
vol "k8s.io/kubernetes/pkg/volume"
)
const PluginName = "PersistentVolumeLabel"
const (
// PluginName is the name of persistent volume label admission plugin
PluginName = "PersistentVolumeLabel"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
persistentVolumeLabelAdmission := NewPersistentVolumeLabel()
persistentVolumeLabelAdmission := newPersistentVolumeLabel()
return persistentVolumeLabelAdmission, nil
})
}
@ -57,11 +60,11 @@ type persistentVolumeLabel struct {
var _ admission.MutationInterface = &persistentVolumeLabel{}
var _ kubeapiserveradmission.WantsCloudConfig = &persistentVolumeLabel{}
// NewPersistentVolumeLabel returns an admission.Interface implementation which adds labels to PersistentVolume CREATE requests,
// newPersistentVolumeLabel returns an admission.Interface implementation which adds labels to PersistentVolume CREATE requests,
// based on the labels provided by the underlying cloud provider.
//
// As a side effect, the cloud provider may block invalid or non-existent volumes.
func NewPersistentVolumeLabel() *persistentVolumeLabel {
func newPersistentVolumeLabel() *persistentVolumeLabel {
// DEPRECATED: cloud-controller-manager will now start NewPersistentVolumeLabelController
// which does exactly what this admission controller used to do. So once GCE and AWS can
// run externally, we can remove this admission controller.

View File

@ -36,7 +36,7 @@ type mockVolumes struct {
var _ aws.Volumes = &mockVolumes{}
func (v *mockVolumes) AttachDisk(diskName aws.KubernetesVolumeID, nodeName types.NodeName, readOnly bool) (string, error) {
func (v *mockVolumes) AttachDisk(diskName aws.KubernetesVolumeID, nodeName types.NodeName) (string, error) {
return "", fmt.Errorf("not implemented")
}
@ -56,19 +56,19 @@ func (v *mockVolumes) GetVolumeLabels(volumeName aws.KubernetesVolumeID) (map[st
return v.volumeLabels, v.volumeLabelsError
}
func (c *mockVolumes) GetDiskPath(volumeName aws.KubernetesVolumeID) (string, error) {
func (v *mockVolumes) GetDiskPath(volumeName aws.KubernetesVolumeID) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (c *mockVolumes) DiskIsAttached(volumeName aws.KubernetesVolumeID, nodeName types.NodeName) (bool, error) {
func (v *mockVolumes) DiskIsAttached(volumeName aws.KubernetesVolumeID, nodeName types.NodeName) (bool, error) {
return false, fmt.Errorf("not implemented")
}
func (c *mockVolumes) DisksAreAttached(nodeDisks map[types.NodeName][]aws.KubernetesVolumeID) (map[types.NodeName]map[aws.KubernetesVolumeID]bool, error) {
func (v *mockVolumes) DisksAreAttached(nodeDisks map[types.NodeName][]aws.KubernetesVolumeID) (map[types.NodeName]map[aws.KubernetesVolumeID]bool, error) {
return nil, fmt.Errorf("not implemented")
}
func (c *mockVolumes) ResizeDisk(
func (v *mockVolumes) ResizeDisk(
diskName aws.KubernetesVolumeID,
oldSize resource.Quantity,
newSize resource.Quantity) (resource.Quantity, error) {
@ -85,7 +85,7 @@ func mockVolumeLabels(labels map[string]string) *mockVolumes {
// TestAdmission
func TestAdmission(t *testing.T) {
pvHandler := NewPersistentVolumeLabel()
pvHandler := newPersistentVolumeLabel()
handler := admission.NewChainHandler(pvHandler)
ignoredPV := api.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},

View File

@ -14,6 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// labels created persistent volumes with zone information
// Package label created persistent volumes with zone information
// as provided by the cloud provider
package label // import "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label"
package label // import "k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/label"

View File

@ -26,7 +26,7 @@ go_test(
go_library(
name = "go_default_library",
srcs = ["admission.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/resize",
importpath = "k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/resize",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",

View File

@ -149,7 +149,7 @@ func (pvcr *persistentVolumeClaimResize) allowResize(pvc, oldPvc *api.Persistent
// checkVolumePlugin checks whether the volume plugin supports resize
func (pvcr *persistentVolumeClaimResize) checkVolumePlugin(pv *api.PersistentVolume) bool {
if pv.Spec.Glusterfs != nil || pv.Spec.Cinder != nil || pv.Spec.RBD != nil {
if pv.Spec.Glusterfs != nil || pv.Spec.Cinder != nil || pv.Spec.RBD != nil || pv.Spec.PortworxVolume != nil {
return true
}
@ -164,5 +164,9 @@ func (pvcr *persistentVolumeClaimResize) checkVolumePlugin(pv *api.PersistentVol
if pv.Spec.AzureFile != nil {
return true
}
if pv.Spec.AzureDisk != nil {
return true
}
return false
}