vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

46
vendor/k8s.io/kubernetes/plugin/BUILD generated vendored Normal file
View File

@ -0,0 +1,46 @@
package(default_visibility = ["//visibility:public"])
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//plugin/cmd/kube-scheduler:all-srcs",
"//plugin/pkg/admission/admit:all-srcs",
"//plugin/pkg/admission/alwayspullimages:all-srcs",
"//plugin/pkg/admission/antiaffinity:all-srcs",
"//plugin/pkg/admission/defaulttolerationseconds:all-srcs",
"//plugin/pkg/admission/deny:all-srcs",
"//plugin/pkg/admission/eventratelimit:all-srcs",
"//plugin/pkg/admission/exec:all-srcs",
"//plugin/pkg/admission/extendedresourcetoleration:all-srcs",
"//plugin/pkg/admission/gc:all-srcs",
"//plugin/pkg/admission/imagepolicy:all-srcs",
"//plugin/pkg/admission/initialresources:all-srcs",
"//plugin/pkg/admission/limitranger:all-srcs",
"//plugin/pkg/admission/namespace/autoprovision:all-srcs",
"//plugin/pkg/admission/namespace/exists:all-srcs",
"//plugin/pkg/admission/noderestriction:all-srcs",
"//plugin/pkg/admission/persistentvolume/label:all-srcs",
"//plugin/pkg/admission/persistentvolume/resize:all-srcs",
"//plugin/pkg/admission/persistentvolumeclaim/pvcprotection:all-srcs",
"//plugin/pkg/admission/podnodeselector:all-srcs",
"//plugin/pkg/admission/podpreset:all-srcs",
"//plugin/pkg/admission/podtolerationrestriction:all-srcs",
"//plugin/pkg/admission/priority:all-srcs",
"//plugin/pkg/admission/resourcequota:all-srcs",
"//plugin/pkg/admission/security:all-srcs",
"//plugin/pkg/admission/securitycontext/scdeny:all-srcs",
"//plugin/pkg/admission/serviceaccount:all-srcs",
"//plugin/pkg/admission/storageclass/setdefault:all-srcs",
"//plugin/pkg/auth:all-srcs",
"//plugin/pkg/scheduler:all-srcs",
],
tags = ["automanaged"],
)

12
vendor/k8s.io/kubernetes/plugin/OWNERS generated vendored Normal file
View File

@ -0,0 +1,12 @@
reviewers:
- brendandburns
- davidopp
- dchen1107
- lavalamp
- thockin
approvers:
- brendandburns
- davidopp
- dchen1107
- lavalamp
- thockin

View File

@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_binary",
"go_library",
)
load("//pkg/version:def.bzl", "version_x_defs")
go_binary(
name = "kube-scheduler",
gc_linkopts = [
"-linkmode",
"external",
"-extldflags",
"-static",
],
importpath = "k8s.io/kubernetes/plugin/cmd/kube-scheduler",
library = ":go_default_library",
x_defs = version_x_defs(),
)
go_library(
name = "go_default_library",
srcs = ["scheduler.go"],
importpath = "k8s.io/kubernetes/plugin/cmd/kube-scheduler",
deps = [
"//pkg/client/metrics/prometheus:go_default_library",
"//pkg/version/prometheus:go_default_library",
"//plugin/cmd/kube-scheduler/app:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/flag:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/logs:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//plugin/cmd/kube-scheduler/app:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -0,0 +1,4 @@
approvers:
- sig-scheduling-maintainers
reviewers:
- sig-scheduling

View File

@ -0,0 +1,66 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["server.go"],
importpath = "k8s.io/kubernetes/plugin/cmd/kube-scheduler/app",
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/componentconfig:go_default_library",
"//pkg/apis/componentconfig/v1alpha1:go_default_library",
"//pkg/client/leaderelectionconfig:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/features:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/master/ports:go_default_library",
"//pkg/util/configz:go_default_library",
"//pkg/version:go_default_library",
"//pkg/version/verflag:go_default_library",
"//plugin/pkg/scheduler:go_default_library",
"//plugin/pkg/scheduler/algorithmprovider:go_default_library",
"//plugin/pkg/scheduler/api:go_default_library",
"//plugin/pkg/scheduler/api/latest:go_default_library",
"//plugin/pkg/scheduler/factory:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/prometheus/client_golang/prometheus:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag: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/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/healthz:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/informers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/informers/storage/v1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
"//vendor/k8s.io/client-go/tools/leaderelection:go_default_library",
"//vendor/k8s.io/client-go/tools/leaderelection/resourcelock:go_default_library",
"//vendor/k8s.io/client-go/tools/record: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

@ -0,0 +1,708 @@
/*
Copyright 2014 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 app implements a Server object for running the scheduler.
package app
import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/pprof"
"os"
"reflect"
goruntime "runtime"
"strconv"
"time"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/healthz"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
coreinformers "k8s.io/client-go/informers/core/v1"
storageinformers "k8s.io/client-go/informers/storage/v1"
clientset "k8s.io/client-go/kubernetes"
v1core "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
"k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/componentconfig"
componentconfigv1alpha1 "k8s.io/kubernetes/pkg/apis/componentconfig/v1alpha1"
"k8s.io/kubernetes/pkg/client/leaderelectionconfig"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/features"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/master/ports"
"k8s.io/kubernetes/pkg/util/configz"
"k8s.io/kubernetes/pkg/version"
"k8s.io/kubernetes/pkg/version/verflag"
"k8s.io/kubernetes/plugin/pkg/scheduler"
"k8s.io/kubernetes/plugin/pkg/scheduler/algorithmprovider"
schedulerapi "k8s.io/kubernetes/plugin/pkg/scheduler/api"
latestschedulerapi "k8s.io/kubernetes/plugin/pkg/scheduler/api/latest"
"k8s.io/kubernetes/plugin/pkg/scheduler/factory"
"github.com/golang/glog"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/prometheus/client_golang/prometheus"
)
// SchedulerServer has all the context and params needed to run a Scheduler
type Options struct {
// ConfigFile is the location of the scheduler server's configuration file.
ConfigFile string
// config is the scheduler server's configuration object.
config *componentconfig.KubeSchedulerConfiguration
scheme *runtime.Scheme
codecs serializer.CodecFactory
// The fields below here are placeholders for flags that can't be directly
// mapped into componentconfig.KubeSchedulerConfiguration.
//
// TODO remove these fields once the deprecated flags are removed.
// master is the address of the Kubernetes API server (overrides any
// value in kubeconfig).
master string
healthzAddress string
healthzPort int32
policyConfigFile string
policyConfigMapName string
policyConfigMapNamespace string
useLegacyPolicyConfig bool
algorithmProvider string
}
// AddFlags adds flags for a specific SchedulerServer to the specified FlagSet
func AddFlags(options *Options, fs *pflag.FlagSet) {
fs.StringVar(&options.ConfigFile, "config", options.ConfigFile, "The path to the configuration file.")
// All flags below here are deprecated and will eventually be removed.
fs.Int32Var(&options.healthzPort, "port", ports.SchedulerPort, "The port that the scheduler's http service runs on")
fs.StringVar(&options.healthzAddress, "address", options.healthzAddress, "The IP address to serve on (set to 0.0.0.0 for all interfaces)")
fs.StringVar(&options.algorithmProvider, "algorithm-provider", options.algorithmProvider, "The scheduling algorithm provider to use, one of: "+factory.ListAlgorithmProviders())
fs.StringVar(&options.policyConfigFile, "policy-config-file", options.policyConfigFile, "File with scheduler policy configuration. This file is used if policy ConfigMap is not provided or --use-legacy-policy-config==true")
usage := fmt.Sprintf("Name of the ConfigMap object that contains scheduler's policy configuration. It must exist in the system namespace before scheduler initialization if --use-legacy-policy-config==false. The config must be provided as the value of an element in 'Data' map with the key='%v'", componentconfig.SchedulerPolicyConfigMapKey)
fs.StringVar(&options.policyConfigMapName, "policy-configmap", options.policyConfigMapName, usage)
fs.StringVar(&options.policyConfigMapNamespace, "policy-configmap-namespace", options.policyConfigMapNamespace, "The namespace where policy ConfigMap is located. The system namespace will be used if this is not provided or is empty.")
fs.BoolVar(&options.useLegacyPolicyConfig, "use-legacy-policy-config", false, "When set to true, scheduler will ignore policy ConfigMap and uses policy config file")
fs.BoolVar(&options.config.EnableProfiling, "profiling", options.config.EnableProfiling, "Enable profiling via web interface host:port/debug/pprof/")
fs.BoolVar(&options.config.EnableContentionProfiling, "contention-profiling", options.config.EnableContentionProfiling, "Enable lock contention profiling, if profiling is enabled")
fs.StringVar(&options.master, "master", options.master, "The address of the Kubernetes API server (overrides any value in kubeconfig)")
fs.StringVar(&options.config.ClientConnection.KubeConfigFile, "kubeconfig", options.config.ClientConnection.KubeConfigFile, "Path to kubeconfig file with authorization and master location information.")
fs.StringVar(&options.config.ClientConnection.ContentType, "kube-api-content-type", options.config.ClientConnection.ContentType, "Content type of requests sent to apiserver.")
fs.Float32Var(&options.config.ClientConnection.QPS, "kube-api-qps", options.config.ClientConnection.QPS, "QPS to use while talking with kubernetes apiserver")
fs.Int32Var(&options.config.ClientConnection.Burst, "kube-api-burst", options.config.ClientConnection.Burst, "Burst to use while talking with kubernetes apiserver")
fs.StringVar(&options.config.SchedulerName, "scheduler-name", options.config.SchedulerName, "Name of the scheduler, used to select which pods will be processed by this scheduler, based on pod's \"spec.SchedulerName\".")
fs.StringVar(&options.config.LeaderElection.LockObjectNamespace, "lock-object-namespace", options.config.LeaderElection.LockObjectNamespace, "Define the namespace of the lock object.")
fs.StringVar(&options.config.LeaderElection.LockObjectName, "lock-object-name", options.config.LeaderElection.LockObjectName, "Define the name of the lock object.")
fs.Int32Var(&options.config.HardPodAffinitySymmetricWeight, "hard-pod-affinity-symmetric-weight", options.config.HardPodAffinitySymmetricWeight,
"RequiredDuringScheduling affinity is not symmetric, but there is an implicit PreferredDuringScheduling affinity rule corresponding "+
"to every RequiredDuringScheduling affinity rule. --hard-pod-affinity-symmetric-weight represents the weight of implicit PreferredDuringScheduling affinity rule.")
fs.MarkDeprecated("hard-pod-affinity-symmetric-weight", "This option was moved to the policy configuration file")
fs.StringVar(&options.config.FailureDomains, "failure-domains", options.config.FailureDomains, "Indicate the \"all topologies\" set for an empty topologyKey when it's used for PreferredDuringScheduling pod anti-affinity.")
fs.MarkDeprecated("failure-domains", "Doesn't have any effect. Will be removed in future version.")
leaderelectionconfig.BindFlags(&options.config.LeaderElection.LeaderElectionConfiguration, fs)
utilfeature.DefaultFeatureGate.AddFlag(fs)
}
func NewOptions() (*Options, error) {
o := &Options{
config: new(componentconfig.KubeSchedulerConfiguration),
}
o.scheme = runtime.NewScheme()
o.codecs = serializer.NewCodecFactory(o.scheme)
if err := componentconfig.AddToScheme(o.scheme); err != nil {
return nil, err
}
if err := componentconfigv1alpha1.AddToScheme(o.scheme); err != nil {
return nil, err
}
return o, nil
}
func (o *Options) Complete() error {
if len(o.ConfigFile) == 0 {
glog.Warning("WARNING: all flags than --config are deprecated. Please begin using a config file ASAP.")
o.applyDeprecatedHealthzAddressToConfig()
o.applyDeprecatedHealthzPortToConfig()
o.applyDeprecatedAlgorithmSourceOptionsToConfig()
}
return nil
}
// applyDeprecatedHealthzAddressToConfig sets o.config.HealthzBindAddress and
// o.config.MetricsBindAddress from flags passed on the command line based on
// the following rules:
//
// 1. If --address is empty, leave the config as-is.
// 2. Otherwise, use the value of --address for the address portion of
// o.config.HealthzBindAddress
func (o *Options) applyDeprecatedHealthzAddressToConfig() {
if len(o.healthzAddress) == 0 {
return
}
_, port, err := net.SplitHostPort(o.config.HealthzBindAddress)
if err != nil {
glog.Fatalf("invalid healthz bind address %q: %v", o.config.HealthzBindAddress, err)
}
o.config.HealthzBindAddress = net.JoinHostPort(o.healthzAddress, port)
o.config.MetricsBindAddress = net.JoinHostPort(o.healthzAddress, port)
}
// applyDeprecatedHealthzPortToConfig sets o.config.HealthzBindAddress and
// o.config.MetricsBindAddress from flags passed on the command line based on
// the following rules:
//
// 1. If --port is -1, disable the healthz server.
// 2. Otherwise, use the value of --port for the port portion of
// o.config.HealthzBindAddress
func (o *Options) applyDeprecatedHealthzPortToConfig() {
if o.healthzPort == -1 {
o.config.HealthzBindAddress = ""
return
}
host, _, err := net.SplitHostPort(o.config.HealthzBindAddress)
if err != nil {
glog.Fatalf("invalid healthz bind address %q: %v", o.config.HealthzBindAddress, err)
}
o.config.HealthzBindAddress = net.JoinHostPort(host, strconv.Itoa(int(o.healthzPort)))
o.config.MetricsBindAddress = net.JoinHostPort(host, strconv.Itoa(int(o.healthzPort)))
}
// applyDeprecatedAlgorithmSourceOptionsToConfig sets o.config.AlgorithmSource from
// flags passed on the command line in the following precedence order:
//
// 1. --use-legacy-policy-config to use a policy file.
// 2. --policy-configmap to use a policy config map value.
// 3. --algorithm-provider to use a named algorithm provider.
func (o *Options) applyDeprecatedAlgorithmSourceOptionsToConfig() {
switch {
case o.useLegacyPolicyConfig:
o.config.AlgorithmSource = componentconfig.SchedulerAlgorithmSource{
Policy: &componentconfig.SchedulerPolicySource{
File: &componentconfig.SchedulerPolicyFileSource{
Path: o.policyConfigFile,
},
},
}
case len(o.policyConfigMapName) > 0:
o.config.AlgorithmSource = componentconfig.SchedulerAlgorithmSource{
Policy: &componentconfig.SchedulerPolicySource{
ConfigMap: &componentconfig.SchedulerPolicyConfigMapSource{
Name: o.policyConfigMapName,
Namespace: o.policyConfigMapNamespace,
},
},
}
case len(o.algorithmProvider) > 0:
o.config.AlgorithmSource = componentconfig.SchedulerAlgorithmSource{
Provider: &o.algorithmProvider,
}
}
}
// Validate validates all the required options.
func (o *Options) Validate(args []string) error {
if len(args) != 0 {
return errors.New("no arguments are supported")
}
return nil
}
// loadConfigFromFile loads the contents of file and decodes it as a
// KubeSchedulerConfiguration object.
func (o *Options) loadConfigFromFile(file string) (*componentconfig.KubeSchedulerConfiguration, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
return o.loadConfig(data)
}
// loadConfig decodes data as a KubeSchedulerConfiguration object.
func (o *Options) loadConfig(data []byte) (*componentconfig.KubeSchedulerConfiguration, error) {
configObj, gvk, err := o.codecs.UniversalDecoder().Decode(data, nil, nil)
if err != nil {
return nil, err
}
config, ok := configObj.(*componentconfig.KubeSchedulerConfiguration)
if !ok {
return nil, fmt.Errorf("got unexpected config type: %v", gvk)
}
return config, nil
}
func (o *Options) ApplyDefaults(in *componentconfig.KubeSchedulerConfiguration) (*componentconfig.KubeSchedulerConfiguration, error) {
external, err := o.scheme.ConvertToVersion(in, componentconfigv1alpha1.SchemeGroupVersion)
if err != nil {
return nil, err
}
o.scheme.Default(external)
internal, err := o.scheme.ConvertToVersion(external, componentconfig.SchemeGroupVersion)
if err != nil {
return nil, err
}
out := internal.(*componentconfig.KubeSchedulerConfiguration)
return out, nil
}
func (o *Options) Run() error {
config := o.config
if len(o.ConfigFile) > 0 {
if c, err := o.loadConfigFromFile(o.ConfigFile); err != nil {
return err
} else {
config = c
}
}
// Apply algorithms based on feature gates.
// TODO: make configurable?
algorithmprovider.ApplyFeatureGates()
server, err := NewSchedulerServer(config, o.master)
if err != nil {
return err
}
stop := make(chan struct{})
return server.Run(stop)
}
// NewSchedulerCommand creates a *cobra.Command object with default parameters
func NewSchedulerCommand() *cobra.Command {
opts, err := NewOptions()
if err != nil {
glog.Fatalf("unable to initialize command options: %v", err)
}
cmd := &cobra.Command{
Use: "kube-scheduler",
Long: `The Kubernetes scheduler is a policy-rich, topology-aware,
workload-specific function that significantly impacts availability, performance,
and capacity. The scheduler needs to take into account individual and collective
resource requirements, quality of service requirements, hardware/software/policy
constraints, affinity and anti-affinity specifications, data locality, inter-workload
interference, deadlines, and so on. Workload-specific requirements will be exposed
through the API as necessary.`,
Run: func(cmd *cobra.Command, args []string) {
verflag.PrintAndExitIfRequested()
cmdutil.CheckErr(opts.Complete())
cmdutil.CheckErr(opts.Validate(args))
cmdutil.CheckErr(opts.Run())
},
}
opts.config, err = opts.ApplyDefaults(opts.config)
if err != nil {
glog.Fatalf("unable to apply config defaults: %v", err)
}
flags := cmd.Flags()
AddFlags(opts, flags)
cmd.MarkFlagFilename("config", "yaml", "yml", "json")
return cmd
}
// SchedulerServer represents all the parameters required to start the
// Kubernetes scheduler server.
type SchedulerServer struct {
SchedulerName string
Client clientset.Interface
InformerFactory informers.SharedInformerFactory
PodInformer coreinformers.PodInformer
AlgorithmSource componentconfig.SchedulerAlgorithmSource
HardPodAffinitySymmetricWeight int32
EventClient v1core.EventsGetter
Recorder record.EventRecorder
Broadcaster record.EventBroadcaster
// LeaderElection is optional.
LeaderElection *leaderelection.LeaderElectionConfig
// HealthzServer is optional.
HealthzServer *http.Server
// MetricsServer is optional.
MetricsServer *http.Server
}
// NewSchedulerServer creates a runnable SchedulerServer from configuration.
func NewSchedulerServer(config *componentconfig.KubeSchedulerConfiguration, master string) (*SchedulerServer, error) {
if config == nil {
return nil, errors.New("config is required")
}
// Configz registration.
if c, err := configz.New("componentconfig"); err == nil {
c.Set(config)
} else {
return nil, fmt.Errorf("unable to register configz: %s", err)
}
// Prepare some Kube clients.
client, leaderElectionClient, eventClient, err := createClients(config.ClientConnection, master)
if err != nil {
return nil, err
}
// Prepare event clients.
eventBroadcaster := record.NewBroadcaster()
recorder := eventBroadcaster.NewRecorder(legacyscheme.Scheme, v1.EventSource{Component: config.SchedulerName})
// Set up leader election if enabled.
var leaderElectionConfig *leaderelection.LeaderElectionConfig
if config.LeaderElection.LeaderElect {
leaderElectionConfig, err = makeLeaderElectionConfig(config.LeaderElection, leaderElectionClient, recorder)
if err != nil {
return nil, err
}
}
// Prepare a healthz server. If the metrics bind address is the same as the
// healthz bind address, consolidate the servers into one.
var healthzServer *http.Server
if len(config.HealthzBindAddress) != 0 {
healthzServer = makeHealthzServer(config)
}
// Prepare a separate metrics server only if the bind address differs from the
// healthz bind address.
var metricsServer *http.Server
if len(config.MetricsBindAddress) > 0 && config.HealthzBindAddress != config.MetricsBindAddress {
metricsServer = makeMetricsServer(config)
}
return &SchedulerServer{
SchedulerName: config.SchedulerName,
Client: client,
InformerFactory: informers.NewSharedInformerFactory(client, 0),
PodInformer: factory.NewPodInformer(client, 0, config.SchedulerName),
AlgorithmSource: config.AlgorithmSource,
HardPodAffinitySymmetricWeight: config.HardPodAffinitySymmetricWeight,
EventClient: eventClient,
Recorder: recorder,
Broadcaster: eventBroadcaster,
LeaderElection: leaderElectionConfig,
HealthzServer: healthzServer,
MetricsServer: metricsServer,
}, nil
}
// makeLeaderElectionConfig builds a leader election configuration. It will
// create a new resource lock associated with the configuration.
func makeLeaderElectionConfig(config componentconfig.KubeSchedulerLeaderElectionConfiguration, client clientset.Interface, recorder record.EventRecorder) (*leaderelection.LeaderElectionConfig, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("unable to get hostname: %v", err)
}
rl, err := resourcelock.New(config.ResourceLock,
config.LockObjectNamespace,
config.LockObjectName,
client.CoreV1(),
resourcelock.ResourceLockConfig{
Identity: hostname,
EventRecorder: recorder,
})
if err != nil {
return nil, fmt.Errorf("couldn't create resource lock: %v", err)
}
return &leaderelection.LeaderElectionConfig{
Lock: rl,
LeaseDuration: config.LeaseDuration.Duration,
RenewDeadline: config.RenewDeadline.Duration,
RetryPeriod: config.RetryPeriod.Duration,
}, nil
}
// makeHealthzServer creates a healthz server from the config, and will also
// embed the metrics handler if the healthz and metrics address configurations
// are the same.
func makeHealthzServer(config *componentconfig.KubeSchedulerConfiguration) *http.Server {
mux := http.NewServeMux()
healthz.InstallHandler(mux)
if config.HealthzBindAddress == config.MetricsBindAddress {
configz.InstallHandler(mux)
mux.Handle("/metrics", prometheus.Handler())
}
if config.EnableProfiling {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
if config.EnableContentionProfiling {
goruntime.SetBlockProfileRate(1)
}
}
return &http.Server{
Addr: config.HealthzBindAddress,
Handler: mux,
}
}
// makeMetricsServer builds a metrics server from the config.
func makeMetricsServer(config *componentconfig.KubeSchedulerConfiguration) *http.Server {
mux := http.NewServeMux()
configz.InstallHandler(mux)
mux.Handle("/metrics", prometheus.Handler())
if config.EnableProfiling {
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
if config.EnableContentionProfiling {
goruntime.SetBlockProfileRate(1)
}
}
return &http.Server{
Addr: config.MetricsBindAddress,
Handler: mux,
}
}
// createClients creates a kube client and an event client from the given config and masterOverride.
// TODO remove masterOverride when CLI flags are removed.
func createClients(config componentconfig.ClientConnectionConfiguration, masterOverride string) (clientset.Interface, clientset.Interface, v1core.EventsGetter, error) {
if len(config.KubeConfigFile) == 0 && len(masterOverride) == 0 {
glog.Warningf("Neither --kubeconfig nor --master was specified. Using default API client. This might not work.")
}
// This creates a client, first loading any specified kubeconfig
// file, and then overriding the Master flag, if non-empty.
kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: config.KubeConfigFile},
&clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterOverride}}).ClientConfig()
if err != nil {
return nil, nil, nil, err
}
kubeConfig.AcceptContentTypes = config.AcceptContentTypes
kubeConfig.ContentType = config.ContentType
kubeConfig.QPS = config.QPS
//TODO make config struct use int instead of int32?
kubeConfig.Burst = int(config.Burst)
client, err := clientset.NewForConfig(restclient.AddUserAgent(kubeConfig, "scheduler"))
if err != nil {
return nil, nil, nil, err
}
leaderElectionClient, err := clientset.NewForConfig(restclient.AddUserAgent(kubeConfig, "leader-election"))
if err != nil {
return nil, nil, nil, err
}
eventClient, err := clientset.NewForConfig(kubeConfig)
if err != nil {
return nil, nil, nil, err
}
return client, leaderElectionClient, eventClient.CoreV1(), nil
}
// Run runs the SchedulerServer. This should never exit.
func (s *SchedulerServer) Run(stop chan struct{}) error {
// To help debugging, immediately log version
glog.Infof("Version: %+v", version.Get())
// Build a scheduler config from the provided algorithm source.
schedulerConfig, err := s.SchedulerConfig()
if err != nil {
return err
}
// Create the scheduler.
sched := scheduler.NewFromConfig(schedulerConfig)
// Prepare the event broadcaster.
if !reflect.ValueOf(s.Broadcaster).IsNil() && !reflect.ValueOf(s.EventClient).IsNil() {
s.Broadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: s.EventClient.Events("")})
}
// Start up the healthz server.
if s.HealthzServer != nil {
go wait.Until(func() {
glog.Infof("starting healthz server on %v", s.HealthzServer.Addr)
err := s.HealthzServer.ListenAndServe()
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to start healthz server: %v", err))
}
}, 5*time.Second, stop)
}
// Start up the metrics server.
if s.MetricsServer != nil {
go wait.Until(func() {
glog.Infof("starting metrics server on %v", s.MetricsServer.Addr)
err := s.MetricsServer.ListenAndServe()
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to start metrics server: %v", err))
}
}, 5*time.Second, stop)
}
// Start all informers.
go s.PodInformer.Informer().Run(stop)
s.InformerFactory.Start(stop)
// Wait for all caches to sync before scheduling.
s.InformerFactory.WaitForCacheSync(stop)
controller.WaitForCacheSync("scheduler", stop, s.PodInformer.Informer().HasSynced)
// Prepare a reusable run function.
run := func(stopCh <-chan struct{}) {
sched.Run()
<-stopCh
}
// If leader election is enabled, run via LeaderElector until done and exit.
if s.LeaderElection != nil {
s.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{
OnStartedLeading: run,
OnStoppedLeading: func() {
utilruntime.HandleError(fmt.Errorf("lost master"))
},
}
leaderElector, err := leaderelection.NewLeaderElector(*s.LeaderElection)
if err != nil {
return fmt.Errorf("couldn't create leader elector: %v", err)
}
leaderElector.Run()
return fmt.Errorf("lost lease")
}
// Leader election is disabled, so run inline until done.
run(stop)
return fmt.Errorf("finished without leader elect")
}
// SchedulerConfig creates the scheduler configuration. This is exposed for use
// by tests.
func (s *SchedulerServer) SchedulerConfig() (*scheduler.Config, error) {
var storageClassInformer storageinformers.StorageClassInformer
if utilfeature.DefaultFeatureGate.Enabled(features.VolumeScheduling) {
storageClassInformer = s.InformerFactory.Storage().V1().StorageClasses()
}
// Set up the configurator which can create schedulers from configs.
configurator := factory.NewConfigFactory(
s.SchedulerName,
s.Client,
s.InformerFactory.Core().V1().Nodes(),
s.PodInformer,
s.InformerFactory.Core().V1().PersistentVolumes(),
s.InformerFactory.Core().V1().PersistentVolumeClaims(),
s.InformerFactory.Core().V1().ReplicationControllers(),
s.InformerFactory.Extensions().V1beta1().ReplicaSets(),
s.InformerFactory.Apps().V1beta1().StatefulSets(),
s.InformerFactory.Core().V1().Services(),
s.InformerFactory.Policy().V1beta1().PodDisruptionBudgets(),
storageClassInformer,
s.HardPodAffinitySymmetricWeight,
utilfeature.DefaultFeatureGate.Enabled(features.EnableEquivalenceClassCache),
)
source := s.AlgorithmSource
var config *scheduler.Config
switch {
case source.Provider != nil:
// Create the config from a named algorithm provider.
sc, err := configurator.CreateFromProvider(*source.Provider)
if err != nil {
return nil, fmt.Errorf("couldn't create scheduler using provider %q: %v", *source.Provider, err)
}
config = sc
case source.Policy != nil:
// Create the config from a user specified policy source.
policy := &schedulerapi.Policy{}
switch {
case source.Policy.File != nil:
// Use a policy serialized in a file.
policyFile := source.Policy.File.Path
_, err := os.Stat(policyFile)
if err != nil {
return nil, fmt.Errorf("missing policy config file %s", policyFile)
}
data, err := ioutil.ReadFile(policyFile)
if err != nil {
return nil, fmt.Errorf("couldn't read policy config: %v", err)
}
err = runtime.DecodeInto(latestschedulerapi.Codec, []byte(data), policy)
if err != nil {
return nil, fmt.Errorf("invalid policy: %v", err)
}
case source.Policy.ConfigMap != nil:
// Use a policy serialized in a config map value.
policyRef := source.Policy.ConfigMap
policyConfigMap, err := s.Client.CoreV1().ConfigMaps(policyRef.Namespace).Get(policyRef.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("couldn't get policy config map %s/%s: %v", policyRef.Namespace, policyRef.Name, err)
}
data, found := policyConfigMap.Data[componentconfig.SchedulerPolicyConfigMapKey]
if !found {
return nil, fmt.Errorf("missing policy config map value at key %q", componentconfig.SchedulerPolicyConfigMapKey)
}
err = runtime.DecodeInto(latestschedulerapi.Codec, []byte(data), policy)
if err != nil {
return nil, fmt.Errorf("invalid policy: %v", err)
}
}
sc, err := configurator.CreateFromConfig(*policy)
if err != nil {
return nil, fmt.Errorf("couldn't create scheduler from policy: %v", err)
}
config = sc
default:
return nil, fmt.Errorf("unsupported algorithm source: %v", source)
}
// Additional tweaks to the config produced by the configurator.
config.Recorder = s.Recorder
return config, nil
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2014 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 main
import (
goflag "flag"
"os"
"github.com/spf13/pflag"
utilflag "k8s.io/apiserver/pkg/util/flag"
"k8s.io/apiserver/pkg/util/logs"
_ "k8s.io/kubernetes/pkg/client/metrics/prometheus" // for client metric registration
_ "k8s.io/kubernetes/pkg/version/prometheus" // for version metric registration
"k8s.io/kubernetes/plugin/cmd/kube-scheduler/app"
)
func main() {
command := app.NewSchedulerCommand()
// TODO: once we switch everything over to Cobra commands, we can go back to calling
// utilflag.InitFlags() (by removing its pflag.Parse() call). For now, we have to set the
// normalize func and add the go flag set by hand.
pflag.CommandLine.SetNormalizeFunc(utilflag.WordSepNormalizeFunc)
pflag.CommandLine.AddGoFlagSet(goflag.CommandLine)
// utilflag.InitFlags()
logs.InitLogs()
defer logs.FlushLogs()
if err := command.Execute(); err != nil {
os.Exit(1)
}
}

6
vendor/k8s.io/kubernetes/plugin/pkg/admission/OWNERS generated vendored Normal file
View File

@ -0,0 +1,6 @@
approvers:
- derekwaynecarr
- deads2k
reviewers:
- derekwaynecarr
- deads2k

View File

@ -0,0 +1,38 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/admit",
deps = ["//vendor/k8s.io/apiserver/pkg/admission:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/admit",
library = ":go_default_library",
deps = [
"//pkg/apis/core: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

@ -0,0 +1,58 @@
/*
Copyright 2014 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 admit
import (
"io"
"k8s.io/apiserver/pkg/admission"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("AlwaysAdmit", func(config io.Reader) (admission.Interface, error) {
return NewAlwaysAdmit(), nil
})
}
// AlwaysAdmit is an implementation of admission.Interface which always says yes to an admit request.
// It is useful in tests and when using kubernetes in an open manner.
type AlwaysAdmit struct{}
var _ admission.MutationInterface = AlwaysAdmit{}
var _ admission.ValidationInterface = AlwaysAdmit{}
// Admit makes an admission decision based on the request attributes
func (AlwaysAdmit) Admit(a admission.Attributes) (err error) {
return nil
}
// Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate.
func (AlwaysAdmit) Validate(a admission.Attributes) (err error) {
return nil
}
// Handles returns true if this admission controller can handle the given operation
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
func (AlwaysAdmit) Handles(operation admission.Operation) bool {
return true
}
// NewAlwaysAdmit creates a new always admit admission handler
func NewAlwaysAdmit() *AlwaysAdmit {
return new(AlwaysAdmit)
}

View File

@ -0,0 +1,51 @@
/*
Copyright 2014 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 admit
import (
"testing"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestAdmissionNonNilAttribute(t *testing.T) {
handler := NewAlwaysAdmit()
err := handler.Admit(admission.NewAttributesRecord(nil, nil, api.Kind("kind").WithVersion("version"), "namespace", "name", api.Resource("resource").WithVersion("version"), "subresource", admission.Create, nil))
if err != nil {
t.Errorf("Unexpected error returned from admission handler")
}
}
func TestAdmissionNilAttribute(t *testing.T) {
handler := NewAlwaysAdmit()
err := handler.Admit(nil)
if err != nil {
t.Errorf("Unexpected error returned from admission handler")
}
}
func TestHandles(t *testing.T) {
handler := NewAlwaysAdmit()
tests := []admission.Operation{admission.Create, admission.Connect, admission.Update, admission.Delete}
for _, test := range tests {
if !handler.Handles(test) {
t.Errorf("Expected handling all operations, including: %v", test)
}
}
}

View File

@ -0,0 +1,45 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages",
deps = [
"//pkg/apis/core:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages",
library = ":go_default_library",
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/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

@ -0,0 +1,121 @@
/*
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 alwayspullimages contains an admission controller that modifies every new Pod to force
// the image pull policy to Always. This is useful in a multitenant cluster so that users can be
// assured that their private images can only be used by those who have the credentials to pull
// them. Without this admission controller, once an image has been pulled to a node, any pod from
// any user can use it simply by knowing the image's name (assuming the Pod is scheduled onto the
// right node), without any authorization check against the image. With this admission controller
// enabled, images are always pulled prior to starting containers, which means valid credentials are
// required.
package alwayspullimages
import (
"io"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("AlwaysPullImages", func(config io.Reader) (admission.Interface, error) {
return NewAlwaysPullImages(), nil
})
}
// AlwaysPullImages is an implementation of admission.Interface.
// It looks at all new pods and overrides each container's image pull policy to Always.
type AlwaysPullImages struct {
*admission.Handler
}
var _ admission.MutationInterface = &AlwaysPullImages{}
var _ admission.ValidationInterface = &AlwaysPullImages{}
// Admit makes an admission decision based on the request attributes
func (a *AlwaysPullImages) Admit(attributes admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
if shouldIgnore(attributes) {
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
for i := range pod.Spec.InitContainers {
pod.Spec.InitContainers[i].ImagePullPolicy = api.PullAlways
}
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].ImagePullPolicy = api.PullAlways
}
return nil
}
// Validate makes sure that all containers are set to always pull images
func (*AlwaysPullImages) Validate(attributes admission.Attributes) (err error) {
if shouldIgnore(attributes) {
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
for i := range pod.Spec.InitContainers {
if pod.Spec.InitContainers[i].ImagePullPolicy != api.PullAlways {
return admission.NewForbidden(attributes,
field.NotSupported(field.NewPath("spec", "initContainers").Index(i).Child("imagePullPolicy"),
pod.Spec.InitContainers[i].ImagePullPolicy, []string{string(api.PullAlways)},
),
)
}
}
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].ImagePullPolicy != api.PullAlways {
return admission.NewForbidden(attributes,
field.NotSupported(field.NewPath("spec", "containers").Index(i).Child("imagePullPolicy"),
pod.Spec.Containers[i].ImagePullPolicy, []string{string(api.PullAlways)},
),
)
}
}
return nil
}
func shouldIgnore(attributes admission.Attributes) bool {
// Ignore all calls to subresources or resources other than pods.
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != api.Resource("pods") {
return true
}
return false
}
// NewAlwaysPullImages creates a new always pull images admission control handler
func NewAlwaysPullImages() *AlwaysPullImages {
return &AlwaysPullImages{
Handler: admission.NewHandler(admission.Create, admission.Update),
}
}

View File

@ -0,0 +1,160 @@
/*
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 alwayspullimages
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
// TestAdmission verifies all create requests for pods result in every container's image pull policy
// set to Always
func TestAdmission(t *testing.T) {
namespace := "test"
handler := &AlwaysPullImages{}
pod := api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
Spec: api.PodSpec{
InitContainers: []api.Container{
{Name: "init1", Image: "image"},
{Name: "init2", Image: "image", ImagePullPolicy: api.PullNever},
{Name: "init3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
{Name: "init4", Image: "image", ImagePullPolicy: api.PullAlways},
},
Containers: []api.Container{
{Name: "ctr1", Image: "image"},
{Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever},
{Name: "ctr3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
{Name: "ctr4", Image: "image", ImagePullPolicy: api.PullAlways},
},
},
}
err := handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("Unexpected error returned from admission handler")
}
for _, c := range pod.Spec.InitContainers {
if c.ImagePullPolicy != api.PullAlways {
t.Errorf("Container %v: expected pull always, got %v", c, c.ImagePullPolicy)
}
}
for _, c := range pod.Spec.Containers {
if c.ImagePullPolicy != api.PullAlways {
t.Errorf("Container %v: expected pull always, got %v", c, c.ImagePullPolicy)
}
}
}
func TestValidate(t *testing.T) {
namespace := "test"
handler := &AlwaysPullImages{}
pod := api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
Spec: api.PodSpec{
InitContainers: []api.Container{
{Name: "init1", Image: "image"},
{Name: "init2", Image: "image", ImagePullPolicy: api.PullNever},
{Name: "init3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
{Name: "init4", Image: "image", ImagePullPolicy: api.PullAlways},
},
Containers: []api.Container{
{Name: "ctr1", Image: "image"},
{Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever},
{Name: "ctr3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
{Name: "ctr4", Image: "image", ImagePullPolicy: api.PullAlways},
},
},
}
expectedError := `pods "123" is forbidden: spec.initContainers[0].imagePullPolicy: Unsupported value: "": supported values: "Always"`
err := handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err == nil {
t.Fatal("missing expected error")
}
if err.Error() != expectedError {
t.Fatal(err)
}
}
// TestOtherResources ensures that this admission controller is a no-op for other resources,
// subresources, and non-pods.
func TestOtherResources(t *testing.T) {
namespace := "testnamespace"
name := "testname"
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
Spec: api.PodSpec{
Containers: []api.Container{
{Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever},
},
},
}
tests := []struct {
name string
kind string
resource string
subresource string
object runtime.Object
expectError bool
}{
{
name: "non-pod resource",
kind: "Foo",
resource: "foos",
object: pod,
},
{
name: "pod subresource",
kind: "Pod",
resource: "pods",
subresource: "exec",
object: pod,
},
{
name: "non-pod object",
kind: "Pod",
resource: "pods",
object: &api.Service{},
expectError: true,
},
}
for _, tc := range tests {
handler := &AlwaysPullImages{}
err := handler.Admit(admission.NewAttributesRecord(tc.object, nil, api.Kind(tc.kind).WithVersion("version"), namespace, name, api.Resource(tc.resource).WithVersion("version"), tc.subresource, admission.Create, nil))
if tc.expectError {
if err == nil {
t.Errorf("%s: unexpected nil error", tc.name)
}
continue
}
if err != nil {
t.Errorf("%s: unexpected error: %v", tc.name, err)
continue
}
if e, a := api.PullNever, pod.Spec.Containers[0].ImagePullPolicy; e != a {
t.Errorf("%s: image pull policy was changed to %s", tc.name, a)
}
}
}

View File

@ -0,0 +1,49 @@
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",
"doc.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/antiaffinity",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/kubelet/apis:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/antiaffinity",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/kubelet/apis: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/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

@ -0,0 +1,78 @@
/*
Copyright 2016 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 antiaffinity
import (
"fmt"
"io"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("LimitPodHardAntiAffinityTopology", func(config io.Reader) (admission.Interface, error) {
return NewInterPodAntiAffinity(), nil
})
}
// Plugin contains the client used by the admission controller
type Plugin struct {
*admission.Handler
}
var _ admission.ValidationInterface = &Plugin{}
// NewInterPodAntiAffinity creates a new instance of the LimitPodHardAntiAffinityTopology admission controller
func NewInterPodAntiAffinity() *Plugin {
return &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
}
}
// Validate will deny any pod that defines AntiAffinity topology key other than kubeletapis.LabelHostname i.e. "kubernetes.io/hostname"
// in requiredDuringSchedulingRequiredDuringExecution and requiredDuringSchedulingIgnoredDuringExecution.
func (p *Plugin) Validate(attributes admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != api.Resource("pods") {
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
affinity := pod.Spec.Affinity
if affinity != nil && affinity.PodAntiAffinity != nil {
var podAntiAffinityTerms []api.PodAffinityTerm
if len(affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) != 0 {
podAntiAffinityTerms = affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution
}
// TODO: Uncomment this block when implement RequiredDuringSchedulingRequiredDuringExecution.
//if len(affinity.PodAntiAffinity.RequiredDuringSchedulingRequiredDuringExecution) != 0 {
// podAntiAffinityTerms = append(podAntiAffinityTerms, affinity.PodAntiAffinity.RequiredDuringSchedulingRequiredDuringExecution...)
//}
for _, v := range podAntiAffinityTerms {
if v.TopologyKey != kubeletapis.LabelHostname {
return apierrors.NewForbidden(attributes.GetResource().GroupResource(), pod.Name, fmt.Errorf("affinity.PodAntiAffinity.RequiredDuringScheduling has TopologyKey %v but only key %v is allowed", v.TopologyKey, kubeletapis.LabelHostname))
}
}
}
return nil
}

View File

@ -0,0 +1,284 @@
/*
Copyright 2016 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 antiaffinity
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
)
// ensures the hard PodAntiAffinity is denied if it defines TopologyKey other than kubernetes.io/hostname.
// TODO: Add test case "invalid topologyKey in requiredDuringSchedulingRequiredDuringExecution then admission fails"
// after RequiredDuringSchedulingRequiredDuringExecution is implemented.
func TestInterPodAffinityAdmission(t *testing.T) {
handler := NewInterPodAntiAffinity()
pod := api.Pod{
Spec: api.PodSpec{},
}
tests := []struct {
affinity *api.Affinity
errorExpected bool
}{
// empty affinity its success.
{
affinity: &api.Affinity{},
errorExpected: false,
},
// what ever topologyKey in preferredDuringSchedulingIgnoredDuringExecution, the admission should success.
{
affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
{
Weight: 5,
PodAffinityTerm: api.PodAffinityTerm{
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: "az",
},
},
},
},
},
errorExpected: false,
},
// valid topologyKey in requiredDuringSchedulingIgnoredDuringExecution,
// plus any topologyKey in preferredDuringSchedulingIgnoredDuringExecution, then admission success.
{
affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
{
Weight: 5,
PodAffinityTerm: api.PodAffinityTerm{
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: "az",
},
},
},
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: kubeletapis.LabelHostname,
},
},
},
},
errorExpected: false,
},
// valid topologyKey in requiredDuringSchedulingIgnoredDuringExecution then admission success.
{
affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: kubeletapis.LabelHostname,
},
},
},
},
errorExpected: false,
},
// invalid topologyKey in requiredDuringSchedulingIgnoredDuringExecution then admission fails.
{
affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: " zone ",
},
},
},
},
errorExpected: true,
},
// list of requiredDuringSchedulingIgnoredDuringExecution middle element topologyKey is not valid.
{
affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: kubeletapis.LabelHostname,
}, {
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: " zone ",
}, {
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "security",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"S2"},
},
},
},
TopologyKey: kubeletapis.LabelHostname,
},
},
},
},
errorExpected: true,
},
}
for _, test := range tests {
pod.Spec.Affinity = test.affinity
err := handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), "foo", "name", api.Resource("pods").WithVersion("version"), "", "ignored", nil))
if test.errorExpected && err == nil {
t.Errorf("Expected error for Anti Affinity %+v but did not get an error", test.affinity)
}
if !test.errorExpected && err != nil {
t.Errorf("Unexpected error %v for AntiAffinity %+v", err, test.affinity)
}
}
}
func TestHandles(t *testing.T) {
handler := NewInterPodAntiAffinity()
tests := map[admission.Operation]bool{
admission.Update: true,
admission.Create: true,
admission.Delete: false,
admission.Connect: false,
}
for op, expected := range tests {
result := handler.Handles(op)
if result != expected {
t.Errorf("Unexpected result for operation %s: %v\n", op, result)
}
}
}
// TestOtherResources ensures that this admission controller is a no-op for other resources,
// subresources, and non-pods.
func TestOtherResources(t *testing.T) {
namespace := "testnamespace"
name := "testname"
pod := &api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
}
tests := []struct {
name string
kind string
resource string
subresource string
object runtime.Object
expectError bool
}{
{
name: "non-pod resource",
kind: "Foo",
resource: "foos",
object: pod,
},
{
name: "pod subresource",
kind: "Pod",
resource: "pods",
subresource: "eviction",
object: pod,
},
{
name: "non-pod object",
kind: "Pod",
resource: "pods",
object: &api.Service{},
expectError: true,
},
}
for _, tc := range tests {
handler := &Plugin{}
err := handler.Validate(admission.NewAttributesRecord(tc.object, nil, api.Kind(tc.kind).WithVersion("version"), namespace, name, api.Resource(tc.resource).WithVersion("version"), tc.subresource, admission.Create, nil))
if tc.expectError {
if err == nil {
t.Errorf("%s: unexpected nil error", tc.name)
}
continue
}
if err != nil {
t.Errorf("%s: unexpected error: %v", tc.name, err)
continue
}
}
}

View File

@ -0,0 +1,28 @@
/*
Copyright 2016 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.
*/
// LimitPodHardAntiAffinityTopology admission controller rejects any pod
// that specifies "hard" (RequiredDuringScheduling) anti-affinity
// with a TopologyKey other than kubeletapis.LabelHostname.
// Because anti-affinity is symmetric, without this admission controller,
// a user could maliciously or accidentally specify that their pod (once it has scheduled)
// should block other pods from scheduling into the same zone or some other large topology,
// essentially DoSing the cluster.
// In the future we will address this problem more fully by using quota and priority,
// but for now this admission controller provides a simple protection,
// on the assumption that the only legitimate use of hard pod anti-affinity
// is to exclude other pods from the same node.
package antiaffinity // import "k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"

View File

@ -0,0 +1,46 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//plugin/pkg/scheduler/algorithm:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["admission.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//plugin/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",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,122 @@
/*
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 defaulttolerationseconds
import (
"flag"
"fmt"
"io"
"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/plugin/pkg/scheduler/algorithm"
)
var (
defaultNotReadyTolerationSeconds = flag.Int64("default-not-ready-toleration-seconds", 300,
"Indicates the tolerationSeconds of the toleration for notReady:NoExecute"+
" that is added by default to every pod that does not already have such a toleration.")
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.")
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("DefaultTolerationSeconds", func(config io.Reader) (admission.Interface, error) {
return NewDefaultTolerationSeconds(), nil
})
}
// Plugin contains the client used by the admission controller
// It will add default tolerations for every pod
// that tolerate taints `notReady:NoExecute` and `unreachable:NoExecute`,
// with tolerationSeconds of 300s.
// If the pod already specifies a toleration for taint `notReady:NoExecute`
// or `unreachable:NoExecute`, the plugin won't touch it.
type Plugin struct {
*admission.Handler
}
var _ admission.MutationInterface = &Plugin{}
// NewDefaultTolerationSeconds creates a new instance of the DefaultTolerationSeconds admission controller
func NewDefaultTolerationSeconds() *Plugin {
return &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
}
}
// Admit makes an admission decision based on the request attributes
func (p *Plugin) Admit(attributes admission.Attributes) (err error) {
if attributes.GetResource().GroupResource() != api.Resource("pods") {
return nil
}
if len(attributes.GetSubresource()) > 0 {
// only run the checks below on pods proper and not subresources
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return errors.NewBadRequest(fmt.Sprintf("expected *api.Pod but got %T", attributes.GetObject()))
}
tolerations := pod.Spec.Tolerations
toleratesNodeNotReady := false
toleratesNodeUnreachable := false
for _, toleration := range tolerations {
if (toleration.Key == algorithm.TaintNodeNotReady || len(toleration.Key) == 0) &&
(toleration.Effect == api.TaintEffectNoExecute || len(toleration.Effect) == 0) {
toleratesNodeNotReady = true
}
if (toleration.Key == algorithm.TaintNodeUnreachable || len(toleration.Key) == 0) &&
(toleration.Effect == api.TaintEffectNoExecute || len(toleration.Effect) == 0) {
toleratesNodeUnreachable = true
}
}
// 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,
})
}
if !toleratesNodeUnreachable {
helper.AddOrUpdateTolerationInPod(pod, &api.Toleration{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: defaultUnreachableTolerationSeconds,
})
}
return nil
}

View File

@ -0,0 +1,423 @@
/*
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 defaulttolerationseconds
import (
"testing"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper"
"k8s.io/kubernetes/plugin/pkg/scheduler/algorithm"
)
func TestForgivenessAdmission(t *testing.T) {
var defaultTolerationSeconds int64 = 300
genTolerationSeconds := func(s int64) *int64 {
return &s
}
handler := NewDefaultTolerationSeconds()
// NOTE: for anyone who want to modify this test, the order of tolerations matters!
tests := []struct {
description string
requestedPod api.Pod
expectedPod api.Pod
}{
{
description: "pod has no tolerations, expect add tolerations for `not-ready:NoExecute` and `unreachable:NoExecute`",
requestedPod: api.Pod{
Spec: api.PodSpec{},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod has alpha tolerations, expect add tolerations for `not-ready:NoExecute` and `unreachable:NoExecute`" +
", the alpha tolerations will not be touched",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.DeprecatedTaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.DeprecatedTaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.DeprecatedTaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.DeprecatedTaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod has alpha not-ready toleration, expect add tolerations for `not-ready:NoExecute` and `unreachable:NoExecute`" +
", the alpha tolerations will not be touched",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.DeprecatedTaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.DeprecatedTaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod has alpha unreachable toleration, expect add tolerations for `not-ready:NoExecute` and `unreachable:NoExecute`" +
", the alpha tolerations will not be touched",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.DeprecatedTaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.DeprecatedTaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod has tolerations, but none is for taint `not-ready:NoExecute` or `unreachable:NoExecute`, expect add tolerations for `not-ready:NoExecute` and `unreachable:NoExecute`",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: "foo",
Operator: api.TolerationOpEqual,
Value: "bar",
Effect: api.TaintEffectNoSchedule,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: "foo",
Operator: api.TolerationOpEqual,
Value: "bar",
Effect: api.TaintEffectNoSchedule,
TolerationSeconds: genTolerationSeconds(700),
},
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod specified a toleration for taint `not-ready:NoExecute`, expect add toleration for `unreachable:NoExecute`",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod specified a toleration for taint `unreachable:NoExecute`, expect add toleration for `not-ready:NoExecute`",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: &defaultTolerationSeconds,
},
},
},
},
},
{
description: "pod specified tolerations for both `not-ready:NoExecute` and `unreachable:NoExecute`, expect no change",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
},
{
description: "pod specified toleration for taint `unreachable`, expect add toleration for `not-ready:NoExecute`",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Key: algorithm.TaintNodeUnreachable,
Operator: api.TolerationOpExists,
TolerationSeconds: genTolerationSeconds(700),
},
{
Key: algorithm.TaintNodeNotReady,
Operator: api.TolerationOpExists,
Effect: api.TaintEffectNoExecute,
TolerationSeconds: genTolerationSeconds(300),
},
},
},
},
},
{
description: "pod has wildcard toleration for all kind of taints, expect no change",
requestedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{Operator: api.TolerationOpExists, TolerationSeconds: genTolerationSeconds(700)},
},
},
},
expectedPod: api.Pod{
Spec: api.PodSpec{
Tolerations: []api.Toleration{
{
Operator: api.TolerationOpExists,
TolerationSeconds: genTolerationSeconds(700),
},
},
},
},
},
}
for _, test := range tests {
err := handler.Admit(admission.NewAttributesRecord(&test.requestedPod, nil, api.Kind("Pod").WithVersion("version"), "foo", "name", api.Resource("pods").WithVersion("version"), "", "ignored", nil))
if err != nil {
t.Errorf("[%s]: unexpected error %v for pod %+v", test.description, err, test.requestedPod)
}
if !helper.Semantic.DeepEqual(test.expectedPod.Spec.Tolerations, test.requestedPod.Spec.Tolerations) {
t.Errorf("[%s]: expected %#v got %#v", test.description, test.expectedPod.Spec.Tolerations, test.requestedPod.Spec.Tolerations)
}
}
}
func TestHandles(t *testing.T) {
handler := NewDefaultTolerationSeconds()
tests := map[admission.Operation]bool{
admission.Update: true,
admission.Create: true,
admission.Delete: false,
admission.Connect: false,
}
for op, expected := range tests {
result := handler.Handles(op)
if result != expected {
t.Errorf("Unexpected result for operation %s: %v\n", op, result)
}
}
}

View File

@ -0,0 +1,38 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/deny",
deps = ["//vendor/k8s.io/apiserver/pkg/admission:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/deny",
library = ":go_default_library",
deps = [
"//pkg/apis/core: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

@ -0,0 +1,59 @@
/*
Copyright 2014 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 deny
import (
"errors"
"io"
"k8s.io/apiserver/pkg/admission"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("AlwaysDeny", func(config io.Reader) (admission.Interface, error) {
return NewAlwaysDeny(), nil
})
}
// AlwaysDeny is an implementation of admission.Interface which always says no to an admission request.
// It is useful in unit tests to force an operation to be forbidden.
type AlwaysDeny struct{}
var _ admission.MutationInterface = AlwaysDeny{}
var _ admission.ValidationInterface = AlwaysDeny{}
// Admit makes an admission decision based on the request attributes.
func (AlwaysDeny) Admit(a admission.Attributes) (err error) {
return admission.NewForbidden(a, errors.New("Admission control is denying all modifications"))
}
// Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate.
func (AlwaysDeny) Validate(a admission.Attributes) (err error) {
return admission.NewForbidden(a, errors.New("Admission control is denying all modifications"))
}
// Handles returns true if this admission controller can handle the given operation
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
func (AlwaysDeny) Handles(operation admission.Operation) bool {
return true
}
// NewAlwaysDeny creates an always deny admission handler
func NewAlwaysDeny() *AlwaysDeny {
return new(AlwaysDeny)
}

View File

@ -0,0 +1,43 @@
/*
Copyright 2014 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 deny
import (
"testing"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
func TestAdmission(t *testing.T) {
handler := NewAlwaysDeny()
err := handler.Admit(admission.NewAttributesRecord(nil, nil, api.Kind("kind").WithVersion("version"), "namespace", "name", api.Resource("resource").WithVersion("version"), "subresource", admission.Create, nil))
if err == nil {
t.Error("Expected error returned from admission handler")
}
}
func TestHandles(t *testing.T) {
handler := NewAlwaysDeny()
tests := []admission.Operation{admission.Create, admission.Connect, admission.Update, admission.Delete}
for _, test := range tests {
if !handler.Handles(test) {
t.Errorf("Expected handling all operations, including: %v", test)
}
}
}

View File

@ -0,0 +1,72 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"cache_test.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit: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/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/clock: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/client-go/util/flowcontrol:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"cache.go",
"clock.go",
"config.go",
"doc.go",
"limitenforcer.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit",
deps = [
"//pkg/apis/core:go_default_library",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/install:go_default_library",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1:go_default_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",
"//vendor/k8s.io/client-go/util/flowcontrol:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -0,0 +1,94 @@
/*
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 eventratelimit
import (
"io"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/util/flowcontrol"
api "k8s.io/kubernetes/pkg/apis/core"
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("EventRateLimit",
func(config io.Reader) (admission.Interface, error) {
// load the configuration provided (if any)
configuration, err := LoadConfiguration(config)
if err != nil {
return nil, err
}
// validate the configuration (if any)
if configuration != nil {
if errs := validation.ValidateConfiguration(configuration); len(errs) != 0 {
return nil, errs.ToAggregate()
}
}
return newEventRateLimit(configuration, realClock{})
})
}
// Plugin implements an admission controller that can enforce event rate limits
type Plugin struct {
*admission.Handler
// limitEnforcers is the collection of limit enforcers. There is one limit enforcer for each
// active limit type. As there are 4 limit types, the length of the array will be at most 4.
// The array is read-only after construction.
limitEnforcers []*limitEnforcer
}
var _ admission.ValidationInterface = &Plugin{}
// newEventRateLimit configures an admission controller that can enforce event rate limits
func newEventRateLimit(config *eventratelimitapi.Configuration, clock flowcontrol.Clock) (*Plugin, error) {
limitEnforcers := make([]*limitEnforcer, 0, len(config.Limits))
for _, limitConfig := range config.Limits {
enforcer, err := newLimitEnforcer(limitConfig, clock)
if err != nil {
return nil, err
}
limitEnforcers = append(limitEnforcers, enforcer)
}
eventRateLimitAdmission := &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
limitEnforcers: limitEnforcers,
}
return eventRateLimitAdmission, nil
}
// Validate makes admission decisions while enforcing event rate limits
func (a *Plugin) Validate(attr admission.Attributes) (err error) {
// ignore all operations that do not correspond to an Event kind
if attr.GetKind().GroupKind() != api.Kind("Event") {
return nil
}
var rejectionError error
// give each limit enforcer a chance to reject the event
for _, enforcer := range a.limitEnforcers {
if err := enforcer.accept(attr); err != nil {
rejectionError = err
}
}
return rejectionError
}

View File

@ -0,0 +1,502 @@
/*
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 eventratelimit
import (
"testing"
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
api "k8s.io/kubernetes/pkg/apis/core"
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
)
const (
qps = 1
eventKind = "Event"
nonEventKind = "NonEvent"
)
// attributesForRequest generates the admission.Attributes that for the specified request
func attributesForRequest(rq request) admission.Attributes {
return admission.NewAttributesRecord(
rq.event,
nil,
api.Kind(rq.kind).WithVersion("version"),
rq.namespace,
"name",
api.Resource("resource").WithVersion("version"),
"",
admission.Create,
&user.DefaultInfo{Name: rq.username})
}
type request struct {
kind string
namespace string
username string
event *api.Event
delay time.Duration
accepted bool
}
func newRequest(kind string) request {
return request{
kind: kind,
accepted: true,
}
}
func newEventRequest() request {
return newRequest(eventKind)
}
func newNonEventRequest() request {
return newRequest(nonEventKind)
}
func (r request) withNamespace(namespace string) request {
r.namespace = namespace
return r
}
func (r request) withEvent(event *api.Event) request {
r.event = event
return r
}
func (r request) withEventComponent(component string) request {
return r.withEvent(&api.Event{
Source: api.EventSource{
Component: component,
},
})
}
func (r request) withUser(name string) request {
r.username = name
return r
}
func (r request) blocked() request {
r.accepted = false
return r
}
// withDelay will adjust the clock to simulate the specified delay, in seconds
func (r request) withDelay(delayInSeconds int) request {
r.delay = time.Duration(delayInSeconds) * time.Second
return r
}
// createSourceAndObjectKeyInclusionRequests creates a series of requests that can be used
// to test that a particular part of the event is included in the source+object key
func createSourceAndObjectKeyInclusionRequests(eventFactory func(label string) *api.Event) []request {
return []request{
newEventRequest().withEvent(eventFactory("A")),
newEventRequest().withEvent(eventFactory("A")).blocked(),
newEventRequest().withEvent(eventFactory("B")),
}
}
func TestEventRateLimiting(t *testing.T) {
cases := []struct {
name string
serverBurst int32
namespaceBurst int32
namespaceCacheSize int32
sourceAndObjectBurst int32
sourceAndObjectCacheSize int32
userBurst int32
userCacheSize int32
requests []request
}{
{
name: "event not blocked when tokens available",
serverBurst: 3,
requests: []request{
newEventRequest(),
},
},
{
name: "non-event not blocked",
serverBurst: 3,
requests: []request{
newNonEventRequest(),
},
},
{
name: "event blocked after tokens exhausted",
serverBurst: 3,
requests: []request{
newEventRequest(),
newEventRequest(),
newEventRequest(),
newEventRequest().blocked(),
},
},
{
name: "non-event not blocked after tokens exhausted",
serverBurst: 3,
requests: []request{
newEventRequest(),
newEventRequest(),
newEventRequest(),
newNonEventRequest(),
},
},
{
name: "non-events should not count against limit",
serverBurst: 3,
requests: []request{
newEventRequest(),
newEventRequest(),
newNonEventRequest(),
newEventRequest(),
},
},
{
name: "event accepted after token refill",
serverBurst: 3,
requests: []request{
newEventRequest(),
newEventRequest(),
newEventRequest(),
newEventRequest().blocked(),
newEventRequest().withDelay(1),
},
},
{
name: "event blocked by namespace limits",
serverBurst: 100,
namespaceBurst: 3,
namespaceCacheSize: 10,
requests: []request{
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A").blocked(),
},
},
{
name: "event from other namespace not blocked",
serverBurst: 100,
namespaceBurst: 3,
namespaceCacheSize: 10,
requests: []request{
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("B"),
},
},
{
name: "events from other namespaces should not count against limit",
serverBurst: 100,
namespaceBurst: 3,
namespaceCacheSize: 10,
requests: []request{
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("B"),
newEventRequest().withNamespace("A"),
},
},
{
name: "event accepted after namespace token refill",
serverBurst: 100,
namespaceBurst: 3,
namespaceCacheSize: 10,
requests: []request{
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A").blocked(),
newEventRequest().withNamespace("A").withDelay(1),
},
},
{
name: "event from other namespaces should not clear namespace limits",
serverBurst: 100,
namespaceBurst: 3,
namespaceCacheSize: 10,
requests: []request{
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("B"),
newEventRequest().withNamespace("A").blocked(),
},
},
{
name: "namespace limits from lru namespace should clear when cache size exceeded",
serverBurst: 100,
namespaceBurst: 3,
namespaceCacheSize: 2,
requests: []request{
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("B"),
newEventRequest().withNamespace("B"),
newEventRequest().withNamespace("B"),
newEventRequest().withNamespace("A"),
newEventRequest().withNamespace("B").blocked(),
newEventRequest().withNamespace("A").blocked(),
// This should clear out namespace B from the lru cache
newEventRequest().withNamespace("C"),
newEventRequest().withNamespace("A").blocked(),
newEventRequest().withNamespace("B"),
},
},
{
name: "event blocked by source+object limits",
serverBurst: 100,
sourceAndObjectBurst: 3,
sourceAndObjectCacheSize: 10,
requests: []request{
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A").blocked(),
},
},
{
name: "event from other source+object not blocked",
serverBurst: 100,
sourceAndObjectBurst: 3,
sourceAndObjectCacheSize: 10,
requests: []request{
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("B"),
},
},
{
name: "events from other source+object should not count against limit",
serverBurst: 100,
sourceAndObjectBurst: 3,
sourceAndObjectCacheSize: 10,
requests: []request{
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("B"),
newEventRequest().withEventComponent("A"),
},
},
{
name: "event accepted after source+object token refill",
serverBurst: 100,
sourceAndObjectBurst: 3,
sourceAndObjectCacheSize: 10,
requests: []request{
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A").blocked(),
newEventRequest().withEventComponent("A").withDelay(1),
},
},
{
name: "event from other source+object should not clear source+object limits",
serverBurst: 100,
sourceAndObjectBurst: 3,
sourceAndObjectCacheSize: 10,
requests: []request{
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("B"),
newEventRequest().withEventComponent("A").blocked(),
},
},
{
name: "source+object limits from lru source+object should clear when cache size exceeded",
serverBurst: 100,
sourceAndObjectBurst: 3,
sourceAndObjectCacheSize: 2,
requests: []request{
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("B"),
newEventRequest().withEventComponent("B"),
newEventRequest().withEventComponent("B"),
newEventRequest().withEventComponent("A"),
newEventRequest().withEventComponent("B").blocked(),
newEventRequest().withEventComponent("A").blocked(),
// This should clear out component B from the lru cache
newEventRequest().withEventComponent("C"),
newEventRequest().withEventComponent("A").blocked(),
newEventRequest().withEventComponent("B"),
},
},
{
name: "source host should be included in source+object key",
serverBurst: 100,
sourceAndObjectBurst: 1,
sourceAndObjectCacheSize: 10,
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
return &api.Event{Source: api.EventSource{Host: label}}
}),
},
{
name: "involved object kind should be included in source+object key",
serverBurst: 100,
sourceAndObjectBurst: 1,
sourceAndObjectCacheSize: 10,
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
return &api.Event{InvolvedObject: api.ObjectReference{Kind: label}}
}),
},
{
name: "involved object namespace should be included in source+object key",
serverBurst: 100,
sourceAndObjectBurst: 1,
sourceAndObjectCacheSize: 10,
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
return &api.Event{InvolvedObject: api.ObjectReference{Namespace: label}}
}),
},
{
name: "involved object name should be included in source+object key",
serverBurst: 100,
sourceAndObjectBurst: 1,
sourceAndObjectCacheSize: 10,
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
return &api.Event{InvolvedObject: api.ObjectReference{Name: label}}
}),
},
{
name: "involved object UID should be included in source+object key",
serverBurst: 100,
sourceAndObjectBurst: 1,
sourceAndObjectCacheSize: 10,
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
return &api.Event{InvolvedObject: api.ObjectReference{UID: types.UID(label)}}
}),
},
{
name: "involved object APIVersion should be included in source+object key",
serverBurst: 100,
sourceAndObjectBurst: 1,
sourceAndObjectCacheSize: 10,
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
return &api.Event{InvolvedObject: api.ObjectReference{APIVersion: label}}
}),
},
{
name: "event blocked by user limits",
userBurst: 3,
userCacheSize: 10,
requests: []request{
newEventRequest().withUser("A"),
newEventRequest().withUser("A"),
newEventRequest().withUser("A"),
newEventRequest().withUser("A").blocked(),
},
},
{
name: "event from other user not blocked",
requests: []request{
newEventRequest().withUser("A"),
newEventRequest().withUser("A"),
newEventRequest().withUser("A"),
newEventRequest().withUser("B"),
},
},
{
name: "events from other user should not count against limit",
requests: []request{
newEventRequest().withUser("A"),
newEventRequest().withUser("A"),
newEventRequest().withUser("B"),
newEventRequest().withUser("A"),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
clock := clock.NewFakeClock(time.Now())
config := &eventratelimitapi.Configuration{}
if tc.serverBurst > 0 {
serverLimit := eventratelimitapi.Limit{
Type: eventratelimitapi.ServerLimitType,
QPS: qps,
Burst: tc.serverBurst,
}
config.Limits = append(config.Limits, serverLimit)
}
if tc.namespaceBurst > 0 {
namespaceLimit := eventratelimitapi.Limit{
Type: eventratelimitapi.NamespaceLimitType,
Burst: tc.namespaceBurst,
QPS: qps,
CacheSize: tc.namespaceCacheSize,
}
config.Limits = append(config.Limits, namespaceLimit)
}
if tc.userBurst > 0 {
userLimit := eventratelimitapi.Limit{
Type: eventratelimitapi.UserLimitType,
Burst: tc.userBurst,
QPS: qps,
CacheSize: tc.userCacheSize,
}
config.Limits = append(config.Limits, userLimit)
}
if tc.sourceAndObjectBurst > 0 {
sourceAndObjectLimit := eventratelimitapi.Limit{
Type: eventratelimitapi.SourceAndObjectLimitType,
Burst: tc.sourceAndObjectBurst,
QPS: qps,
CacheSize: tc.sourceAndObjectCacheSize,
}
config.Limits = append(config.Limits, sourceAndObjectLimit)
}
eventratelimit, err := newEventRateLimit(config, clock)
if err != nil {
t.Fatalf("%v: Could not create EventRateLimit: %v", tc.name, err)
}
for rqIndex, rq := range tc.requests {
if rq.delay > 0 {
clock.Step(rq.delay)
}
attributes := attributesForRequest(rq)
err = eventratelimit.Validate(attributes)
if rq.accepted != (err == nil) {
expectedAction := "admitted"
if !rq.accepted {
expectedAction = "blocked"
}
t.Fatalf("%v: Request %v should have been %v: %v", tc.name, rqIndex, expectedAction, err)
}
if err != nil {
statusErr, ok := err.(*errors.StatusError)
if ok && statusErr.ErrStatus.Code != errors.StatusTooManyRequests {
t.Fatalf("%v: Request %v should yield a 429 response: %v", tc.name, rqIndex, err)
}
}
}
})
}
}

View File

@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"register.go",
"types.go",
"zz_generated.deepcopy.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit",
deps = [
"//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",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/install:all-srcs",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1:all-srcs",
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -0,0 +1,7 @@
reviewers:
- deads2k
- derekwaynecarr
approvers:
- deads2k
- derekwaynecarr
- smarterclayton

View File

@ -0,0 +1,19 @@
/*
Copyright 2016 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.
*/
// +k8s:deepcopy-gen=package
package eventratelimit // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"

View File

@ -0,0 +1,32 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["install.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/install",
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",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,43 @@
/*
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 install installs the experimental API group, making it available as
// an option to all of the API encoding/decoding machinery.
package install
import (
"k8s.io/apimachinery/pkg/apimachinery/announced"
"k8s.io/apimachinery/pkg/apimachinery/registered"
"k8s.io/apimachinery/pkg/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)
}
}

View File

@ -0,0 +1,51 @@
/*
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 eventratelimit
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
// GroupName is the group name use in this package
const GroupName = "eventratelimit.admission.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
// Kind takes an unqualified kind and returns a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
func addKnownTypes(scheme *runtime.Scheme) error {
// TODO this will get cleaned up with the scheme types are fixed
scheme.AddKnownTypes(SchemeGroupVersion,
&Configuration{},
)
return nil
}

View File

@ -0,0 +1,85 @@
/*
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 eventratelimit
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// LimitType is the type of the limit (e.g., per-namespace)
type LimitType string
const (
// ServerLimitType is a type of limit where there is one bucket shared by
// all of the event queries received by the API Server.
ServerLimitType LimitType = "Server"
// NamespaceLimitType is a type of limit where there is one bucket used by
// each namespace
NamespaceLimitType LimitType = "Namespace"
// UserLimitType is a type of limit where there is one bucket used by each
// user
UserLimitType LimitType = "User"
// SourceAndObjectLimitType is a type of limit where there is one bucket used
// by each combination of source and involved object of the event.
SourceAndObjectLimitType LimitType = "SourceAndObject"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Configuration provides configuration for the EventRateLimit admission
// controller.
type Configuration struct {
metav1.TypeMeta `json:",inline"`
// limits are the limits to place on event queries received.
// Limits can be placed on events received server-wide, per namespace,
// per user, and per source+object.
// At least one limit is required.
Limits []Limit `json:"limits"`
}
// Limit is the configuration for a particular limit type
type Limit struct {
// type is the type of limit to which this configuration applies
Type LimitType `json:"type"`
// qps is the number of event queries per second that are allowed for this
// type of limit. The qps and burst fields are used together to determine if
// a particular event query is accepted. The qps determines how many queries
// are accepted once the burst amount of queries has been exhausted.
QPS int32 `json:"qps"`
// burst is the burst number of event queries that are allowed for this type
// of limit. The qps and burst fields are used together to determine if a
// particular event query is accepted. The burst determines the maximum size
// of the allowance granted for a particular bucket. For example, if the burst
// is 10 and the qps is 3, then the admission control will accept 10 queries
// before blocking any queries. Every second, 3 more queries will be allowed.
// If some of that allowance is not used, then it will roll over to the next
// second, until the maximum allowance of 10 is reached.
Burst int32 `json:"burst"`
// cacheSize is the size of the LRU cache for this type of limit. If a bucket
// is evicted from the cache, then the allowance for that bucket is reset. If
// more queries are later received for an evicted bucket, then that bucket
// will re-enter the cache with a clean slate, giving that bucket a full
// allowance of burst queries.
//
// The default cache size is 4096.
//
// If limitType is 'server', then cacheSize is ignored.
// +optional
CacheSize int32 `json:"cacheSize,omitempty"`
}

View File

@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = [
"defaults.go",
"doc.go",
"register.go",
"types.go",
"zz_generated.conversion.go",
"zz_generated.deepcopy.go",
"zz_generated.defaults.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1",
deps = [
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit: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",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema: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

@ -0,0 +1,25 @@
/*
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 v1alpha1
import kruntime "k8s.io/apimachinery/pkg/runtime"
func addDefaultingFuncs(scheme *kruntime.Scheme) error {
return RegisterDefaults(scheme)
}
func SetDefaults_Configuration(obj *Configuration) {}

View File

@ -0,0 +1,23 @@
/*
Copyright 2016 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.
*/
// +k8s:deepcopy-gen=package
// +k8s:conversion-gen=k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit
// +k8s:defaulter-gen=TypeMeta
// Package v1alpha1 is the v1alpha1 version of the API.
// +groupName=eventratelimit.admission.k8s.io
package v1alpha1 // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1"

View File

@ -0,0 +1,50 @@
/*
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 v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "eventratelimit.admission.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
var (
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
}
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Configuration{},
)
return nil
}

View File

@ -0,0 +1,85 @@
/*
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 v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// LimitType is the type of the limit (e.g., per-namespace)
type LimitType string
const (
// ServerLimitType is a type of limit where there is one bucket shared by
// all of the event queries received by the API Server.
ServerLimitType LimitType = "Server"
// NamespaceLimitType is a type of limit where there is one bucket used by
// each namespace
NamespaceLimitType LimitType = "Namespace"
// UserLimitType is a type of limit where there is one bucket used by each
// user
UserLimitType LimitType = "User"
// SourceAndObjectLimitType is a type of limit where there is one bucket used
// by each combination of source and involved object of the event.
SourceAndObjectLimitType LimitType = "SourceAndObject"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Configuration provides configuration for the EventRateLimit admission
// controller.
type Configuration struct {
metav1.TypeMeta `json:",inline"`
// limits are the limits to place on event queries received.
// Limits can be placed on events received server-wide, per namespace,
// per user, and per source+object.
// At least one limit is required.
Limits []Limit `json:"limits"`
}
// Limit is the configuration for a particular limit type
type Limit struct {
// type is the type of limit to which this configuration applies
Type LimitType `json:"type"`
// qps is the number of event queries per second that are allowed for this
// type of limit. The qps and burst fields are used together to determine if
// a particular event query is accepted. The qps determines how many queries
// are accepted once the burst amount of queries has been exhausted.
QPS int32 `json:"qps"`
// burst is the burst number of event queries that are allowed for this type
// of limit. The qps and burst fields are used together to determine if a
// particular event query is accepted. The burst determines the maximum size
// of the allowance granted for a particular bucket. For example, if the burst
// is 10 and the qps is 3, then the admission control will accept 10 queries
// before blocking any queries. Every second, 3 more queries will be allowed.
// If some of that allowance is not used, then it will roll over to the next
// second, until the maximum allowance of 10 is reached.
Burst int32 `json:"burst"`
// cacheSize is the size of the LRU cache for this type of limit. If a bucket
// is evicted from the cache, then the allowance for that bucket is reset. If
// more queries are later received for an evicted bucket, then that bucket
// will re-enter the cache with a clean slate, giving that bucket a full
// allowance of burst queries.
//
// The default cache size is 4096.
//
// If limitType is 'server', then cacheSize is ignored.
// +optional
CacheSize int32 `json:"cacheSize,omitempty"`
}

View File

@ -0,0 +1,89 @@
// +build !ignore_autogenerated
/*
Copyright 2018 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.
*/
// This file was autogenerated by conversion-gen. Do not edit it manually!
package v1alpha1
import (
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
eventratelimit "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
unsafe "unsafe"
)
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(scheme *runtime.Scheme) error {
return scheme.AddGeneratedConversionFuncs(
Convert_v1alpha1_Configuration_To_eventratelimit_Configuration,
Convert_eventratelimit_Configuration_To_v1alpha1_Configuration,
Convert_v1alpha1_Limit_To_eventratelimit_Limit,
Convert_eventratelimit_Limit_To_v1alpha1_Limit,
)
}
func autoConvert_v1alpha1_Configuration_To_eventratelimit_Configuration(in *Configuration, out *eventratelimit.Configuration, s conversion.Scope) error {
out.Limits = *(*[]eventratelimit.Limit)(unsafe.Pointer(&in.Limits))
return nil
}
// Convert_v1alpha1_Configuration_To_eventratelimit_Configuration is an autogenerated conversion function.
func Convert_v1alpha1_Configuration_To_eventratelimit_Configuration(in *Configuration, out *eventratelimit.Configuration, s conversion.Scope) error {
return autoConvert_v1alpha1_Configuration_To_eventratelimit_Configuration(in, out, s)
}
func autoConvert_eventratelimit_Configuration_To_v1alpha1_Configuration(in *eventratelimit.Configuration, out *Configuration, s conversion.Scope) error {
out.Limits = *(*[]Limit)(unsafe.Pointer(&in.Limits))
return nil
}
// Convert_eventratelimit_Configuration_To_v1alpha1_Configuration is an autogenerated conversion function.
func Convert_eventratelimit_Configuration_To_v1alpha1_Configuration(in *eventratelimit.Configuration, out *Configuration, s conversion.Scope) error {
return autoConvert_eventratelimit_Configuration_To_v1alpha1_Configuration(in, out, s)
}
func autoConvert_v1alpha1_Limit_To_eventratelimit_Limit(in *Limit, out *eventratelimit.Limit, s conversion.Scope) error {
out.Type = eventratelimit.LimitType(in.Type)
out.QPS = in.QPS
out.Burst = in.Burst
out.CacheSize = in.CacheSize
return nil
}
// Convert_v1alpha1_Limit_To_eventratelimit_Limit is an autogenerated conversion function.
func Convert_v1alpha1_Limit_To_eventratelimit_Limit(in *Limit, out *eventratelimit.Limit, s conversion.Scope) error {
return autoConvert_v1alpha1_Limit_To_eventratelimit_Limit(in, out, s)
}
func autoConvert_eventratelimit_Limit_To_v1alpha1_Limit(in *eventratelimit.Limit, out *Limit, s conversion.Scope) error {
out.Type = LimitType(in.Type)
out.QPS = in.QPS
out.Burst = in.Burst
out.CacheSize = in.CacheSize
return nil
}
// Convert_eventratelimit_Limit_To_v1alpha1_Limit is an autogenerated conversion function.
func Convert_eventratelimit_Limit_To_v1alpha1_Limit(in *eventratelimit.Limit, out *Limit, s conversion.Scope) error {
return autoConvert_eventratelimit_Limit_To_v1alpha1_Limit(in, out, s)
}

View File

@ -0,0 +1,72 @@
// +build !ignore_autogenerated
/*
Copyright 2018 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.
*/
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Configuration) DeepCopyInto(out *Configuration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Limits != nil {
in, out := &in.Limits, &out.Limits
*out = make([]Limit, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
func (in *Configuration) DeepCopy() *Configuration {
if in == nil {
return nil
}
out := new(Configuration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Configuration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Limit) DeepCopyInto(out *Limit) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Limit.
func (in *Limit) DeepCopy() *Limit {
if in == nil {
return nil
}
out := new(Limit)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,37 @@
// +build !ignore_autogenerated
/*
Copyright 2018 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.
*/
// This file was autogenerated by defaulter-gen. Do not edit it manually!
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
scheme.AddTypeDefaultingFunc(&Configuration{}, func(obj interface{}) { SetObjectDefaults_Configuration(obj.(*Configuration)) })
return nil
}
func SetObjectDefaults_Configuration(in *Configuration) {
SetDefaults_Configuration(in)
}

View File

@ -0,0 +1,38 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["validation.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation",
deps = [
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["validation_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation",
library = ":go_default_library",
deps = ["//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library"],
)

View File

@ -0,0 +1,63 @@
/*
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 validation
import (
"k8s.io/apimachinery/pkg/util/validation/field"
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
)
var limitTypes = map[eventratelimitapi.LimitType]bool{
eventratelimitapi.ServerLimitType: true,
eventratelimitapi.NamespaceLimitType: true,
eventratelimitapi.UserLimitType: true,
eventratelimitapi.SourceAndObjectLimitType: true,
}
// ValidateConfiguration validates the configuration.
func ValidateConfiguration(config *eventratelimitapi.Configuration) field.ErrorList {
allErrs := field.ErrorList{}
limitsPath := field.NewPath("limits")
if len(config.Limits) == 0 {
allErrs = append(allErrs, field.Invalid(limitsPath, config.Limits, "must not be empty"))
}
for i, limit := range config.Limits {
idxPath := limitsPath.Index(i)
if !limitTypes[limit.Type] {
allowedValues := make([]string, len(limitTypes))
i := 0
for limitType := range limitTypes {
allowedValues[i] = string(limitType)
i++
}
allErrs = append(allErrs, field.NotSupported(idxPath.Child("type"), limit.Type, allowedValues))
}
if limit.Burst <= 0 {
allErrs = append(allErrs, field.Invalid(idxPath.Child("burst"), limit.Burst, "must be positive"))
}
if limit.QPS <= 0 {
allErrs = append(allErrs, field.Invalid(idxPath.Child("qps"), limit.QPS, "must be positive"))
}
if limit.Type != eventratelimitapi.ServerLimitType {
if limit.CacheSize < 0 {
allErrs = append(allErrs, field.Invalid(idxPath.Child("cacheSize"), limit.CacheSize, "must not be negative"))
}
}
}
return allErrs
}

View File

@ -0,0 +1,192 @@
/*
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 validation
import (
"testing"
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
)
func TestValidateConfiguration(t *testing.T) {
cases := []struct {
name string
config eventratelimitapi.Configuration
expectedResult bool
}{
{
name: "valid server",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "Server",
Burst: 5,
QPS: 1,
},
},
},
expectedResult: true,
},
{
name: "valid namespace",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "Namespace",
Burst: 10,
QPS: 2,
CacheSize: 100,
},
},
},
expectedResult: true,
},
{
name: "valid user",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "User",
Burst: 10,
QPS: 2,
CacheSize: 100,
},
},
},
expectedResult: true,
},
{
name: "valid source+object",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "SourceAndObject",
Burst: 5,
QPS: 1,
CacheSize: 1000,
},
},
},
expectedResult: true,
},
{
name: "valid multiple",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "Server",
Burst: 5,
QPS: 1,
},
{
Type: "Namespace",
Burst: 10,
QPS: 2,
CacheSize: 100,
},
{
Type: "SourceAndObject",
Burst: 25,
QPS: 10,
CacheSize: 1000,
},
},
},
expectedResult: true,
},
{
name: "missing limits",
config: eventratelimitapi.Configuration{},
expectedResult: false,
},
{
name: "missing type",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Burst: 25,
QPS: 10,
CacheSize: 1000,
},
},
},
expectedResult: false,
},
{
name: "invalid type",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "unknown-type",
Burst: 25,
QPS: 10,
CacheSize: 1000,
},
},
},
expectedResult: false,
},
{
name: "missing burst",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "Server",
QPS: 1,
},
},
},
expectedResult: false,
},
{
name: "missing qps",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "Server",
Burst: 5,
},
},
},
expectedResult: false,
},
{
name: "negative cache size",
config: eventratelimitapi.Configuration{
Limits: []eventratelimitapi.Limit{
{
Type: "Namespace",
Burst: 10,
QPS: 2,
CacheSize: -1,
},
},
},
expectedResult: false,
},
}
for _, tc := range cases {
errs := ValidateConfiguration(&tc.config)
if e, a := tc.expectedResult, len(errs) == 0; e != a {
if e {
t.Errorf("%v: expected success: %v", tc.name, errs)
} else {
t.Errorf("%v: expected failure", tc.name)
}
}
}
}

View File

@ -0,0 +1,72 @@
// +build !ignore_autogenerated
/*
Copyright 2018 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.
*/
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
package eventratelimit
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Configuration) DeepCopyInto(out *Configuration) {
*out = *in
out.TypeMeta = in.TypeMeta
if in.Limits != nil {
in, out := &in.Limits, &out.Limits
*out = make([]Limit, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
func (in *Configuration) DeepCopy() *Configuration {
if in == nil {
return nil
}
out := new(Configuration)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Configuration) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
} else {
return nil
}
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Limit) DeepCopyInto(out *Limit) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Limit.
func (in *Limit) DeepCopy() *Limit {
if in == nil {
return nil
}
out := new(Limit)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,57 @@
/*
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 eventratelimit
import (
"github.com/hashicorp/golang-lru"
"k8s.io/client-go/util/flowcontrol"
)
// cache is an interface for caching the limits of a particular type
type cache interface {
// get the rate limiter associated with the specified key
get(key interface{}) flowcontrol.RateLimiter
}
// singleCache is a cache that only stores a single, constant item
type singleCache struct {
// the single rate limiter held by the cache
rateLimiter flowcontrol.RateLimiter
}
func (c *singleCache) get(key interface{}) flowcontrol.RateLimiter {
return c.rateLimiter
}
// lruCache is a least-recently-used cache
type lruCache struct {
// factory to use to create new rate limiters
rateLimiterFactory func() flowcontrol.RateLimiter
// the actual LRU cache
cache *lru.Cache
}
func (c *lruCache) get(key interface{}) flowcontrol.RateLimiter {
value, found := c.cache.Get(key)
if !found {
rateLimter := c.rateLimiterFactory()
c.cache.Add(key, rateLimter)
return rateLimter
}
return value.(flowcontrol.RateLimiter)
}

View File

@ -0,0 +1,119 @@
/*
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 eventratelimit
import (
"testing"
"github.com/hashicorp/golang-lru"
"k8s.io/client-go/util/flowcontrol"
)
func TestSingleCache(t *testing.T) {
rateLimiter := flowcontrol.NewTokenBucketRateLimiter(1., 1)
cache := singleCache{
rateLimiter: rateLimiter,
}
cases := []interface{}{nil, "key1", "key2"}
for _, tc := range cases {
actual := cache.get(tc)
if e, a := rateLimiter, actual; e != a {
t.Errorf("unexpected entry in cache for key %v: expected %v, got %v", tc, e, a)
}
}
}
func TestLRUCache(t *testing.T) {
rateLimiters := []flowcontrol.RateLimiter{
flowcontrol.NewTokenBucketRateLimiter(1., 1),
flowcontrol.NewTokenBucketRateLimiter(2., 2),
flowcontrol.NewTokenBucketRateLimiter(3., 3),
flowcontrol.NewTokenBucketRateLimiter(4., 4),
}
nextRateLimiter := 0
rateLimiterFactory := func() flowcontrol.RateLimiter {
rateLimiter := rateLimiters[nextRateLimiter]
nextRateLimiter++
return rateLimiter
}
underlyingCache, err := lru.New(2)
if err != nil {
t.Fatalf("Could not create LRU cache: %v", err)
}
cache := lruCache{
rateLimiterFactory: rateLimiterFactory,
cache: underlyingCache,
}
cases := []struct {
name string
key int
expected flowcontrol.RateLimiter
}{
{
name: "first added",
key: 0,
expected: rateLimiters[0],
},
{
name: "first obtained",
key: 0,
expected: rateLimiters[0],
},
{
name: "second added",
key: 1,
expected: rateLimiters[1],
},
{
name: "second obtained",
key: 1,
expected: rateLimiters[1],
},
{
name: "first obtained second time",
key: 0,
expected: rateLimiters[0],
},
{
name: "third added",
key: 2,
expected: rateLimiters[2],
},
{
name: "third obtained",
key: 2,
expected: rateLimiters[2],
},
{
name: "first obtained third time",
key: 0,
expected: rateLimiters[0],
},
{
name: "second re-added after eviction",
key: 1,
expected: rateLimiters[3],
},
}
for _, tc := range cases {
actual := cache.get(tc.key)
if e, a := tc.expected, actual; e != a {
t.Errorf("%v: unexpected entry in cache for key %v: expected %v, got %v", tc.name, tc.key, e, a)
}
}
}

View File

@ -0,0 +1,34 @@
/*
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 eventratelimit
import (
"time"
)
// realClock implements flowcontrol.Clock in terms of standard time functions.
type realClock struct{}
// Now is identical to time.Now.
func (realClock) Now() time.Time {
return time.Now()
}
// Sleep is identical to time.Sleep.
func (realClock) Sleep(d time.Duration) {
time.Sleep(d)
}

View File

@ -0,0 +1,72 @@
/*
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 eventratelimit
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"
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/install"
eventratelimitv1alpha1 "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1"
)
var (
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
scheme = runtime.NewScheme()
codecs = serializer.NewCodecFactory(scheme)
)
func init() {
install.Install(groupFactoryRegistry, registry, scheme)
}
// LoadConfiguration loads the provided configuration.
func LoadConfiguration(config io.Reader) (*eventratelimitapi.Configuration, error) {
// if no config is provided, return a default configuration
if config == nil {
externalConfig := &eventratelimitv1alpha1.Configuration{}
scheme.Default(externalConfig)
internalConfig := &eventratelimitapi.Configuration{}
if err := scheme.Convert(externalConfig, internalConfig, nil); err != nil {
return nil, err
}
return internalConfig, nil
}
// we have a config so parse it.
data, err := ioutil.ReadAll(config)
if err != nil {
return nil, err
}
decoder := codecs.UniversalDecoder()
decodedObj, err := runtime.Decode(decoder, data)
if err != nil {
return nil, err
}
resourceQuotaConfiguration, ok := decodedObj.(*eventratelimitapi.Configuration)
if !ok {
return nil, fmt.Errorf("unexpected type: %T", decodedObj)
}
return resourceQuotaConfiguration, nil
}

View File

@ -0,0 +1,18 @@
/*
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 eventratelimit contains an admission controller that enforces a rate limit on events
package eventratelimit // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"

View File

@ -0,0 +1,145 @@
/*
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 eventratelimit
import (
"fmt"
"strings"
"github.com/hashicorp/golang-lru"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/util/flowcontrol"
api "k8s.io/kubernetes/pkg/apis/core"
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
)
const (
// cache size to use if the user did not specify a cache size
defaultCacheSize = 4096
)
// limitEnforcer enforces a single type of event rate limit, such as server, namespace, or source+object
type limitEnforcer struct {
// type of this limit
limitType eventratelimitapi.LimitType
// cache for holding the rate limiters
cache cache
// a keyFunc which is responsible for computing a single key based on input
keyFunc func(admission.Attributes) string
}
func newLimitEnforcer(config eventratelimitapi.Limit, clock flowcontrol.Clock) (*limitEnforcer, error) {
rateLimiterFactory := func() flowcontrol.RateLimiter {
return flowcontrol.NewTokenBucketRateLimiterWithClock(float32(config.QPS), int(config.Burst), clock)
}
if config.Type == eventratelimitapi.ServerLimitType {
return &limitEnforcer{
limitType: config.Type,
cache: &singleCache{
rateLimiter: rateLimiterFactory(),
},
keyFunc: getServerKey,
}, nil
}
cacheSize := int(config.CacheSize)
if cacheSize == 0 {
cacheSize = defaultCacheSize
}
underlyingCache, err := lru.New(cacheSize)
if err != nil {
return nil, fmt.Errorf("could not create lru cache: %v", err)
}
cache := &lruCache{
rateLimiterFactory: rateLimiterFactory,
cache: underlyingCache,
}
var keyFunc func(admission.Attributes) string
switch t := config.Type; t {
case eventratelimitapi.NamespaceLimitType:
keyFunc = getNamespaceKey
case eventratelimitapi.UserLimitType:
keyFunc = getUserKey
case eventratelimitapi.SourceAndObjectLimitType:
keyFunc = getSourceAndObjectKey
default:
return nil, fmt.Errorf("unknown event rate limit type: %v", t)
}
return &limitEnforcer{
limitType: config.Type,
cache: cache,
keyFunc: keyFunc,
}, nil
}
func (enforcer *limitEnforcer) accept(attr admission.Attributes) error {
key := enforcer.keyFunc(attr)
rateLimiter := enforcer.cache.get(key)
// ensure we have available rate
allow := rateLimiter.TryAccept()
if !allow {
return apierrors.NewTooManyRequestsError(fmt.Sprintf("limit reached on type %v for key %v", enforcer.limitType, key))
}
return nil
}
func getServerKey(attr admission.Attributes) string {
return ""
}
// getNamespaceKey returns a cache key that is based on the namespace of the event request
func getNamespaceKey(attr admission.Attributes) string {
return attr.GetNamespace()
}
// getUserKey returns a cache key that is based on the user of the event request
func getUserKey(attr admission.Attributes) string {
userInfo := attr.GetUserInfo()
if userInfo == nil {
return ""
}
return userInfo.GetName()
}
// getSourceAndObjectKey returns a cache key that is based on the source+object of the event
func getSourceAndObjectKey(attr admission.Attributes) string {
object := attr.GetObject()
if object == nil {
return ""
}
event, ok := object.(*api.Event)
if !ok {
return ""
}
return strings.Join([]string{
event.Source.Component,
event.Source.Host,
event.InvolvedObject.Kind,
event.InvolvedObject.Namespace,
event.InvolvedObject.Name,
string(event.InvolvedObject.UID),
event.InvolvedObject.APIVersion,
}, "")
}

View File

@ -0,0 +1,51 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/exec",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/kubeapiserver/admission: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/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/exec",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake: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/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/client-go/testing: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

@ -0,0 +1,146 @@
/*
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 exec
import (
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/registry/rest"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("DenyEscalatingExec", func(config io.Reader) (admission.Interface, error) {
return NewDenyEscalatingExec(), nil
})
// This is for legacy support of the DenyExecOnPrivileged admission controller. Most
// of the time DenyEscalatingExec should be preferred.
plugins.Register("DenyExecOnPrivileged", func(config io.Reader) (admission.Interface, error) {
return NewDenyExecOnPrivileged(), nil
})
}
// DenyExec is an implementation of admission.Interface which says no to a pod/exec on
// a pod using host based configurations.
type DenyExec struct {
*admission.Handler
client internalclientset.Interface
// these flags control which items will be checked to deny exec/attach
hostIPC bool
hostPID bool
privileged bool
}
var _ admission.ValidationInterface = &DenyExec{}
var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&DenyExec{})
// NewDenyEscalatingExec creates a new admission controller that denies an exec operation on a pod
// using host based configurations.
func NewDenyEscalatingExec() *DenyExec {
return &DenyExec{
Handler: admission.NewHandler(admission.Connect),
hostIPC: true,
hostPID: true,
privileged: true,
}
}
// NewDenyExecOnPrivileged creates a new admission controller that is only checking the privileged
// option. This is for legacy support of the DenyExecOnPrivileged admission controller. Most
// of the time NewDenyEscalatingExec should be preferred.
func NewDenyExecOnPrivileged() *DenyExec {
return &DenyExec{
Handler: admission.NewHandler(admission.Connect),
hostIPC: false,
hostPID: false,
privileged: true,
}
}
// Validate makes an admission decision based on the request attributes
func (d *DenyExec) Validate(a admission.Attributes) (err error) {
connectRequest, ok := a.GetObject().(*rest.ConnectRequest)
if !ok {
return errors.NewBadRequest("a connect request was received, but could not convert the request object.")
}
// Only handle exec or attach requests on pods
if connectRequest.ResourcePath != "pods/exec" && connectRequest.ResourcePath != "pods/attach" {
return nil
}
pod, err := d.client.Core().Pods(a.GetNamespace()).Get(connectRequest.Name, metav1.GetOptions{})
if err != nil {
return admission.NewForbidden(a, err)
}
if d.hostPID && pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.HostPID {
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a container using host pid"))
}
if d.hostIPC && pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.HostIPC {
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a container using host ipc"))
}
if d.privileged && isPrivileged(pod) {
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a privileged container"))
}
return nil
}
// isPrivileged will return true a pod has any privileged containers
func isPrivileged(pod *api.Pod) bool {
for _, c := range pod.Spec.InitContainers {
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil {
continue
}
if *c.SecurityContext.Privileged {
return true
}
}
for _, c := range pod.Spec.Containers {
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil {
continue
}
if *c.SecurityContext.Privileged {
return true
}
}
return false
}
// SetInternalKubeClientSet implements the WantsInternalKubeClientSet interface.
func (d *DenyExec) SetInternalKubeClientSet(client internalclientset.Interface) {
d.client = client
}
// ValidateInitialization implements the InitializationValidator interface.
func (d *DenyExec) ValidateInitialization() error {
if d.client == nil {
return fmt.Errorf("missing client")
}
return nil
}

View File

@ -0,0 +1,211 @@
/*
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 exec
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/registry/rest"
core "k8s.io/client-go/testing"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
)
// newAllowEscalatingExec returns `admission.Interface` that allows execution on
// "hostIPC", "hostPID" and "privileged".
func newAllowEscalatingExec() *DenyExec {
return &DenyExec{
Handler: admission.NewHandler(admission.Connect),
hostIPC: false,
hostPID: false,
privileged: false,
}
}
func TestAdmission(t *testing.T) {
privPod := validPod("privileged")
priv := true
privPod.Spec.Containers[0].SecurityContext = &api.SecurityContext{
Privileged: &priv,
}
hostPIDPod := validPod("hostPID")
hostPIDPod.Spec.SecurityContext = &api.PodSecurityContext{}
hostPIDPod.Spec.SecurityContext.HostPID = true
hostIPCPod := validPod("hostIPC")
hostIPCPod.Spec.SecurityContext = &api.PodSecurityContext{}
hostIPCPod.Spec.SecurityContext.HostIPC = true
testCases := map[string]struct {
pod *api.Pod
shouldAccept bool
}{
"priv": {
shouldAccept: false,
pod: privPod,
},
"hostPID": {
shouldAccept: false,
pod: hostPIDPod,
},
"hostIPC": {
shouldAccept: false,
pod: hostIPCPod,
},
"non privileged": {
shouldAccept: true,
pod: validPod("nonPrivileged"),
},
}
// Get the direct object though to allow testAdmission to inject the client
handler := NewDenyEscalatingExec()
for _, tc := range testCases {
testAdmission(t, tc.pod, handler, tc.shouldAccept)
}
// run with a permissive config and all cases should pass
handler = newAllowEscalatingExec()
for _, tc := range testCases {
testAdmission(t, tc.pod, handler, true)
}
// run against an init container
handler = NewDenyEscalatingExec()
for _, tc := range testCases {
tc.pod.Spec.InitContainers = tc.pod.Spec.Containers
tc.pod.Spec.Containers = nil
testAdmission(t, tc.pod, handler, tc.shouldAccept)
}
// run with a permissive config and all cases should pass
handler = newAllowEscalatingExec()
for _, tc := range testCases {
testAdmission(t, tc.pod, handler, true)
}
}
func testAdmission(t *testing.T, pod *api.Pod, handler *DenyExec, shouldAccept bool) {
mockClient := &fake.Clientset{}
mockClient.AddReactor("get", "pods", func(action core.Action) (bool, runtime.Object, error) {
if action.(core.GetAction).GetName() == pod.Name {
return true, pod, nil
}
t.Errorf("Unexpected API call: %#v", action)
return true, nil, nil
})
handler.SetInternalKubeClientSet(mockClient)
admission.ValidateInitialization(handler)
// pods/exec
{
req := &rest.ConnectRequest{Name: pod.Name, ResourcePath: "pods/exec"}
err := handler.Validate(admission.NewAttributesRecord(req, nil, api.Kind("Pod").WithVersion("version"), "test", "name", api.Resource("pods").WithVersion("version"), "exec", admission.Connect, nil))
if shouldAccept && err != nil {
t.Errorf("Unexpected error returned from admission handler: %v", err)
}
if !shouldAccept && err == nil {
t.Errorf("An error was expected from the admission handler. Received nil")
}
}
// pods/attach
{
req := &rest.ConnectRequest{Name: pod.Name, ResourcePath: "pods/attach"}
err := handler.Validate(admission.NewAttributesRecord(req, nil, api.Kind("Pod").WithVersion("version"), "test", "name", api.Resource("pods").WithVersion("version"), "attach", admission.Connect, nil))
if shouldAccept && err != nil {
t.Errorf("Unexpected error returned from admission handler: %v", err)
}
if !shouldAccept && err == nil {
t.Errorf("An error was expected from the admission handler. Received nil")
}
}
}
// Test to ensure legacy admission controller works as expected.
func TestDenyExecOnPrivileged(t *testing.T) {
privPod := validPod("privileged")
priv := true
privPod.Spec.Containers[0].SecurityContext = &api.SecurityContext{
Privileged: &priv,
}
hostPIDPod := validPod("hostPID")
hostPIDPod.Spec.SecurityContext = &api.PodSecurityContext{}
hostPIDPod.Spec.SecurityContext.HostPID = true
hostIPCPod := validPod("hostIPC")
hostIPCPod.Spec.SecurityContext = &api.PodSecurityContext{}
hostIPCPod.Spec.SecurityContext.HostIPC = true
testCases := map[string]struct {
pod *api.Pod
shouldAccept bool
}{
"priv": {
shouldAccept: false,
pod: privPod,
},
"hostPID": {
shouldAccept: true,
pod: hostPIDPod,
},
"hostIPC": {
shouldAccept: true,
pod: hostIPCPod,
},
"non privileged": {
shouldAccept: true,
pod: validPod("nonPrivileged"),
},
}
// Get the direct object though to allow testAdmission to inject the client
handler := NewDenyExecOnPrivileged()
for _, tc := range testCases {
testAdmission(t, tc.pod, handler, tc.shouldAccept)
}
// test init containers
for _, tc := range testCases {
tc.pod.Spec.InitContainers = tc.pod.Spec.Containers
tc.pod.Spec.Containers = nil
testAdmission(t, tc.pod, handler, tc.shouldAccept)
}
}
func validPod(name string) *api.Pod {
return &api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
Spec: api.PodSpec{
Containers: []api.Container{
{Name: "ctr1", Image: "image"},
{Name: "ctr2", Image: "image2"},
},
},
}
}

View File

@ -0,0 +1,42 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["admission.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/extendedresourcetoleration",
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/extendedresourcetoleration",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource: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"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,94 @@
/*
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 extendedresourcetoleration
import (
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper"
)
// Register is called by the apiserver to register the plugin factory.
func Register(plugins *admission.Plugins) {
plugins.Register("ExtendedResourceToleration", func(config io.Reader) (admission.Interface, error) {
return newExtendedResourceToleration(), nil
})
}
// newExtendedResourceToleration creates a new instance of the ExtendedResourceToleration admission controller.
func newExtendedResourceToleration() *plugin {
return &plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
}
}
// Make sure we are implementing the interface.
var _ admission.MutationInterface = &plugin{}
type plugin struct {
*admission.Handler
}
// Admit updates the toleration of a pod based on the resources requested by it.
// If an extended resource of name "example.com/device" is requested, it adds
// a toleration with key "example.com/device", operator "Exists" and effect "NoSchedule".
// The rationale for this is described in:
// https://github.com/kubernetes/kubernetes/issues/55080
func (p *plugin) Admit(attributes admission.Attributes) error {
// Ignore all calls to subresources or resources other than pods.
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != core.Resource("pods") {
return nil
}
pod, ok := attributes.GetObject().(*core.Pod)
if !ok {
return errors.NewBadRequest(fmt.Sprintf("expected *core.Pod but got %T", attributes.GetObject()))
}
resources := sets.String{}
for _, container := range pod.Spec.Containers {
for resourceName := range container.Resources.Requests {
if helper.IsExtendedResourceName(resourceName) {
resources.Insert(string(resourceName))
}
}
}
for _, container := range pod.Spec.InitContainers {
for resourceName := range container.Resources.Requests {
if helper.IsExtendedResourceName(resourceName) {
resources.Insert(string(resourceName))
}
}
}
// Doing .List() so that we get a stable sorted list.
// This allows us to test adding tolerations for multiple extended resources.
for _, resource := range resources.List() {
helper.AddOrUpdateTolerationInPod(pod, &core.Toleration{
Key: resource,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
})
}
return nil
}

View File

@ -0,0 +1,382 @@
/*
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 extendedresourcetoleration
import (
"testing"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apiserver/pkg/admission"
"k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/helper"
)
func TestAdmit(t *testing.T) {
plugin := newExtendedResourceToleration()
containerRequestingCPU := core.Container{
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI),
},
},
}
containerRequestingMemory := core.Container{
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceMemory: *resource.NewQuantity(2048, resource.DecimalSI),
},
},
}
extendedResource1 := "example.com/device-ek"
extendedResource2 := "example.com/device-do"
containerRequestingExtendedResource1 := core.Container{
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(extendedResource1): *resource.NewQuantity(1, resource.DecimalSI),
},
},
}
containerRequestingExtendedResource2 := core.Container{
Resources: core.ResourceRequirements{
Requests: core.ResourceList{
core.ResourceName(extendedResource2): *resource.NewQuantity(2, resource.DecimalSI),
},
},
}
tests := []struct {
description string
requestedPod core.Pod
expectedPod core.Pod
}{
{
description: "empty pod without any extended resources, expect no change in tolerations",
requestedPod: core.Pod{
Spec: core.PodSpec{},
},
expectedPod: core.Pod{
Spec: core.PodSpec{},
},
},
{
description: "pod with container without any extended resources, expect no change in tolerations",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
},
},
},
},
{
description: "pod with init container without any extended resources, expect no change in tolerations",
requestedPod: core.Pod{
Spec: core.PodSpec{
InitContainers: []core.Container{
containerRequestingMemory,
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
InitContainers: []core.Container{
containerRequestingMemory,
},
},
},
},
{
description: "pod with container with extended resource, expect toleration to be added",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingExtendedResource1,
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
{
description: "pod with init container with extended resource, expect toleration to be added",
requestedPod: core.Pod{
Spec: core.PodSpec{
InitContainers: []core.Container{
containerRequestingExtendedResource2,
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
InitContainers: []core.Container{
containerRequestingExtendedResource2,
},
Tolerations: []core.Toleration{
{
Key: extendedResource2,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
{
description: "pod with existing tolerations and container with extended resource, expect existing tolerations to be preserved and new toleration to be added",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: "foo",
Operator: core.TolerationOpEqual,
Value: "bar",
Effect: core.TaintEffectNoSchedule,
},
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: "foo",
Operator: core.TolerationOpEqual,
Value: "bar",
Effect: core.TaintEffectNoSchedule,
},
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
{
description: "pod with multiple extended resources, expect multiple tolerations to be added",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingMemory,
containerRequestingExtendedResource1,
},
InitContainers: []core.Container{
containerRequestingCPU,
containerRequestingExtendedResource2,
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingMemory,
containerRequestingExtendedResource1,
},
InitContainers: []core.Container{
containerRequestingCPU,
containerRequestingExtendedResource2,
},
Tolerations: []core.Toleration{
// Note the order, it's sorted by the Key
{
Key: extendedResource2,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
{
description: "pod with container requesting extended resource and existing correct toleration, expect no change in tolerations",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingMemory,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingMemory,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
{
description: "pod with container requesting extended resource and existing toleration with the same key but different effect and value, expect existing tolerations to be preserved and new toleration to be added",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingMemory,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: extendedResource1,
Operator: core.TolerationOpEqual,
Value: "foo",
Effect: core.TaintEffectNoExecute,
},
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingMemory,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Key: extendedResource1,
Operator: core.TolerationOpEqual,
Value: "foo",
Effect: core.TaintEffectNoExecute,
},
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
{
description: "pod with wildcard toleration and container requesting extended resource, expect existing tolerations to be preserved and new toleration to be added",
requestedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingMemory,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Operator: core.TolerationOpExists,
},
},
},
},
expectedPod: core.Pod{
Spec: core.PodSpec{
Containers: []core.Container{
containerRequestingCPU,
containerRequestingMemory,
containerRequestingExtendedResource1,
},
Tolerations: []core.Toleration{
{
Operator: core.TolerationOpExists,
},
{
Key: extendedResource1,
Operator: core.TolerationOpExists,
Effect: core.TaintEffectNoSchedule,
},
},
},
},
},
}
for i, test := range tests {
err := plugin.Admit(admission.NewAttributesRecord(&test.requestedPod, nil, core.Kind("Pod").WithVersion("version"), "foo", "name", core.Resource("pods").WithVersion("version"), "", "ignored", nil))
if err != nil {
t.Errorf("[%d: %s] unexpected error %v for pod %+v", i, test.description, err, test.requestedPod)
}
if !helper.Semantic.DeepEqual(test.expectedPod.Spec.Tolerations, test.requestedPod.Spec.Tolerations) {
t.Errorf("[%d: %s] expected %#v got %#v", i, test.description, test.expectedPod.Spec.Tolerations, test.requestedPod.Spec.Tolerations)
}
}
}
func TestHandles(t *testing.T) {
plugin := newExtendedResourceToleration()
tests := map[admission.Operation]bool{
admission.Create: true,
admission.Update: true,
admission.Delete: false,
admission.Connect: false,
}
for op, expected := range tests {
result := plugin.Handles(op)
if result != expected {
t.Errorf("Unexpected result for operation %s: %v\n", op, result)
}
}
}

55
vendor/k8s.io/kubernetes/plugin/pkg/admission/gc/BUILD generated vendored Normal file
View File

@ -0,0 +1,55 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["gc_admission.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/gc",
deps = [
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta: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",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["gc_admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/gc",
library = ":go_default_library",
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/kubeapiserver/admission: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",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authorization/authorizer: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

@ -0,0 +1,273 @@
/*
Copyright 2016 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 gc
import (
"fmt"
"io"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("OwnerReferencesPermissionEnforcement", func(config io.Reader) (admission.Interface, error) {
// the pods/status endpoint is ignored by this plugin since old kubelets
// corrupt them. the pod status strategy ensures status updates cannot mutate
// ownerRef.
whiteList := []whiteListItem{
{
groupResource: schema.GroupResource{Resource: "pods"},
subresource: "status",
},
}
return &gcPermissionsEnforcement{
Handler: admission.NewHandler(admission.Create, admission.Update),
whiteList: whiteList,
}, nil
})
}
// gcPermissionsEnforcement is an implementation of admission.Interface.
type gcPermissionsEnforcement struct {
*admission.Handler
authorizer authorizer.Authorizer
restMapper meta.RESTMapper
// items in this whitelist are ignored upon admission.
// any item in this list must protect against ownerRef mutations
// via strategy enforcement.
whiteList []whiteListItem
}
var _ admission.ValidationInterface = &gcPermissionsEnforcement{}
// whiteListItem describes an entry in a whitelist ignored by gc permission enforcement.
type whiteListItem struct {
groupResource schema.GroupResource
subresource string
}
// isWhiteListed returns true if the specified item is in the whitelist.
func (a *gcPermissionsEnforcement) isWhiteListed(groupResource schema.GroupResource, subresource string) bool {
for _, item := range a.whiteList {
if item.groupResource == groupResource && item.subresource == subresource {
return true
}
}
return false
}
func (a *gcPermissionsEnforcement) Validate(attributes admission.Attributes) (err error) {
// // if the request is in the whitelist, we skip mutation checks for this resource.
if a.isWhiteListed(attributes.GetResource().GroupResource(), attributes.GetSubresource()) {
return nil
}
// if we aren't changing owner references, then the edit is always allowed
if !isChangingOwnerReference(attributes.GetObject(), attributes.GetOldObject()) {
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))
}
// Further check if the user is setting ownerReference.blockOwnerDeletion to
// true. If so, only allows the change if the user has delete permission of
// the _OWNER_
newBlockingRefs := newBlockingOwnerDeletionRefs(attributes.GetObject(), attributes.GetOldObject())
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))
}
// Multiple records are returned if ref.Kind could map to multiple
// resources. User needs to have delete permission on all the
// matched Resources.
for _, record := range records {
decision, reason, err := a.authorizer.Authorize(record)
if decision != authorizer.DecisionAllow {
return admission.NewForbidden(attributes, fmt.Errorf("cannot set blockOwnerDeletion if an ownerReference refers to a resource you can't set finalizers on: %v, %v", reason, err))
}
}
}
return nil
}
func isChangingOwnerReference(newObj, oldObj runtime.Object) bool {
newMeta, err := meta.Accessor(newObj)
if err != nil {
// if we don't have objectmeta, we don't have the object reference
return false
}
if oldObj == nil {
return len(newMeta.GetOwnerReferences()) > 0
}
oldMeta, err := meta.Accessor(oldObj)
if err != nil {
// if we don't have objectmeta, we don't have the object reference
return false
}
// compare the old and new. If they aren't the same, then we're trying to change an ownerRef
oldOwners := oldMeta.GetOwnerReferences()
newOwners := newMeta.GetOwnerReferences()
if len(oldOwners) != len(newOwners) {
return true
}
for i := range oldOwners {
if !apiequality.Semantic.DeepEqual(oldOwners[i], newOwners[i]) {
return true
}
}
return false
}
// Translates ref to a DeleteAttribute deleting the object referred by the ref.
// OwnerReference only records the object kind, which might map to multiple
// resources, so multiple DeleteAttribute might be returned.
func (a *gcPermissionsEnforcement) ownerRefToDeleteAttributeRecords(ref metav1.OwnerReference, attributes admission.Attributes) ([]authorizer.AttributesRecord, error) {
var ret []authorizer.AttributesRecord
groupVersion, err := schema.ParseGroupVersion(ref.APIVersion)
if err != nil {
return ret, err
}
mappings, err := a.restMapper.RESTMappings(schema.GroupKind{Group: groupVersion.Group, Kind: ref.Kind}, groupVersion.Version)
if err != nil {
return ret, err
}
for _, mapping := range mappings {
ret = append(ret, authorizer.AttributesRecord{
User: attributes.GetUserInfo(),
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,
Subresource: "finalizers",
Name: ref.Name,
ResourceRequest: true,
Path: "",
})
}
return ret, nil
}
// only keeps the blocking refs
func blockingOwnerRefs(refs []metav1.OwnerReference) []metav1.OwnerReference {
var ret []metav1.OwnerReference
for _, ref := range refs {
if ref.BlockOwnerDeletion != nil && *ref.BlockOwnerDeletion == true {
ret = append(ret, ref)
}
}
return ret
}
func indexByUID(refs []metav1.OwnerReference) map[types.UID]metav1.OwnerReference {
ret := make(map[types.UID]metav1.OwnerReference)
for _, ref := range refs {
ret[ref.UID] = ref
}
return ret
}
// Returns new blocking ownerReferences, and references whose blockOwnerDeletion
// field is changed from nil or false to true.
func newBlockingOwnerDeletionRefs(newObj, oldObj runtime.Object) []metav1.OwnerReference {
newMeta, err := meta.Accessor(newObj)
if err != nil {
// if we don't have objectmeta, we don't have the object reference
return nil
}
newRefs := newMeta.GetOwnerReferences()
blockingNewRefs := blockingOwnerRefs(newRefs)
if len(blockingNewRefs) == 0 {
return nil
}
if oldObj == nil {
return blockingNewRefs
}
oldMeta, err := meta.Accessor(oldObj)
if err != nil {
// if we don't have objectmeta, treat it as if all the ownerReference are newly created
return blockingNewRefs
}
var ret []metav1.OwnerReference
indexedOldRefs := indexByUID(oldMeta.GetOwnerReferences())
for _, ref := range blockingNewRefs {
oldRef, ok := indexedOldRefs[ref.UID]
if !ok {
// if ref is newly added, and it's blocking, then returns it.
ret = append(ret, ref)
continue
}
wasNotBlocking := oldRef.BlockOwnerDeletion == nil || *oldRef.BlockOwnerDeletion == false
if wasNotBlocking {
ret = append(ret, ref)
}
}
return ret
}
func (a *gcPermissionsEnforcement) SetAuthorizer(authorizer authorizer.Authorizer) {
a.authorizer = authorizer
}
func (a *gcPermissionsEnforcement) SetRESTMapper(restMapper meta.RESTMapper) {
a.restMapper = restMapper
}
func (a *gcPermissionsEnforcement) ValidateInitialization() error {
if a.authorizer == nil {
return fmt.Errorf("missing authorizer")
}
if a.restMapper == nil {
return fmt.Errorf("missing restMapper")
}
return nil
}

View File

@ -0,0 +1,522 @@
/*
Copyright 2016 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 gc
import (
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
type fakeAuthorizer struct{}
func (fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
username := a.GetUser().GetName()
if username == "non-deleter" {
if a.GetVerb() == "delete" {
return authorizer.DecisionNoOpinion, "", nil
}
if a.GetVerb() == "update" && a.GetSubresource() == "finalizers" {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionAllow, "", nil
}
if username == "non-pod-deleter" {
if a.GetVerb() == "delete" && a.GetResource() == "pods" {
return authorizer.DecisionNoOpinion, "", nil
}
if a.GetVerb() == "update" && a.GetResource() == "pods" && a.GetSubresource() == "finalizers" {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionAllow, "", nil
}
if username == "non-rc-deleter" {
if a.GetVerb() == "delete" && a.GetResource() == "replicationcontrollers" {
return authorizer.DecisionNoOpinion, "", nil
}
if a.GetVerb() == "update" && a.GetResource() == "replicationcontrollers" && a.GetSubresource() == "finalizers" {
return authorizer.DecisionNoOpinion, "", nil
}
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionAllow, "", nil
}
// newGCPermissionsEnforcement returns the admission controller configured for testing.
func newGCPermissionsEnforcement() (*gcPermissionsEnforcement, error) {
// the pods/status endpoint is ignored by this plugin since old kubelets
// corrupt them. the pod status strategy ensures status updates cannot mutate
// ownerRef.
whiteList := []whiteListItem{
{
groupResource: schema.GroupResource{Resource: "pods"},
subresource: "status",
},
}
gcAdmit := &gcPermissionsEnforcement{
Handler: admission.NewHandler(admission.Create, admission.Update),
whiteList: whiteList,
}
genericPluginInitializer := initializer.New(nil, nil, fakeAuthorizer{}, nil)
pluginInitializer := kubeadmission.NewPluginInitializer(nil, nil, nil, legacyscheme.Registry.RESTMapper(), nil)
initializersChain := admission.PluginInitializers{}
initializersChain = append(initializersChain, genericPluginInitializer)
initializersChain = append(initializersChain, pluginInitializer)
initializersChain.Initialize(gcAdmit)
return gcAdmit, nil
}
func TestGCAdmission(t *testing.T) {
expectNoError := func(err error) bool {
return err == nil
}
expectCantSetOwnerRefError := func(err error) bool {
return strings.Contains(err.Error(), "cannot set an ownerRef on a resource you can't delete")
}
tests := []struct {
name string
username string
resource schema.GroupVersionResource
subresource string
oldObj runtime.Object
newObj runtime.Object
checkError func(error) bool
}{
{
name: "super-user, create, no objectref change",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{},
checkError: expectNoError,
},
{
name: "super-user, create, objectref change",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectNoError,
},
{
name: "non-deleter, create, no objectref change",
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{},
checkError: expectNoError,
},
{
name: "non-deleter, create, objectref change",
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectCantSetOwnerRefError,
},
{
name: "non-pod-deleter, create, no objectref change",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{},
checkError: expectNoError,
},
{
name: "non-pod-deleter, create, objectref change",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectCantSetOwnerRefError,
},
{
name: "non-pod-deleter, create, objectref change, but not a pod",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("not-pods"),
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectNoError,
},
{
name: "super-user, update, no objectref change",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{},
checkError: expectNoError,
},
{
name: "super-user, update, no objectref change two",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectNoError,
},
{
name: "super-user, update, objectref change",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectNoError,
},
{
name: "non-deleter, update, no objectref change",
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{},
checkError: expectNoError,
},
{
name: "non-deleter, update, no objectref change two",
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectNoError,
},
{
name: "non-deleter, update, objectref change",
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectCantSetOwnerRefError,
},
{
name: "non-deleter, update, objectref change two",
username: "non-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}, {Name: "second"}}}},
checkError: expectCantSetOwnerRefError,
},
{
name: "non-pod-deleter, update, no objectref change",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{},
checkError: expectNoError,
},
{
name: "non-pod-deleter, update status, objectref change",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
subresource: "status",
oldObj: &api.Pod{},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectNoError,
},
{
name: "non-pod-deleter, update, objectref change",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
checkError: expectCantSetOwnerRefError,
},
{
name: "non-pod-deleter, update, objectref change, but not a pod",
username: "non-pod-deleter",
resource: api.SchemeGroupVersion.WithResource("not-pods"),
oldObj: &api.Pod{},
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
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)
err := gcAdmit.Validate(attributes)
if !tc.checkError(err) {
t.Errorf("%v: unexpected err: %v", tc.name, 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)
}
return &api.Pod{
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: refSlice,
},
}
}
getTrueVar := func() *bool {
ret := true
return &ret
}
getFalseVar := func() *bool {
ret := false
return &ret
}
blockRC1 := metav1.OwnerReference{
APIVersion: "v1",
Kind: "ReplicationController",
Name: "rc1",
BlockOwnerDeletion: getTrueVar(),
}
blockRC2 := metav1.OwnerReference{
APIVersion: "v1",
Kind: "ReplicationController",
Name: "rc2",
BlockOwnerDeletion: getTrueVar(),
}
notBlockRC1 := metav1.OwnerReference{
APIVersion: "v1",
Kind: "ReplicationController",
Name: "rc1",
BlockOwnerDeletion: getFalseVar(),
}
notBlockRC2 := metav1.OwnerReference{
APIVersion: "v1",
Kind: "ReplicationController",
Name: "rc2",
BlockOwnerDeletion: getFalseVar(),
}
nilBlockRC1 := metav1.OwnerReference{
APIVersion: "v1",
Kind: "ReplicationController",
Name: "rc1",
}
nilBlockRC2 := metav1.OwnerReference{
APIVersion: "v1",
Kind: "ReplicationController",
Name: "rc2",
}
blockDS1 := metav1.OwnerReference{
APIVersion: "extensions/v1beta1",
Kind: "DaemonSet",
Name: "ds1",
BlockOwnerDeletion: getTrueVar(),
}
notBlockDS1 := metav1.OwnerReference{
APIVersion: "extensions/v1beta1",
Kind: "DaemonSet",
Name: "ds1",
BlockOwnerDeletion: getFalseVar(),
}
expectNoError := func(err error) bool {
return err == nil
}
expectCantSetBlockOwnerDeletionError := func(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "cannot set blockOwnerDeletion if an ownerReference refers to a resource you can't set finalizers on")
}
tests := []struct {
name string
username string
resource schema.GroupVersionResource
subresource string
oldObj runtime.Object
newObj runtime.Object
checkError func(error) bool
}{
// cases for create
{
name: "super-user, create, no ownerReferences",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(),
checkError: expectNoError,
},
{
name: "super-user, create, all ownerReferences have blockOwnerDeletion=false",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(notBlockRC1, notBlockRC2),
checkError: expectNoError,
},
{
name: "super-user, create, some ownerReferences have blockOwnerDeletion=true",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(blockRC1, blockRC2),
checkError: expectNoError,
},
{
name: "non-rc-deleter, create, no ownerReferences",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(),
checkError: expectNoError,
},
{
name: "non-rc-deleter, create, all ownerReferences have blockOwnerDeletion=false or nil",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(notBlockRC1, nilBlockRC2),
checkError: expectNoError,
},
{
name: "non-rc-deleter, create, some ownerReferences have blockOwnerDeletion=true",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(blockRC1, notBlockRC2),
checkError: expectCantSetBlockOwnerDeletionError,
},
{
name: "non-rc-deleter, create, some ownerReferences have blockOwnerDeletion=true, but are pointing to daemonset",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
newObj: podWithOwnerRefs(blockDS1),
checkError: expectNoError,
},
// cases are for update
{
name: "super-user, update, no ownerReferences change blockOwnerDeletion",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(nilBlockRC1),
newObj: podWithOwnerRefs(notBlockRC1),
checkError: expectNoError,
},
{
name: "super-user, update, some ownerReferences change to blockOwnerDeletion=true",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(notBlockRC1),
newObj: podWithOwnerRefs(blockRC1),
checkError: expectNoError,
},
{
name: "super-user, update, add new ownerReferences with blockOwnerDeletion=true",
username: "super",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(),
newObj: podWithOwnerRefs(blockRC1),
checkError: expectNoError,
},
{
name: "non-rc-deleter, update, no ownerReferences change blockOwnerDeletion",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(nilBlockRC1),
newObj: podWithOwnerRefs(notBlockRC1),
checkError: expectNoError,
},
{
name: "non-rc-deleter, update, some ownerReferences change from blockOwnerDeletion=false to true",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(notBlockRC1),
newObj: podWithOwnerRefs(blockRC1),
checkError: expectCantSetBlockOwnerDeletionError,
},
{
name: "non-rc-deleter, update, some ownerReferences change from blockOwnerDeletion=nil to true",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(nilBlockRC1),
newObj: podWithOwnerRefs(blockRC1),
checkError: expectCantSetBlockOwnerDeletionError,
},
{
name: "non-rc-deleter, update, some ownerReferences change from blockOwnerDeletion=true to false",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(blockRC1),
newObj: podWithOwnerRefs(notBlockRC1),
checkError: expectNoError,
},
{
name: "non-rc-deleter, update, some ownerReferences change blockOwnerDeletion, but all such references are to daemonset",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(notBlockDS1),
newObj: podWithOwnerRefs(blockDS1),
checkError: expectNoError,
},
{
name: "non-rc-deleter, update, add new ownerReferences with blockOwnerDeletion=nil or false",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(),
newObj: podWithOwnerRefs(notBlockRC1, nilBlockRC2),
checkError: expectNoError,
},
{
name: "non-rc-deleter, update, add new ownerReferences with blockOwnerDeletion=true",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(),
newObj: podWithOwnerRefs(blockRC1),
checkError: expectCantSetBlockOwnerDeletionError,
},
{
name: "non-rc-deleter, update, add new ownerReferences with blockOwnerDeletion=true, but the references are to daemonset",
username: "non-rc-deleter",
resource: api.SchemeGroupVersion.WithResource("pods"),
oldObj: podWithOwnerRefs(),
newObj: podWithOwnerRefs(blockDS1),
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)
err := gcAdmit.Validate(attributes)
if !tc.checkError(err) {
t.Errorf("%v: unexpected err: %v", tc.name, err)
}
}
}

View File

@ -0,0 +1,63 @@
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",
"config.go",
"doc.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy",
deps = [
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/imagepolicy/install:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/imagepolicy/v1alpha1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/cache:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"admission_test.go",
"certs_test.go",
"config_test.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/imagepolicy/install:go_default_library",
"//vendor/k8s.io/api/imagepolicy/v1alpha1: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/client-go/tools/clientcmd/api/v1: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

@ -0,0 +1,251 @@
/*
Copyright 2016 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 imagepolicy contains an admission controller that configures a webhook to which policy
// decisions are delegated.
package imagepolicy
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/golang/glog"
"k8s.io/api/imagepolicy/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
// install the clientgo image policy API for use with api registry
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
)
var (
groupVersions = []schema.GroupVersion{v1alpha1.SchemeGroupVersion}
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("ImagePolicyWebhook", func(config io.Reader) (admission.Interface, error) {
newImagePolicyWebhook, err := NewImagePolicyWebhook(config)
if err != nil {
return nil, err
}
return newImagePolicyWebhook, nil
})
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*admission.Handler
webhook *webhook.GenericWebhook
responseCache *cache.LRUExpireCache
allowTTL time.Duration
denyTTL time.Duration
retryBackoff time.Duration
defaultAllow bool
}
var _ admission.ValidationInterface = &Plugin{}
func (a *Plugin) statusTTL(status v1alpha1.ImageReviewStatus) time.Duration {
if status.Allowed {
return a.allowTTL
}
return a.denyTTL
}
// Filter out annotations that don't match *.image-policy.k8s.io/*
func (a *Plugin) filterAnnotations(allAnnotations map[string]string) map[string]string {
annotations := make(map[string]string)
for k, v := range allAnnotations {
if strings.Contains(k, ".image-policy.k8s.io/") {
annotations[k] = v
}
}
return annotations
}
// Function to call on webhook failure; behavior determined by defaultAllow flag
func (a *Plugin) webhookError(pod *api.Pod, attributes admission.Attributes, err error) error {
if err != nil {
glog.V(2).Infof("error contacting webhook backend: %s", err)
if a.defaultAllow {
annotations := pod.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[api.ImagePolicyFailedOpenKey] = "true"
pod.ObjectMeta.SetAnnotations(annotations)
glog.V(2).Infof("resource allowed in spite of webhook backend failure")
return nil
}
glog.V(2).Infof("resource not allowed due to webhook backend failure ")
return admission.NewForbidden(attributes, err)
}
return nil
}
// Validate makes an admission decision based on the request attributes
func (a *Plugin) Validate(attributes admission.Attributes) (err error) {
// Ignore all calls to subresources or resources other than pods.
if attributes.GetSubresource() != "" || attributes.GetResource().GroupResource() != api.Resource("pods") {
return nil
}
pod, ok := attributes.GetObject().(*api.Pod)
if !ok {
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
}
// Build list of ImageReviewContainerSpec
var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec
containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
containers = append(containers, pod.Spec.Containers...)
containers = append(containers, pod.Spec.InitContainers...)
for _, c := range containers {
imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{
Image: c.Image,
})
}
imageReview := v1alpha1.ImageReview{
Spec: v1alpha1.ImageReviewSpec{
Containers: imageReviewContainerSpecs,
Annotations: a.filterAnnotations(pod.Annotations),
Namespace: attributes.GetNamespace(),
},
}
if err := a.admitPod(pod, attributes, &imageReview); err != nil {
return admission.NewForbidden(attributes, err)
}
return nil
}
func (a *Plugin) admitPod(pod *api.Pod, attributes admission.Attributes, review *v1alpha1.ImageReview) error {
cacheKey, err := json.Marshal(review.Spec)
if err != nil {
return err
}
if entry, ok := a.responseCache.Get(string(cacheKey)); ok {
review.Status = entry.(v1alpha1.ImageReviewStatus)
} else {
result := a.webhook.WithExponentialBackoff(func() rest.Result {
return a.webhook.RestClient.Post().Body(review).Do()
})
if err := result.Error(); err != nil {
return a.webhookError(pod, attributes, err)
}
var statusCode int
if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
return a.webhookError(pod, attributes, fmt.Errorf("Error contacting webhook: %d", statusCode))
}
if err := result.Into(review); err != nil {
return a.webhookError(pod, attributes, err)
}
a.responseCache.Add(string(cacheKey), review.Status, a.statusTTL(review.Status))
}
if !review.Status.Allowed {
if len(review.Status.Reason) > 0 {
return fmt.Errorf("image policy webhook backend denied one or more images: %s", review.Status.Reason)
}
return errors.New("one or more images rejected by webhook backend")
}
return nil
}
// NewImagePolicyWebhook a new ImagePolicyWebhook plugin from the provided config file.
// The config file is specified by --admission-control-config-file and has the
// following format for a webhook:
//
// {
// "imagePolicy": {
// "kubeConfigFile": "path/to/kubeconfig/for/backend",
// "allowTTL": 30, # time in s to cache approval
// "denyTTL": 30, # time in s to cache denial
// "retryBackoff": 500, # time in ms to wait between retries
// "defaultAllow": true # determines behavior if the webhook backend fails
// }
// }
//
// The config file may be json or yaml.
//
// The kubeconfig property refers to another file in the kubeconfig format which
// specifies how to connect to the webhook backend.
//
// The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer.
//
// # clusters refers to the remote service.
// clusters:
// - name: name-of-remote-imagepolicy-service
// cluster:
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
// server: https://images.example.com/policy # URL of remote service to query. Must use 'https'.
//
// # users refers to the API server's webhook configuration.
// users:
// - name: name-of-api-server
// user:
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
// client-key: /path/to/key.pem # key matching the cert
//
// For additional HTTP configuration, refer to the kubeconfig documentation
// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
func NewImagePolicyWebhook(configFile io.Reader) (*Plugin, error) {
if configFile == nil {
return nil, fmt.Errorf("no config specified")
}
// TODO: move this to a versioned configuration file format
var config AdmissionConfig
d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
err := d.Decode(&config)
if err != nil {
return nil, err
}
whConfig := config.ImagePolicyWebhook
if err := normalizeWebhookConfig(&whConfig); err != nil {
return nil, err
}
gw, err := webhook.NewGenericWebhook(legacyscheme.Registry, legacyscheme.Codecs, whConfig.KubeConfigFile, groupVersions, whConfig.RetryBackoff)
if err != nil {
return nil, err
}
return &Plugin{
Handler: admission.NewHandler(admission.Create, admission.Update),
webhook: gw,
responseCache: cache.NewLRUExpireCache(1024),
allowTTL: whConfig.AllowTTL,
denyTTL: whConfig.DenyTTL,
defaultAllow: whConfig.DefaultAllow,
}, nil
}

View File

@ -0,0 +1,948 @@
/*
Copyright 2016 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 imagepolicy
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"math/rand"
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"testing"
"time"
"k8s.io/api/imagepolicy/v1alpha1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/tools/clientcmd/api/v1"
api "k8s.io/kubernetes/pkg/apis/core"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"text/template"
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
)
const defaultConfigTmplJSON = `
{
"imagePolicy": {
"kubeConfigFile": "{{ .KubeConfig }}",
"allowTTL": {{ .AllowTTL }},
"denyTTL": {{ .DenyTTL }},
"retryBackoff": {{ .RetryBackoff }},
"defaultAllow": {{ .DefaultAllow }}
}
}
`
const defaultConfigTmplYAML = `
imagePolicy:
kubeConfigFile: "{{ .KubeConfig }}"
allowTTL: {{ .AllowTTL }}
denyTTL: {{ .DenyTTL }}
retryBackoff: {{ .RetryBackoff }}
defaultAllow: {{ .DefaultAllow }}
`
func TestNewFromConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
data := struct {
CA string
Cert string
Key string
}{
CA: filepath.Join(dir, "ca.pem"),
Cert: filepath.Join(dir, "clientcert.pem"),
Key: filepath.Join(dir, "clientkey.pem"),
}
files := []struct {
name string
data []byte
}{
{data.CA, caCert},
{data.Cert, clientCert},
{data.Key, clientKey},
}
for _, file := range files {
if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
t.Fatal(err)
}
}
tests := []struct {
msg string
kubeConfigTmpl string
wantErr bool
}{
{
msg: "a single cluster and single user",
kubeConfigTmpl: `
clusters:
- cluster:
certificate-authority: {{ .CA }}
server: https://admission.example.com
name: foobar
users:
- name: a cluster
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
`,
wantErr: true,
},
{
msg: "multiple clusters with no context",
kubeConfigTmpl: `
clusters:
- cluster:
certificate-authority: {{ .CA }}
server: https://admission.example.com
name: foobar
- cluster:
certificate-authority: a bad certificate path
server: https://admission.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
`,
wantErr: true,
},
{
msg: "multiple clusters with a context",
kubeConfigTmpl: `
clusters:
- cluster:
certificate-authority: a bad certificate path
server: https://admission.example.com
name: foobar
- cluster:
certificate-authority: {{ .CA }}
server: https://admission.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
contexts:
- name: default
context:
cluster: barfoo
user: a name
current-context: default
`,
wantErr: false,
},
{
msg: "cluster with bad certificate path specified",
kubeConfigTmpl: `
clusters:
- cluster:
certificate-authority: a bad certificate path
server: https://admission.example.com
name: foobar
- cluster:
certificate-authority: {{ .CA }}
server: https://admission.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
contexts:
- name: default
context:
cluster: foobar
user: a name
current-context: default
`,
wantErr: true,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
err := func() error {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
p := tempfile.Name()
defer os.Remove(p)
tmpl, err := template.New("test").Parse(tt.kubeConfigTmpl)
if err != nil {
return fmt.Errorf("failed to parse test template: %v", err)
}
if err := tmpl.Execute(tempfile, data); err != nil {
return fmt.Errorf("failed to execute test template: %v", err)
}
tempconfigfile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
pc := tempconfigfile.Name()
defer os.Remove(pc)
configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplJSON)
if err != nil {
return fmt.Errorf("failed to parse test template: %v", err)
}
dataConfig := struct {
KubeConfig string
AllowTTL int
DenyTTL int
RetryBackoff int
DefaultAllow bool
}{
KubeConfig: p,
AllowTTL: 500,
DenyTTL: 500,
RetryBackoff: 500,
DefaultAllow: true,
}
if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil {
return fmt.Errorf("failed to execute test template: %v", err)
}
// Create a new admission controller
configFile, err := os.Open(pc)
if err != nil {
return fmt.Errorf("failed to read test config: %v", err)
}
defer configFile.Close()
_, err = NewImagePolicyWebhook(configFile)
return err
}()
if err != nil && !tt.wantErr {
t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
}
if err == nil && tt.wantErr {
t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
}
}
}
// Service mocks a remote service.
type Service interface {
Review(*v1alpha1.ImageReview)
HTTPStatusCode() int
}
// NewTestServer wraps a Service as an httptest.Server.
func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
var tlsConfig *tls.Config
if cert != nil {
cert, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
}
if caCert != nil {
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert)
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
tlsConfig.ClientCAs = rootCAs
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
var review v1alpha1.ImageReview
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
return
}
if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
http.Error(w, "HTTP Error", s.HTTPStatusCode())
return
}
s.Review(&review)
type status struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason"`
}
resp := struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Status status `json:"status"`
}{
APIVersion: v1alpha1.SchemeGroupVersion.String(),
Kind: "ImageReview",
Status: status{review.Status.Allowed, review.Status.Reason},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
server.TLS = tlsConfig
server.StartTLS()
return server, nil
}
// A service that can be set to allow all or deny all authorization requests.
type mockService struct {
allow bool
statusCode int
}
func (m *mockService) Review(r *v1alpha1.ImageReview) {
r.Status.Allowed = m.allow
// hardcoded overrides
if r.Spec.Containers[0].Image == "good" {
r.Status.Allowed = true
}
for _, c := range r.Spec.Containers {
if c.Image == "bad" {
r.Status.Allowed = false
}
}
if !r.Status.Allowed {
r.Status.Reason = "not allowed"
}
}
func (m *mockService) Allow() { m.allow = true }
func (m *mockService) Deny() { m.allow = false }
func (m *mockService) HTTPStatusCode() int { return m.statusCode }
// newImagePolicyWebhook creates a temporary kubeconfig file from the provided arguments and attempts to load
// a new newImagePolicyWebhook from it.
func newImagePolicyWebhook(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, defaultAllow bool) (*Plugin, error) {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
p := tempfile.Name()
defer os.Remove(p)
config := v1.Config{
Clusters: []v1.NamedCluster{
{
Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca},
},
},
AuthInfos: []v1.NamedAuthInfo{
{
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
},
},
}
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
return nil, err
}
tempconfigfile, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
pc := tempconfigfile.Name()
defer os.Remove(pc)
configTmpl, err := template.New("testconfig").Parse(defaultConfigTmplYAML)
if err != nil {
return nil, fmt.Errorf("failed to parse test template: %v", err)
}
dataConfig := struct {
KubeConfig string
AllowTTL int64
DenyTTL int64
RetryBackoff int64
DefaultAllow bool
}{
KubeConfig: p,
AllowTTL: cacheTime.Nanoseconds(),
DenyTTL: cacheTime.Nanoseconds(),
RetryBackoff: 0,
DefaultAllow: defaultAllow,
}
if err := configTmpl.Execute(tempconfigfile, dataConfig); err != nil {
return nil, fmt.Errorf("failed to execute test template: %v", err)
}
// Create a new admission controller
configFile, err := os.Open(pc)
if err != nil {
return nil, fmt.Errorf("failed to read test config: %v", err)
}
defer configFile.Close()
wh, err := NewImagePolicyWebhook(configFile)
if err != nil {
return nil, err
}
return wh, err
}
func TestTLSConfig(t *testing.T) {
tests := []struct {
test string
clientCert, clientKey, clientCA []byte
serverCert, serverKey, serverCA []byte
wantAllowed, wantErr bool
}{
{
test: "TLS setup between client and server",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
wantAllowed: true,
},
{
test: "Server does not require client auth",
clientCA: caCert,
serverCert: serverCert, serverKey: serverKey,
wantAllowed: true,
},
{
test: "Server does not require client auth, client provides it",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey,
wantAllowed: true,
},
{
test: "Client does not trust server",
clientCert: clientCert, clientKey: clientKey,
serverCert: serverCert, serverKey: serverKey,
wantErr: true,
},
{
test: "Server does not trust client",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
wantErr: true,
},
{
// Plugin does not support insecure configurations.
test: "Server is using insecure connection",
wantErr: true,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
func() {
service := new(mockService)
service.statusCode = 200
server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
if err != nil {
t.Errorf("%s: failed to create server: %v", tt.test, err)
return
}
defer server.Close()
wh, err := newImagePolicyWebhook(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, -1, false)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
}
pod := goodPod(strconv.Itoa(rand.Intn(1000)))
attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
// Allow all and see if we get an error.
service.Allow()
err = wh.Validate(attr)
if tt.wantAllowed {
if err != nil {
t.Errorf("expected successful admission")
}
} else {
if err == nil {
t.Errorf("expected failed admission")
}
}
if tt.wantErr {
if err == nil {
t.Errorf("expected error making admission request: %v", err)
}
return
}
if err != nil {
t.Errorf("%s: failed to admit with AllowAll policy: %v", tt.test, err)
return
}
service.Deny()
if err := wh.Validate(attr); err == nil {
t.Errorf("%s: incorrectly admitted with DenyAll policy", tt.test)
}
}()
}
}
type webhookCacheTestCase struct {
statusCode int
expectedErr bool
expectedAuthorized bool
expectedCached bool
}
func testWebhookCacheCases(t *testing.T, serv *mockService, wh *Plugin, attr admission.Attributes, tests []webhookCacheTestCase) {
for _, test := range tests {
serv.statusCode = test.statusCode
err := wh.Validate(attr)
authorized := err == nil
if test.expectedErr && err == nil {
t.Errorf("Expected error")
} else if !test.expectedErr && err != nil {
t.Fatal(err)
}
if test.expectedAuthorized && !authorized {
if test.expectedCached {
t.Errorf("Webhook should have successful response cached, but authorizer reported unauthorized.")
} else {
t.Errorf("Webhook returned HTTP %d, but authorizer reported unauthorized.", test.statusCode)
}
} else if !test.expectedAuthorized && authorized {
t.Errorf("Webhook returned HTTP %d, but authorizer reported success.", test.statusCode)
}
}
}
// TestWebhookCache verifies that error responses from the server are not
// cached, but successful responses are.
func TestWebhookCache(t *testing.T) {
serv := new(mockService)
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
// Create an admission controller that caches successful responses.
wh, err := newImagePolicyWebhook(s.URL, clientCert, clientKey, caCert, 200, false)
if err != nil {
t.Fatal(err)
}
tests := []webhookCacheTestCase{
{statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false},
{statusCode: 404, expectedErr: true, expectedAuthorized: false, expectedCached: false},
{statusCode: 403, expectedErr: true, expectedAuthorized: false, expectedCached: false},
{statusCode: 401, expectedErr: true, expectedAuthorized: false, expectedCached: false},
{statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false},
{statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true},
}
attr := admission.NewAttributesRecord(goodPod("test"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
serv.allow = true
testWebhookCacheCases(t, serv, wh, attr, tests)
// For a different request, webhook should be called again.
tests = []webhookCacheTestCase{
{statusCode: 500, expectedErr: true, expectedAuthorized: false, expectedCached: false},
{statusCode: 200, expectedErr: false, expectedAuthorized: true, expectedCached: false},
{statusCode: 500, expectedErr: false, expectedAuthorized: true, expectedCached: true},
}
attr = admission.NewAttributesRecord(goodPod("test2"), nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
testWebhookCacheCases(t, serv, wh, attr, tests)
}
func TestContainerCombinations(t *testing.T) {
tests := []struct {
test string
pod *api.Pod
wantAllowed, wantErr bool
}{
{
test: "Single container allowed",
pod: goodPod("good"),
wantAllowed: true,
},
{
test: "Single container denied",
pod: goodPod("bad"),
wantAllowed: false,
wantErr: true,
},
{
test: "One good container, one bad",
pod: &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: "bad",
SecurityContext: &api.SecurityContext{},
},
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
},
},
},
wantAllowed: false,
wantErr: true,
},
{
test: "Multiple good containers",
pod: &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
},
},
},
wantAllowed: true,
wantErr: false,
},
{
test: "Multiple bad containers",
pod: &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: "bad",
SecurityContext: &api.SecurityContext{},
},
{
Image: "bad",
SecurityContext: &api.SecurityContext{},
},
},
},
},
wantAllowed: false,
wantErr: true,
},
{
test: "Good container, bad init container",
pod: &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
},
InitContainers: []api.Container{
{
Image: "bad",
SecurityContext: &api.SecurityContext{},
},
},
},
},
wantAllowed: false,
wantErr: true,
},
{
test: "Bad container, good init container",
pod: &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: "bad",
SecurityContext: &api.SecurityContext{},
},
},
InitContainers: []api.Container{
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
},
},
},
wantAllowed: false,
wantErr: true,
},
{
test: "Good container, good init container",
pod: &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
},
InitContainers: []api.Container{
{
Image: "good",
SecurityContext: &api.SecurityContext{},
},
},
},
},
wantAllowed: true,
wantErr: false,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
func() {
service := new(mockService)
service.statusCode = 200
server, err := NewTestServer(service, serverCert, serverKey, caCert)
if err != nil {
t.Errorf("%s: failed to create server: %v", tt.test, err)
return
}
defer server.Close()
wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, false)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
}
attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
err = wh.Validate(attr)
if tt.wantAllowed {
if err != nil {
t.Errorf("expected successful admission: %s", tt.test)
}
} else {
if err == nil {
t.Errorf("expected failed admission: %s", tt.test)
}
}
if tt.wantErr {
if err == nil {
t.Errorf("expected error making admission request: %v", err)
}
return
}
if err != nil {
t.Errorf("%s: failed to admit: %v", tt.test, err)
return
}
}()
}
}
func TestDefaultAllow(t *testing.T) {
tests := []struct {
test string
pod *api.Pod
wantAllowed, wantErr, defaultAllow bool
}{
{
test: "DefaultAllow = true, backend unreachable, bad image",
pod: goodPod("bad"),
defaultAllow: true,
wantAllowed: true,
},
{
test: "DefaultAllow = true, backend unreachable, good image",
pod: goodPod("good"),
defaultAllow: true,
wantAllowed: true,
},
{
test: "DefaultAllow = false, backend unreachable, good image",
pod: goodPod("good"),
defaultAllow: false,
wantAllowed: false,
wantErr: true,
},
{
test: "DefaultAllow = false, backend unreachable, bad image",
pod: goodPod("bad"),
defaultAllow: false,
wantAllowed: false,
wantErr: true,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
func() {
service := new(mockService)
service.statusCode = 500
server, err := NewTestServer(service, serverCert, serverKey, caCert)
if err != nil {
t.Errorf("%s: failed to create server: %v", tt.test, err)
return
}
defer server.Close()
wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, tt.defaultAllow)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
}
attr := admission.NewAttributesRecord(tt.pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
err = wh.Validate(attr)
if tt.wantAllowed {
if err != nil {
t.Errorf("expected successful admission")
}
} else {
if err == nil {
t.Errorf("expected failed admission")
}
}
if tt.wantErr {
if err == nil {
t.Errorf("expected error making admission request: %v", err)
}
return
}
if err != nil {
t.Errorf("%s: failed to admit: %v", tt.test, err)
return
}
}()
}
}
// A service that can record annotations sent to it
type annotationService struct {
annotations map[string]string
}
func (a *annotationService) Review(r *v1alpha1.ImageReview) {
a.annotations = make(map[string]string)
for k, v := range r.Spec.Annotations {
a.annotations[k] = v
}
r.Status.Allowed = true
}
func (a *annotationService) HTTPStatusCode() int { return 200 }
func (a *annotationService) Annotations() map[string]string { return a.annotations }
func TestAnnotationFiltering(t *testing.T) {
tests := []struct {
test string
annotations map[string]string
outAnnotations map[string]string
}{
{
test: "all annotations filtered out",
annotations: map[string]string{
"test": "test",
"another": "annotation",
"": "",
},
outAnnotations: map[string]string{},
},
{
test: "image-policy annotations allowed",
annotations: map[string]string{
"my.image-policy.k8s.io/test": "test",
"other.image-policy.k8s.io/test2": "annotation",
"test": "test",
"another": "another",
"": "",
},
outAnnotations: map[string]string{
"my.image-policy.k8s.io/test": "test",
"other.image-policy.k8s.io/test2": "annotation",
},
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
func() {
service := new(annotationService)
server, err := NewTestServer(service, serverCert, serverKey, caCert)
if err != nil {
t.Errorf("%s: failed to create server: %v", tt.test, err)
return
}
defer server.Close()
wh, err := newImagePolicyWebhook(server.URL, clientCert, clientKey, caCert, 0, true)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
}
pod := goodPod("test")
pod.Annotations = tt.annotations
attr := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "namespace", "", api.Resource("pods").WithVersion("version"), "", admission.Create, &user.DefaultInfo{})
err = wh.Validate(attr)
if err != nil {
t.Errorf("expected successful admission")
}
if !reflect.DeepEqual(tt.outAnnotations, service.Annotations()) {
t.Errorf("expected annotations sent to webhook: %v to match expected: %v", service.Annotations(), tt.outAnnotations)
}
}()
}
}
func goodPod(containerID string) *api.Pod {
return &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: "default",
SecurityContext: &api.PodSecurityContext{},
Containers: []api.Container{
{
Image: containerID,
SecurityContext: &api.SecurityContext{},
},
},
},
}
}

View File

@ -0,0 +1,211 @@
/*
Copyright 2016 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.
*/
// This file was generated using openssl by the gencerts.sh script
// and holds raw certificates for the imagepolicy webhook tests.
package imagepolicy
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAoKjaP9PtRAGRNCx8z+0LTGt2eEduqElcPrm8EvlBwn3dnLFo
55x+Tejb6ysQsyy1BKI0dRdX4tNSAgFFFaIVcsOo9kGtPq7QsSd4VWViNE3L5zJA
+0X2ztHBkPlQXwDrtArsNKxwcpyHP9sXE05BN36XBjAz2XkusTkFrdJ/PzjZhlb4
9i9gTZ0bJbexQ1+dfZX2WpY70JypYnKrbV1dLj5ORb65SC8IWZcG/ouqLWAN+lT+
eug8P6PjoOQWs3qsl0bSAtAdiYcwXKtPiBEWPJe24ACywyE+8jVzmIJqAm0U1V8k
GTHzjmSRwzgX/VN5JMri/nxNIW5UsbhHzYHfjQIDAQABAoIBAQCIeAWz1Bwl+ULT
U7rNkChZyKrAbsUDdBVEPtcQMuR2Bh5Z/KUEoHz1RwiP0WwFFsPI5NO0ZpjD1wdB
Jrz9LEoVyzfZvl4f8bTZ1pIzz8PEdBTxFVH3Xy3P7oMC15Q6rviIXgLYl2WJJYcJ
adxHDOD+96vnmMhiQbq01aAKT9TA6PvXXDusfadMQ+il+mEbeZz4aNYBk9u+34Co
aQTNwlLft5anW2820IMJdJR/bFjyX71cPID1rIjw4VOQZExIpIEnuHPiulyE4EvJ
hvvVKAm0dRjHg39cz0eAQ6PntX3DUvjNfcLLrj7sQxLco1cnAKZxhpZ8ajtvynr5
pF2d5xYBAoGBAM8y/e5+raHTLHEKZUc0vekUey3fc4aRqptyAKTS0ZvOYBXg4Vhl
mOK7066IEqwF4UHGmQqW6D5HstqPGx0uN0d9IyImUqDp0JotdFSZMEMQkYLyFD+r
J7O2nOO6E4SOxXO9/q9iSB+G/qgl6LS3O9+58uHTYEbUommiDZ6a18qBAoGBAMZ/
xSGMa3b6vrU3rUTEh+xBh6YRVNYAxWwpGg2sO0k2brT3SxSMCrx1wvNGY+k7XNx0
JJfZQDC/wlR0rcVTnPCi/cE9FTUlh23xXCPRlxwc4vLly+7yU95LhAO+N9XAwsrs
OIi4lR57jxoLNO2ofoAVMvllkE5Eo5W6lOPR2xcNAoGAV1Tv0OFV//pJJhAypfOm
BCLc1HX1dIfbOA+yE8bEEH7I4w/ZC3AvI4n1a//wls8Xpai2gs8ebnm7+gENdZww
MpKdB1zNwQMsKH/2I146CFpoap/sRvW2EzpqIFYiueGPefxf575uFdPJbEgmMF13
ABKZO/PjBZfEKO/j+7DaOYECgYBYX+Zqa1QlIrnpgKJZ7Y3+d6ZnH2w/4xQCdcIt
uDKlA+ECHN+GhFr7UQq8uOgenNlZJTRtjsHvclCYvWHoarOCx25mrEVW5iCHqF+3
asb2Mz4vmnPTLHx+iex6piPBvRJ8ufLpnBR3/9bUZ4znCo9XgxiwxLEcx551OR60
12fNuQKBgC1fkqgtDDxQzrabSmmiqXthcPXxFdsYqnSNlFgba0uaAp9LREztSrX8
QhwSoSwHVmjBvR6SybLYdsZ9Efj/w7XBejOOcS44MOoHYYFdsP7W47Ao5QFqvDoI
oqyQ1R73cF9WX6obRQwH4P3DvcsBebOjvjMX9mljKtpJMc9KqrGc
-----END RSA PRIVATE KEY-----`)
var caCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIJAJlL10mfdZraMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV
BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5
MDA1MjgyMDIyNTBaMCExHzAdBgNVBAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2Ew
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgqNo/0+1EAZE0LHzP7QtM
a3Z4R26oSVw+ubwS+UHCfd2csWjnnH5N6NvrKxCzLLUEojR1F1fi01ICAUUVohVy
w6j2Qa0+rtCxJ3hVZWI0TcvnMkD7RfbO0cGQ+VBfAOu0Cuw0rHBynIc/2xcTTkE3
fpcGMDPZeS6xOQWt0n8/ONmGVvj2L2BNnRslt7FDX519lfZaljvQnKlicqttXV0u
Pk5FvrlILwhZlwb+i6otYA36VP566Dw/o+Og5BazeqyXRtIC0B2JhzBcq0+IERY8
l7bgALLDIT7yNXOYgmoCbRTVXyQZMfOOZJHDOBf9U3kkyuL+fE0hblSxuEfNgd+N
AgMBAAGjUDBOMB0GA1UdDgQWBBSx2m5pJoFpdGDmOzSVl29jkheQFTAfBgNVHSME
GDAWgBSx2m5pJoFpdGDmOzSVl29jkheQFTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQBe6tZzmOQKt8fTsnDDKvEjSwK2Pb91R5tkwmIhdpTjmAgC+Zkk
kSihR9sZIxdRC4wlbuorRl8BjhX5I8Kr3FWdDhOrIhicp7CIrxPiFh6+ZLSOj3o9
pQ6SriIopjXCHvl5XjzKxLg/uQpzui/YUtfqffCRB4EccOsjlyUanK5rjMLBMLCn
2LadiRB2Q/cC9fYigczETACDjq5vzp6I9eqwpCTmv/+4bFncW+VBD4touaJc8FKf
ljW5xekKRh4uzP85X7rEgrFen/my5Fs/cylkFvYIiZwgn6NLgW3BNi+m31XIfU0S
xIbgh4UH0dwc6Zk8WUwFud4GXj6OyGneMGKB
-----END CERTIFICATE-----`)
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAnKpC89q4/H+Xg91xI+GLhkrpJrO4n3nw0+/EQUoF9qwLtEDk
mJp6ymUulwJgfvJwHOsUYqQB6jMKfXyqeSR24ssjjF9LTKhaQMZOGcW5Mshi04Ie
USX93wDZwbWwqihVSqWaMpmf3JByeldnXNtc29Ik6NwqZcNWW5kEsSszheLhOU4i
ZcRUlovwMYhHX37vQCQ1aygMaIMgBOb/vogSNxumqPKS4WdWsjss6LmEPnm350e+
+9cb6RAfrDlOaj32VbLEp500SfBpZeCuyc5v81X12HT4V0qmsqIZ79tIgrqAaPxE
D/HJXPpH64EAr23bR9dMPLXTh6w2yYWu+NGYywIDAQABAoIBAQCE/CZPN1gVxf0I
i12x9o/oVAhruN08Sld6oCm4viwnws1AmmExhNg8m/0bZIIi4Ir4kThBrzSM5/y8
nqlaofBk/cjULEQP80yBdZPwXp2hlOYG4on3mkdRGDjALQmktw4HimFFGJDRuq/i
V/U+plrBojWAkPtQXKsen9qSxbg7qhI6KZyUQKExIHhsCfmE1ZzGx+/bgLVJEagi
7zzZdAj2BzdoCk8yySAAsZG+pNSnd8gs5EzzRJ1RXanwxPSeEG/guX9YhLgLhhFu
XzXngJDKVVhz4F2TfxtqIvZYvTMNh0R1OE0OUO2P88M837KKk5BHvW9oqYKZTUFV
MC9k5No5AoGBAMtUBp8UcYZy+yetOAK2iGaEYwuWx8vwjY0c1POWun2Hny0nYxTQ
WxXXqKaJydxZ+DlD3XuRKmMlKZQsp+bzuL5ukWN/ipO5tgQQfuKOZqVwvL19GkFi
+Qr70G/TvYT/rv6A4s6XqbG4xt+7c2gf/XSghyoIyq1uwOcNNtrMdM/tAoGBAMU/
tYc4d+vAl7hd8TwhFiZiC3N84C1HwsPVj38uqQI/j8boB21Bhpw6HHzq+VdVPfvp
zk5e8AiQdSpitM7pBVmLpoRdTQjdlUDFRUi4TdJwfp5P7dXM8D6swNQ9f9w180na
5ewu16PSC+sh19wAl04KwOmiDqZujJrBgWnFcESXAoGBALGofoybAUK3zqlxWcJN
GUtyG1Sx72tLiXMmIQ+hwNsUGEoM4y75isy//ZVeSammVxQ6Lxjb00yD2RumFSLg
C6kg1Ro6A6xmFRriCuwL/rZJljB/UeSWBQLK2eoL+clu2sl3djWLIPOvft1YXVM6
uGwiI1fgDK+TWSvJSQfOo7ZVAoGBAK+A6DvQeqNBUb2xmJsvtU2hnx661Zx0ZU9q
DavUEHz3oS4R9cm4q9UFv6NGT2Tta6FhfzcsMdbs8dMs0EPqAeCS6S6M9aYVwl9H
J0Z09olvnrmt1KiPGJQrkcdGkSWWu0nTgxCK/UO9+OzVyALwY7AE0XEPyIk9g82O
r181VZcxAoGANY2QGYrNtfa++o2B0O4qskKxhYEeCnZPptmjVO0oHOx2YSDQXK3K
B0evCQ7ylvMnobNLjp9bqD14a0M86QjRlpSg1vHUhBsETZICc+E0UgV28CdWgYtt
urARDE9ZpLVSRfPVAitC1I76pZwevsbQ9TeS2p0cWQpYYKmBtGpkdug=
-----END RSA PRIVATE KEY-----`)
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
MIIDFzCCAf+gAwIBAgIJANQEJyMW4HFZMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV
BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5
MDA1MjgyMDIyNTBaMCExHzAdBgNVBAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2Ew
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCcqkLz2rj8f5eD3XEj4YuG
Sukms7ifefDT78RBSgX2rAu0QOSYmnrKZS6XAmB+8nAc6xRipAHqMwp9fKp5JHbi
yyOMX0tMqFpAxk4ZxbkyyGLTgh5RJf3fANnBtbCqKFVKpZoymZ/ckHJ6V2dc21zb
0iTo3Cplw1ZbmQSxKzOF4uE5TiJlxFSWi/AxiEdffu9AJDVrKAxogyAE5v++iBI3
G6ao8pLhZ1ayOyzouYQ+ebfnR7771xvpEB+sOU5qPfZVssSnnTRJ8Gll4K7Jzm/z
VfXYdPhXSqayohnv20iCuoBo/EQP8clc+kfrgQCvbdtH10w8tdOHrDbJha740ZjL
AgMBAAGjUDBOMB0GA1UdDgQWBBRjFVG818hHK+HSEhdz+gPwSKa4kzAfBgNVHSME
GDAWgBRjFVG818hHK+HSEhdz+gPwSKa4kzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4IBAQBBCl0UJq0iLy/dvym79mnoPZ1KPhS2WnQB5ZLzJwL26ePkr8j8
G/1AOVPu73hovJx51b+T7ZhTgtmAEwqpRHBxRQ0+Yf973YOVJYp4QFGWDnueurzv
bCsnZEPkQtccHzZxT3fUsM6Ejy99j0WBNmvfAj1X7yNaN5EZw6kvuaDDda3I7WNM
0eGy8aoAcPJZkYfZb39VDq/qJn+bVsAJdUaXt/FkDZBJl6XzoGjC/webjRJOpkgN
vgjJDhhQ8LlHFiq+lXIiK4Y55RBWG3iXGTM8W3fjZYTNvH7FlGyuRD4Y4hyaYXTP
+PoFWuDZM89EAyICr0yyTc8mkdrAEM/Lj9GO
-----END CERTIFICATE-----`)
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAyhmjG7BJCGwuf1FyHJtq9iUXZ3oymtrOHdaSAcsCSxFrUJTH
riPOe9d1ahH7bvsZycnzh7/pABdTDUdStiR8/1KUYt8PjjosrmmYyupqNPq+wkBD
EmKa+4voR2EBgXbIGghx8e++KmmNnSCNk6B8m2EJR0fn9zPnoY3uHNogKjCICt19
g+uipuwZco7yTu3e40LwpIVmA8SsrM0S/CaZqSmtIClSwv7YDvreUd6FuI/GT0cj
NMPRuSdfohBxGz6R7Cml7qP4AYKajjl+08mRYv3o+hVclXUltcRmTnJanYGmGS3k
C7KiE2sHINzF2qUUAoO+yXpMx7QtK+NS0PhjmQIDAQABAoIBAFOicmZ1+HM82a0k
llWSV5xPUzUmU6TT4bJlZnzJd0R7i+6H8250MPH9AwEHOgb+cPiZ02cdGx5HiL4Z
AviPdw7uLKwR5U0VdAIlfu6SPat5DNI0Z81G8x4gEtrfIRFjh4GGdykI7qh8j/cz
ToOGSaq/aGiQMEWTvEqWArD7742lVHE4/1bM3GuKV8shy31zfw0d9RCCy1GdBR75
zZ1w4zKL55DM3PC73Ndy2IcrViVXVAgfqD0xxKwQW1qoENgThueALj3PkU1XaKxI
nOdztt1fBFpcSHyFBkJ1sexumnssMRXSVcJ/0D5F2T4QPUnWBM0oSzoyioAab4RP
8XrZwAECgYEA/eFjNgCeHztXgS3YRC/RddLOtobrerYKN7vA64ou5VUCqEQ9rfQE
MbmKdZdiFVNJI0JrPq8Gx39ME9g2OLTVVqdtlm6JYjy5CHdUXHIHObo9oz7Uueos
TdeCf0LFvEUNXvbGIP5KqcdVi+wekauHMqXGQYTNa6bar/FE99MdyAECgYEAy8mU
tCjm4QsuKsdku5bDHGv56ZN9DkWd7Lcjie5otElwH9bKfIQ2lUYyoUAIa0rEJ9Ya
7vuAZ2bX7od9s8Jkci91ONDWxdy361SRZcbpuqgQKKVRuzGlfamufyW4sStbXY1k
+zeQxyWGJHhhLWpapzca89RELGZSkbIMVVIT25kCgYEA7EUYboZuoYQ5cGf476RM
28kfRXEUrvPBWJLr/IhyEk1mFrDDciM40AnrWHpU9qG23BCQ/BopRforFADQnT91
l5pje29NfdYjIUTkhtA79zZi7IyprofHSX453TOIECl3QxyH0Oa3F4ACFiDdZhXq
0XDDq+/quLfkp37y/2xDOAECgYEAmi55g5UumTWMSHFzlToLhIVtH3unMhUZ1u74
xHLMZRrq6ivoJy0g3u+tfrKjrAl1P26OEiHWlGULGj0Ireh1dq7RUZsv46OKw1HI
b+h/Den5z8bEf4ygWOL4UtqHUgQrrCw+KpNvxjxtsUoiu+mrjLf0fGYs7iq8bd73
1dWzkIECgYEAi6P/LzMC6orbyONmwlscqO1Ili8ZBkUjJ/wThkiNMMA3pyKmb68W
yt56Yh0rs+WnuVUN90cG87k+CY35dQ7FAOVUJi9LWGA3Oq9fGkoOB7f4dzaUu/rB
dtit2KPCxiKpZsxqSf4+S8AXYF48abNPLYK3DCCSqAah09gYOrqYlW4=
-----END RSA PRIVATE KEY-----`)
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIJAMvo2rkGpEUQMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV
BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5
MDA1MjgyMDIyNTBaMCUxIzAhBgNVBAMUGndlYmhvb2tfaW1hZ2Vwb2xpY3lfc2Vy
dmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyhmjG7BJCGwuf1Fy
HJtq9iUXZ3oymtrOHdaSAcsCSxFrUJTHriPOe9d1ahH7bvsZycnzh7/pABdTDUdS
tiR8/1KUYt8PjjosrmmYyupqNPq+wkBDEmKa+4voR2EBgXbIGghx8e++KmmNnSCN
k6B8m2EJR0fn9zPnoY3uHNogKjCICt19g+uipuwZco7yTu3e40LwpIVmA8SsrM0S
/CaZqSmtIClSwv7YDvreUd6FuI/GT0cjNMPRuSdfohBxGz6R7Cml7qP4AYKajjl+
08mRYv3o+hVclXUltcRmTnJanYGmGS3kC7KiE2sHINzF2qUUAoO+yXpMx7QtK+NS
0PhjmQIDAQABo0AwPjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAK
BggrBgEFBQcDATAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCF
xaS/KIijKDLbaL/P7AxhnAta8jYSEzL66WTaYV4GeRhLtX/vPUV9gzPWnkNr0TBM
lS+Q0KDxh17rJ/MrWwrMSwsgKZahTR+7mSHiXrIlHcnHXXSvhnoXu8VDu8goqOEI
5yRHt6plzmFZEwVi/hSmIAuQjmyjOk2dc/ZKI0fMExKhnVms8AoztjAMbt3TFMTK
Kk7bVGPblFsXiVPhRlzbLbh5i/PvHHf+12ACrVxoxOOQUmuXy1DPxmkk7jP3FIsE
+rnyWnfmGS5sW8oMkj2nFYIh3LehADsMS9s7JVlJk/loNJDA9Yn2fev/vRKck8RZ
siw54G4e+6nKpY5BAY1M
-----END CERTIFICATE-----`)
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA3IOqCz88jTQpsGIBFTdjbqBg+0NFeym3OEl8zLfzkLQuZieO
3AoFMiLaeYgC4m9BBsdJWSXRzWcqgVWIY8KU7c2SPfErlhP86VFoD0RKHJxwRVh0
y70WyK8+CzzwrrPpWydgtAwbm9F+0v/zdcCL0TEL2/MYgCc97mSGwtTRaW4bqq6V
MWMHBcOu44dHq8+CF8ixxk0WSBl2oocXnF7QdEA15iuOM5hacLB0fyH4T3NM54lO
rOSXUMUuysougSrMcCPv3esFlv4TVUkldwu73jWx+Wja0gNXlnmgU2lqFdM+PsVT
DPMzoHTEhIGPIWO5anYR5Qv0SmX3nXkNcx9QDQIDAQABAoIBADblRCC2pmFUmghB
7ZkVh9hTbrE+Zv6pPOZzTPE93hGo+WAO+v6GNBLuIEte87DhF2QTmovp4VfsFeXK
oECNgTvOEFkBP+OFqFGBJZGfY3/J5h0tTy4lLZXaImzzx8sGGNLLc8R+uyTIO3VV
qIso2uXB+vzPgMrueflt5yp7hoJjI0c+qEktUg5n+WJFAFteI9LCngN+xwRWVEgp
rjKVPcT9zio8tLJOhcSPA7q6lORUkwbPWHyNDpamvldnqjhgp5Ceq5f/qfoWPzvM
H5o72Ax2WduxST+P+hCOqZReUmTaGzAKb5rJwdEpmbnDZ3kSR08aT/40m/EG1SvQ
pi0b3QECgYEA/mRGIjaYPQr+tw3Sz8g76t3PYfrglro60HdLBn2IUpj2sEpazNId
2aPFPb58whL+VPmUfXbpPH+wW/+wWpRw4MraFkJanbOjDiEGXK5ZoUQIDZJWUSwf
oCge5uacU69weC67UyPYmK1e+A/gaFw1Dz729jLxtB3rGWKxEGbWEc0CgYEA3eiP
hv0GxbdEEbSfQoSPKbBHGI9spaqAIcqL+dSsx3m6Ckqx0El/xi9mQkITgqs2gyqI
o2T/3yDli9oF4+3Plz0wrZ11auOWX+nhKfACtF679I1PL0UOavXF0FVgOfwOIqdG
jp4QQV7USkbTP9ZOHo90Y8G4rmTEdMZ/VsH490ECgYEA8u/bsiyk8haf7Tx8SAWW
gtLUi2NEO20ZYZ+qvEYBe6+sVeqMD/HQo9ksMazKA6ST0Z6O2cpHLolaaGEjjz0X
FvVhk8RGOTglzQZoxvWRjtojPqKzX81dXlsyN5ufSqPOKlemeN1QqW1XtlmjGsaD
vU2KFs/L1xCDRbjkEx/B6zkCgYBmqeE9InKvpknnpxjHPWy+bL93rWMmgesltv9r
ZelJoBdiC4yYQGjM18EHhmpgWbWumU79yQxXvnB0czmmaa9Q2Q5cRCy+duxrE1kI
ffHCYNG0ImwwAlLZSTtrVxRdvy8K+Ti7YoVCuQyeEIZLUmpx2QyP2mAGzrfVDsB6
8uKsAQKBgQDO+PmADra91NKJP1iVuvOK8iEy/Z14L03uKtF3X9u8vLdzQZa/Q/P9
hXOX9ovFwSBQOOfgb+/+QRuPL4xxi1J8CFwrSWCEeFgrDijl9DS6aNY6BWHDA8p6
8V7Adb04cnenj8QjYYN8/mqsQlHSoAIxeAlUoJpq+pk7O8PAfbjgMw==
-----END RSA PRIVATE KEY-----`)
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIC+jCCAeKgAwIBAgIJAMvo2rkGpEURMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNV
BAMMFndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2EwIBcNMTYwODEyMjAyMjUwWhgPMjI5
MDA1MjgyMDIyNTBaMCUxIzAhBgNVBAMUGndlYmhvb2tfaW1hZ2Vwb2xpY3lfY2xp
ZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3IOqCz88jTQpsGIB
FTdjbqBg+0NFeym3OEl8zLfzkLQuZieO3AoFMiLaeYgC4m9BBsdJWSXRzWcqgVWI
Y8KU7c2SPfErlhP86VFoD0RKHJxwRVh0y70WyK8+CzzwrrPpWydgtAwbm9F+0v/z
dcCL0TEL2/MYgCc97mSGwtTRaW4bqq6VMWMHBcOu44dHq8+CF8ixxk0WSBl2oocX
nF7QdEA15iuOM5hacLB0fyH4T3NM54lOrOSXUMUuysougSrMcCPv3esFlv4TVUkl
dwu73jWx+Wja0gNXlnmgU2lqFdM+PsVTDPMzoHTEhIGPIWO5anYR5Qv0SmX3nXkN
cx9QDQIDAQABoy8wLTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAK
BggrBgEFBQcDAjANBgkqhkiG9w0BAQsFAAOCAQEAkHIhrPfRROhzLg2hRZz5/7Kw
3V0/Y0XS91YU3rew+c2k++bLp1INzpWxfB6gbSC6bTOgn/seIDvxwJ2g5DRdOxU/
Elcpqg1hTCVfpmra9PCniMzZuP7lsz8sJKj6FgE6ElJ1S74FW/CYz/jA+76LLot4
JwGkCJHzyLgFPBEOjJ/mLYSM/SDzHU5E+NHXVaKz4MjM3JwycN/juqi4ikAcZEBW
1HmpcHKBedAwlCM90zlvG2SL4sFRp/clMbntRdmh5L+/1F6aP82PO3iuvXtXP48d
NtjboxP3IV2eY5iUle8BOQ9CnFQs4wsF1LxTMNACypQyFinMsHrCpwrB3i4VvA==
-----END CERTIFICATE-----`)

View File

@ -0,0 +1,93 @@
/*
Copyright 2016 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 imagepolicy contains an admission controller that configures a webhook to which policy
// decisions are delegated.
package imagepolicy
import (
"fmt"
"time"
"github.com/golang/glog"
)
const (
defaultRetryBackoff = time.Duration(500) * time.Millisecond
minRetryBackoff = time.Duration(1)
maxRetryBackoff = time.Duration(5) * time.Minute
defaultAllowTTL = time.Duration(5) * time.Minute
defaultDenyTTL = time.Duration(30) * time.Second
minAllowTTL = time.Duration(1) * time.Second
maxAllowTTL = time.Duration(30) * time.Minute
minDenyTTL = time.Duration(1) * time.Second
maxDenyTTL = time.Duration(30) * time.Minute
useDefault = time.Duration(0) //sentinel for using default TTL
disableTTL = time.Duration(-1) //sentinel for disabling a TTL
)
// imagePolicyWebhookConfig holds config data for imagePolicyWebhook
type imagePolicyWebhookConfig struct {
KubeConfigFile string `json:"kubeConfigFile"`
AllowTTL time.Duration `json:"allowTTL"`
DenyTTL time.Duration `json:"denyTTL"`
RetryBackoff time.Duration `json:"retryBackoff"`
DefaultAllow bool `json:"defaultAllow"`
}
// AdmissionConfig holds config data for admission controllers
type AdmissionConfig struct {
ImagePolicyWebhook imagePolicyWebhookConfig `json:"imagePolicy"`
}
func normalizeWebhookConfig(config *imagePolicyWebhookConfig) (err error) {
config.RetryBackoff, err = normalizeConfigDuration("backoff", time.Millisecond, config.RetryBackoff, minRetryBackoff, maxRetryBackoff, defaultRetryBackoff)
if err != nil {
return err
}
config.AllowTTL, err = normalizeConfigDuration("allow cache", time.Second, config.AllowTTL, minAllowTTL, maxAllowTTL, defaultAllowTTL)
if err != nil {
return err
}
config.DenyTTL, err = normalizeConfigDuration("deny cache", time.Second, config.DenyTTL, minDenyTTL, maxDenyTTL, defaultDenyTTL)
if err != nil {
return err
}
return nil
}
func normalizeConfigDuration(name string, scale, value, min, max, defaultValue time.Duration) (time.Duration, error) {
// disable with -1 sentinel
if value == disableTTL {
glog.V(2).Infof("image policy webhook %s disabled", name)
return time.Duration(0), nil
}
// use default with 0 sentinel
if value == useDefault {
glog.V(2).Infof("image policy webhook %s using default value", name)
return defaultValue, nil
}
// convert to s; unmarshalling gives ns
value *= scale
// check value is within range
if value < min || value > max {
return value, fmt.Errorf("valid value is between %v and %v, got %v", min, max, value)
}
return value, nil
}

View File

@ -0,0 +1,133 @@
/*
Copyright 2016 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 imagepolicy
import (
"reflect"
"testing"
"time"
)
func TestConfigNormalization(t *testing.T) {
tests := []struct {
test string
config imagePolicyWebhookConfig
normalizedConfig imagePolicyWebhookConfig
wantErr bool
}{
{
test: "config within normal ranges",
config: imagePolicyWebhookConfig{
AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second,
DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second,
RetryBackoff: ((minRetryBackoff + maxRetryBackoff) / 2) / time.Millisecond,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: ((minAllowTTL + maxAllowTTL) / 2) / time.Second * time.Second,
DenyTTL: ((minDenyTTL + maxDenyTTL) / 2) / time.Second * time.Second,
RetryBackoff: (minRetryBackoff + maxRetryBackoff) / 2,
},
wantErr: false,
},
{
test: "config below normal ranges, error",
config: imagePolicyWebhookConfig{
AllowTTL: minAllowTTL - time.Duration(1),
DenyTTL: minDenyTTL - time.Duration(1),
RetryBackoff: minRetryBackoff - time.Duration(1),
},
wantErr: true,
},
{
test: "config above normal ranges, error",
config: imagePolicyWebhookConfig{
AllowTTL: time.Duration(1) + maxAllowTTL,
DenyTTL: time.Duration(1) + maxDenyTTL,
RetryBackoff: time.Duration(1) + maxRetryBackoff,
},
wantErr: true,
},
{
test: "config wants default values",
config: imagePolicyWebhookConfig{
AllowTTL: useDefault,
DenyTTL: useDefault,
RetryBackoff: useDefault,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: defaultAllowTTL,
DenyTTL: defaultDenyTTL,
RetryBackoff: defaultRetryBackoff,
},
wantErr: false,
},
{
test: "config wants disabled values",
config: imagePolicyWebhookConfig{
AllowTTL: disableTTL,
DenyTTL: disableTTL,
RetryBackoff: disableTTL,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: time.Duration(0),
DenyTTL: time.Duration(0),
RetryBackoff: time.Duration(0),
},
wantErr: false,
},
{
test: "config within normal ranges for min values",
config: imagePolicyWebhookConfig{
AllowTTL: minAllowTTL / time.Second,
DenyTTL: minDenyTTL / time.Second,
RetryBackoff: minRetryBackoff,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: minAllowTTL,
DenyTTL: minDenyTTL,
RetryBackoff: minRetryBackoff * time.Millisecond,
},
wantErr: false,
},
{
test: "config within normal ranges for max values",
config: imagePolicyWebhookConfig{
AllowTTL: maxAllowTTL / time.Second,
DenyTTL: maxDenyTTL / time.Second,
RetryBackoff: maxRetryBackoff / time.Millisecond,
},
normalizedConfig: imagePolicyWebhookConfig{
AllowTTL: maxAllowTTL,
DenyTTL: maxDenyTTL,
RetryBackoff: maxRetryBackoff,
},
wantErr: false,
},
}
for _, tt := range tests {
err := normalizeWebhookConfig(&tt.config)
if err == nil && tt.wantErr == true {
t.Errorf("%s: expected error from normalization and didn't have one", tt.test)
}
if err != nil && tt.wantErr == false {
t.Errorf("%s: unexpected error from normalization: %v", tt.test, err)
}
if err == nil && !reflect.DeepEqual(tt.config, tt.normalizedConfig) {
t.Errorf("%s: expected config to be normalized. got: %v expected: %v", tt.test, tt.config, tt.normalizedConfig)
}
}
}

View File

@ -0,0 +1,18 @@
/*
Copyright 2016 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 imagepolicy checks a webhook for image admission
package imagepolicy // import "k8s.io/kubernetes/plugin/pkg/admission/imagepolicy"

View File

@ -0,0 +1,102 @@
#!/bin/bash
# Copyright 2016 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.
set -e
# gencerts.sh generates the certificates for the webhook authz plugin tests.
#
# It is not expected to be run often (there is no go generate rule), and mainly
# exists for documentation purposes.
cat > server.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
EOF
cat > client.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
# Create a certificate authority
openssl genrsa -out caKey.pem 2048
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=webhook_imagepolicy_ca"
# Create a second certificate authority
openssl genrsa -out badCAKey.pem 2048
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=webhook_imagepolicy_ca"
# Create a server certiticate
openssl genrsa -out serverKey.pem 2048
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook_imagepolicy_server" -config server.conf
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
# Create a client certiticate
openssl genrsa -out clientKey.pem 2048
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=webhook_imagepolicy_client" -config client.conf
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
outfile=certs_test.go
cat > $outfile << EOF
/*
Copyright 2016 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.
*/
EOF
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
echo "// and holds raw certificates for the imagepolicy webhook tests." >> $outfile
echo "" >> $outfile
echo "package imagepolicy" >> $outfile
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
data=$(cat ${file}.pem)
echo "" >> $outfile
echo "var $file = []byte(\`$data\`)" >> $outfile
done
# Clean up after we're done.
rm *.pem
rm *.csr
rm *.srl
rm *.conf

View File

@ -0,0 +1,71 @@
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",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/initialresources",
library = ":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

@ -0,0 +1,218 @@
/*
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
)
// Register registers a plugin
// WARNING: this feature is experimental and will definitely change.
func Register(plugins *admission.Plugins) {
plugins.Register("InitialResources", 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

@ -0,0 +1,300 @@
/*
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

@ -0,0 +1,56 @@
/*
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

@ -0,0 +1,45 @@
/*
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

@ -0,0 +1,132 @@
/*
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

@ -0,0 +1,46 @@
/*
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

@ -0,0 +1,223 @@
/*
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

@ -0,0 +1,142 @@
/*
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

@ -0,0 +1,73 @@
/*
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

@ -0,0 +1,40 @@
/*
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

@ -0,0 +1,65 @@
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",
"interfaces.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/limitranger",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta: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/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/limitranger",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/informers/informers_generated/internalversion: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/resource: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/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/testing: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

@ -0,0 +1,595 @@
/*
Copyright 2014 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 limitranger
import (
"fmt"
"io"
"sort"
"strings"
"time"
lru "github.com/hashicorp/golang-lru"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
corelisters "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
const (
limitRangerAnnotation = "kubernetes.io/limit-ranger"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("LimitRanger", func(config io.Reader) (admission.Interface, error) {
return NewLimitRanger(&DefaultLimitRangerActions{})
})
}
// LimitRanger enforces usage limits on a per resource basis in the namespace
type LimitRanger struct {
*admission.Handler
client internalclientset.Interface
actions LimitRangerActions
lister corelisters.LimitRangeLister
// liveLookups holds the last few live lookups we've done to help ammortize cost on repeated lookup failures.
// This let's us handle the case of latent caches, by looking up actual results for a namespace on cache miss/no results.
// We track the lookup result here so that for repeated requests, we don't look it up very often.
liveLookupCache *lru.Cache
liveTTL time.Duration
}
var _ admission.MutationInterface = &LimitRanger{}
var _ admission.ValidationInterface = &LimitRanger{}
var _ kubeapiserveradmission.WantsInternalKubeInformerFactory = &LimitRanger{}
type liveLookupEntry struct {
expiry time.Time
items []*api.LimitRange
}
func (l *LimitRanger) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
limitRangeInformer := f.Core().InternalVersion().LimitRanges()
l.SetReadyFunc(limitRangeInformer.Informer().HasSynced)
l.lister = limitRangeInformer.Lister()
}
func (l *LimitRanger) ValidateInitialization() error {
if l.lister == nil {
return fmt.Errorf("missing limitRange lister")
}
if l.client == nil {
return fmt.Errorf("missing client")
}
return nil
}
// Admit admits resources into cluster that do not violate any defined LimitRange in the namespace
func (l *LimitRanger) Admit(a admission.Attributes) (err error) {
return l.runLimitFunc(a, l.actions.MutateLimit)
}
// Validate admits resources into cluster that do not violate any defined LimitRange in the namespace
func (l *LimitRanger) Validate(a admission.Attributes) (err error) {
return l.runLimitFunc(a, l.actions.ValidateLimit)
}
func (l *LimitRanger) runLimitFunc(a admission.Attributes, limitFn func(limitRange *api.LimitRange, kind string, obj runtime.Object) error) (err error) {
if !l.actions.SupportsAttributes(a) {
return nil
}
obj := a.GetObject()
name := "Unknown"
if obj != nil {
name, _ = meta.NewAccessor().Name(obj)
if len(name) == 0 {
name, _ = meta.NewAccessor().GenerateName(obj)
}
}
items, err := l.GetLimitRanges(a)
if err != nil {
return err
}
// ensure it meets each prescribed min/max
for i := range items {
limitRange := items[i]
if !l.actions.SupportsLimit(limitRange) {
continue
}
err = limitFn(limitRange, a.GetResource().Resource, a.GetObject())
if err != nil {
return admission.NewForbidden(a, err)
}
}
return nil
}
func (l *LimitRanger) GetLimitRanges(a admission.Attributes) ([]*api.LimitRange, error) {
items, err := l.lister.LimitRanges(a.GetNamespace()).List(labels.Everything())
if err != nil {
return nil, admission.NewForbidden(a, fmt.Errorf("unable to %s %v at this time because there was an error enforcing limit ranges", a.GetOperation(), a.GetResource()))
}
// if there are no items held in our indexer, check our live-lookup LRU, if that misses, do the live lookup to prime it.
if len(items) == 0 {
lruItemObj, ok := l.liveLookupCache.Get(a.GetNamespace())
if !ok || lruItemObj.(liveLookupEntry).expiry.Before(time.Now()) {
// TODO: If there are multiple operations at the same time and cache has just expired,
// this may cause multiple List operations being issued at the same time.
// If there is already in-flight List() for a given namespace, we should wait until
// it is finished and cache is updated instead of doing the same, also to avoid
// throttling - see #22422 for details.
liveList, err := l.client.Core().LimitRanges(a.GetNamespace()).List(metav1.ListOptions{})
if err != nil {
return nil, admission.NewForbidden(a, err)
}
newEntry := liveLookupEntry{expiry: time.Now().Add(l.liveTTL)}
for i := range liveList.Items {
newEntry.items = append(newEntry.items, &liveList.Items[i])
}
l.liveLookupCache.Add(a.GetNamespace(), newEntry)
lruItemObj = newEntry
}
lruEntry := lruItemObj.(liveLookupEntry)
for i := range lruEntry.items {
items = append(items, lruEntry.items[i])
}
}
return items, nil
}
// NewLimitRanger returns an object that enforces limits based on the supplied limit function
func NewLimitRanger(actions LimitRangerActions) (*LimitRanger, error) {
liveLookupCache, err := lru.New(10000)
if err != nil {
return nil, err
}
if actions == nil {
actions = &DefaultLimitRangerActions{}
}
return &LimitRanger{
Handler: admission.NewHandler(admission.Create, admission.Update),
actions: actions,
liveLookupCache: liveLookupCache,
liveTTL: time.Duration(30 * time.Second),
}, nil
}
var _ = kubeapiserveradmission.WantsInternalKubeInformerFactory(&LimitRanger{})
var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&LimitRanger{})
func (a *LimitRanger) SetInternalKubeClientSet(client internalclientset.Interface) {
a.client = client
}
// defaultContainerResourceRequirements returns the default requirements for a container
// the requirement.Limits are taken from the LimitRange defaults (if specified)
// the requirement.Requests are taken from the LimitRange default request (if specified)
func defaultContainerResourceRequirements(limitRange *api.LimitRange) api.ResourceRequirements {
requirements := api.ResourceRequirements{}
requirements.Requests = api.ResourceList{}
requirements.Limits = api.ResourceList{}
for i := range limitRange.Spec.Limits {
limit := limitRange.Spec.Limits[i]
if limit.Type == api.LimitTypeContainer {
for k, v := range limit.DefaultRequest {
value := v.Copy()
requirements.Requests[k] = *value
}
for k, v := range limit.Default {
value := v.Copy()
requirements.Limits[k] = *value
}
}
}
return requirements
}
// mergeContainerResources handles defaulting all of the resources on a container.
func mergeContainerResources(container *api.Container, defaultRequirements *api.ResourceRequirements, annotationPrefix string, annotations []string) []string {
setRequests := []string{}
setLimits := []string{}
if container.Resources.Limits == nil {
container.Resources.Limits = api.ResourceList{}
}
if container.Resources.Requests == nil {
container.Resources.Requests = api.ResourceList{}
}
for k, v := range defaultRequirements.Limits {
_, found := container.Resources.Limits[k]
if !found {
container.Resources.Limits[k] = *v.Copy()
setLimits = append(setLimits, string(k))
}
}
for k, v := range defaultRequirements.Requests {
_, found := container.Resources.Requests[k]
if !found {
container.Resources.Requests[k] = *v.Copy()
setRequests = append(setRequests, string(k))
}
}
if len(setRequests) > 0 {
sort.Strings(setRequests)
a := strings.Join(setRequests, ", ") + fmt.Sprintf(" request for %s %s", annotationPrefix, container.Name)
annotations = append(annotations, a)
}
if len(setLimits) > 0 {
sort.Strings(setLimits)
a := strings.Join(setLimits, ", ") + fmt.Sprintf(" limit for %s %s", annotationPrefix, container.Name)
annotations = append(annotations, a)
}
return annotations
}
// mergePodResourceRequirements merges enumerated requirements with default requirements
// it annotates the pod with information about what requirements were modified
func mergePodResourceRequirements(pod *api.Pod, defaultRequirements *api.ResourceRequirements) {
annotations := []string{}
for i := range pod.Spec.Containers {
annotations = mergeContainerResources(&pod.Spec.Containers[i], defaultRequirements, "container", annotations)
}
for i := range pod.Spec.InitContainers {
annotations = mergeContainerResources(&pod.Spec.InitContainers[i], defaultRequirements, "init container", annotations)
}
if len(annotations) > 0 {
if pod.ObjectMeta.Annotations == nil {
pod.ObjectMeta.Annotations = make(map[string]string)
}
val := "LimitRanger plugin set: " + strings.Join(annotations, "; ")
pod.ObjectMeta.Annotations[limitRangerAnnotation] = val
}
}
// requestLimitEnforcedValues returns the specified values at a common precision to support comparability
func requestLimitEnforcedValues(requestQuantity, limitQuantity, enforcedQuantity resource.Quantity) (request, limit, enforced int64) {
request = requestQuantity.Value()
limit = limitQuantity.Value()
enforced = enforcedQuantity.Value()
// do a more precise comparison if possible (if the value won't overflow)
if request <= resource.MaxMilliValue && limit <= resource.MaxMilliValue && enforced <= resource.MaxMilliValue {
request = requestQuantity.MilliValue()
limit = limitQuantity.MilliValue()
enforced = enforcedQuantity.MilliValue()
}
return
}
// minConstraint enforces the min constraint over the specified resource
func minConstraint(limitType api.LimitType, resourceName api.ResourceName, enforced resource.Quantity, request api.ResourceList, limit api.ResourceList) error {
req, reqExists := request[resourceName]
lim, limExists := limit[resourceName]
observedReqValue, observedLimValue, enforcedValue := requestLimitEnforcedValues(req, lim, enforced)
if !reqExists {
return fmt.Errorf("minimum %s usage per %s is %s. No request is specified.", resourceName, limitType, enforced.String())
}
if observedReqValue < enforcedValue {
return fmt.Errorf("minimum %s usage per %s is %s, but request is %s.", resourceName, limitType, enforced.String(), req.String())
}
if limExists && (observedLimValue < enforcedValue) {
return fmt.Errorf("minimum %s usage per %s is %s, but limit is %s.", resourceName, limitType, enforced.String(), lim.String())
}
return nil
}
// maxRequestConstraint enforces the max constraint over the specified resource
// use when specify LimitType resource doesn't recognize limit values
func maxRequestConstraint(limitType api.LimitType, resourceName api.ResourceName, enforced resource.Quantity, request api.ResourceList) error {
req, reqExists := request[resourceName]
observedReqValue, _, enforcedValue := requestLimitEnforcedValues(req, resource.Quantity{}, enforced)
if !reqExists {
return fmt.Errorf("maximum %s usage per %s is %s. No request is specified.", resourceName, limitType, enforced.String())
}
if observedReqValue > enforcedValue {
return fmt.Errorf("maximum %s usage per %s is %s, but request is %s.", resourceName, limitType, enforced.String(), req.String())
}
return nil
}
// maxConstraint enforces the max constraint over the specified resource
func maxConstraint(limitType api.LimitType, resourceName api.ResourceName, enforced resource.Quantity, request api.ResourceList, limit api.ResourceList) error {
req, reqExists := request[resourceName]
lim, limExists := limit[resourceName]
observedReqValue, observedLimValue, enforcedValue := requestLimitEnforcedValues(req, lim, enforced)
if !limExists {
return fmt.Errorf("maximum %s usage per %s is %s. No limit is specified.", resourceName, limitType, enforced.String())
}
if observedLimValue > enforcedValue {
return fmt.Errorf("maximum %s usage per %s is %s, but limit is %s.", resourceName, limitType, enforced.String(), lim.String())
}
if reqExists && (observedReqValue > enforcedValue) {
return fmt.Errorf("maximum %s usage per %s is %s, but request is %s.", resourceName, limitType, enforced.String(), req.String())
}
return nil
}
// limitRequestRatioConstraint enforces the limit to request ratio over the specified resource
func limitRequestRatioConstraint(limitType api.LimitType, resourceName api.ResourceName, enforced resource.Quantity, request api.ResourceList, limit api.ResourceList) error {
req, reqExists := request[resourceName]
lim, limExists := limit[resourceName]
observedReqValue, observedLimValue, _ := requestLimitEnforcedValues(req, lim, enforced)
if !reqExists || (observedReqValue == int64(0)) {
return fmt.Errorf("%s max limit to request ratio per %s is %s, but no request is specified or request is 0.", resourceName, limitType, enforced.String())
}
if !limExists || (observedLimValue == int64(0)) {
return fmt.Errorf("%s max limit to request ratio per %s is %s, but no limit is specified or limit is 0.", resourceName, limitType, enforced.String())
}
observedRatio := float64(observedLimValue) / float64(observedReqValue)
displayObservedRatio := observedRatio
maxLimitRequestRatio := float64(enforced.Value())
if enforced.Value() <= resource.MaxMilliValue {
observedRatio = observedRatio * 1000
maxLimitRequestRatio = float64(enforced.MilliValue())
}
if observedRatio > maxLimitRequestRatio {
return fmt.Errorf("%s max limit to request ratio per %s is %s, but provided ratio is %f.", resourceName, limitType, enforced.String(), displayObservedRatio)
}
return nil
}
// sum takes the total of each named resource across all inputs
// if a key is not in each input, then the output resource list will omit the key
func sum(inputs []api.ResourceList) api.ResourceList {
result := api.ResourceList{}
keys := []api.ResourceName{}
for i := range inputs {
for k := range inputs[i] {
keys = append(keys, k)
}
}
for _, key := range keys {
total, isSet := int64(0), true
for i := range inputs {
input := inputs[i]
v, exists := input[key]
if exists {
if key == api.ResourceCPU {
total = total + v.MilliValue()
} else {
total = total + v.Value()
}
} else {
isSet = false
}
}
if isSet {
if key == api.ResourceCPU {
result[key] = *(resource.NewMilliQuantity(total, resource.DecimalSI))
} else {
result[key] = *(resource.NewQuantity(total, resource.DecimalSI))
}
}
}
return result
}
// DefaultLimitRangerActions is the default implementation of LimitRangerActions.
type DefaultLimitRangerActions struct{}
// ensure DefaultLimitRangerActions implements the LimitRangerActions interface.
var _ LimitRangerActions = &DefaultLimitRangerActions{}
// Limit enforces resource requirements of incoming resources against enumerated constraints
// on the LimitRange. It may modify the incoming object to apply default resource requirements
// if not specified, and enumerated on the LimitRange
func (d *DefaultLimitRangerActions) MutateLimit(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error {
switch resourceName {
case "pods":
return PodMutateLimitFunc(limitRange, obj.(*api.Pod))
}
return nil
}
// Limit enforces resource requirements of incoming resources against enumerated constraints
// on the LimitRange. It may modify the incoming object to apply default resource requirements
// if not specified, and enumerated on the LimitRange
func (d *DefaultLimitRangerActions) ValidateLimit(limitRange *api.LimitRange, resourceName string, obj runtime.Object) error {
switch resourceName {
case "pods":
return PodValidateLimitFunc(limitRange, obj.(*api.Pod))
case "persistentvolumeclaims":
return PersistentVolumeClaimValidateLimitFunc(limitRange, obj.(*api.PersistentVolumeClaim))
}
return nil
}
// SupportsAttributes ignores all calls that do not deal with pod resources or storage requests (PVCs).
// Also ignores any call that has a subresource defined.
func (d *DefaultLimitRangerActions) SupportsAttributes(a admission.Attributes) bool {
if a.GetSubresource() != "" {
return false
}
return a.GetKind().GroupKind() == api.Kind("Pod") || a.GetKind().GroupKind() == api.Kind("PersistentVolumeClaim")
}
// SupportsLimit always returns true.
func (d *DefaultLimitRangerActions) SupportsLimit(limitRange *api.LimitRange) bool {
return true
}
// PersistentVolumeClaimValidateLimitFunc enforces storage limits for PVCs.
// Users request storage via pvc.Spec.Resources.Requests. Min/Max is enforced by an admin with LimitRange.
// Claims will not be modified with default values because storage is a required part of pvc.Spec.
// All storage enforced values *only* apply to pvc.Spec.Resources.Requests.
func PersistentVolumeClaimValidateLimitFunc(limitRange *api.LimitRange, pvc *api.PersistentVolumeClaim) error {
var errs []error
for i := range limitRange.Spec.Limits {
limit := limitRange.Spec.Limits[i]
limitType := limit.Type
if limitType == api.LimitTypePersistentVolumeClaim {
for k, v := range limit.Min {
// normal usage of minConstraint. pvc.Spec.Resources.Limits is not recognized as user input
if err := minConstraint(limitType, k, v, pvc.Spec.Resources.Requests, api.ResourceList{}); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.Max {
// We want to enforce the max of the LimitRange against what
// the user requested.
if err := maxRequestConstraint(limitType, k, v, pvc.Spec.Resources.Requests); err != nil {
errs = append(errs, err)
}
}
}
}
return utilerrors.NewAggregate(errs)
}
// PodMutateLimitFunc sets resource requirements enumerated by the pod against
// the specified LimitRange. The pod may be modified to apply default resource
// requirements if not specified, and enumerated on the LimitRange
func PodMutateLimitFunc(limitRange *api.LimitRange, pod *api.Pod) error {
defaultResources := defaultContainerResourceRequirements(limitRange)
mergePodResourceRequirements(pod, &defaultResources)
return nil
}
// PodValidateLimitFunc enforces resource requirements enumerated by the pod against
// the specified LimitRange.
func PodValidateLimitFunc(limitRange *api.LimitRange, pod *api.Pod) error {
var errs []error
for i := range limitRange.Spec.Limits {
limit := limitRange.Spec.Limits[i]
limitType := limit.Type
// enforce container limits
if limitType == api.LimitTypeContainer {
for j := range pod.Spec.Containers {
container := &pod.Spec.Containers[j]
for k, v := range limit.Min {
if err := minConstraint(limitType, k, v, container.Resources.Requests, container.Resources.Limits); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.Max {
if err := maxConstraint(limitType, k, v, container.Resources.Requests, container.Resources.Limits); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.MaxLimitRequestRatio {
if err := limitRequestRatioConstraint(limitType, k, v, container.Resources.Requests, container.Resources.Limits); err != nil {
errs = append(errs, err)
}
}
}
for j := range pod.Spec.InitContainers {
container := &pod.Spec.InitContainers[j]
for k, v := range limit.Min {
if err := minConstraint(limitType, k, v, container.Resources.Requests, container.Resources.Limits); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.Max {
if err := maxConstraint(limitType, k, v, container.Resources.Requests, container.Resources.Limits); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.MaxLimitRequestRatio {
if err := limitRequestRatioConstraint(limitType, k, v, container.Resources.Requests, container.Resources.Limits); err != nil {
errs = append(errs, err)
}
}
}
}
// enforce pod limits on init containers
if limitType == api.LimitTypePod {
containerRequests, containerLimits := []api.ResourceList{}, []api.ResourceList{}
for j := range pod.Spec.Containers {
container := &pod.Spec.Containers[j]
containerRequests = append(containerRequests, container.Resources.Requests)
containerLimits = append(containerLimits, container.Resources.Limits)
}
podRequests := sum(containerRequests)
podLimits := sum(containerLimits)
for j := range pod.Spec.InitContainers {
container := &pod.Spec.InitContainers[j]
// take max(sum_containers, any_init_container)
for k, v := range container.Resources.Requests {
if v2, ok := podRequests[k]; ok {
if v.Cmp(v2) > 0 {
podRequests[k] = v
}
} else {
podRequests[k] = v
}
}
for k, v := range container.Resources.Limits {
if v2, ok := podLimits[k]; ok {
if v.Cmp(v2) > 0 {
podLimits[k] = v
}
} else {
podLimits[k] = v
}
}
}
for k, v := range limit.Min {
if err := minConstraint(limitType, k, v, podRequests, podLimits); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.Max {
if err := maxConstraint(limitType, k, v, podRequests, podLimits); err != nil {
errs = append(errs, err)
}
}
for k, v := range limit.MaxLimitRequestRatio {
if err := limitRequestRatioConstraint(limitType, k, v, podRequests, podLimits); err != nil {
errs = append(errs, err)
}
}
}
}
return utilerrors.NewAggregate(errs)
}

View File

@ -0,0 +1,828 @@
/*
Copyright 2014 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 limitranger
import (
"fmt"
"strconv"
"testing"
"time"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
core "k8s.io/client-go/testing"
api "k8s.io/kubernetes/pkg/apis/core"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
func getComputeResourceList(cpu, memory string) api.ResourceList {
res := api.ResourceList{}
if cpu != "" {
res[api.ResourceCPU] = resource.MustParse(cpu)
}
if memory != "" {
res[api.ResourceMemory] = resource.MustParse(memory)
}
return res
}
func getStorageResourceList(storage string) api.ResourceList {
res := api.ResourceList{}
if storage != "" {
res[api.ResourceStorage] = resource.MustParse(storage)
}
return res
}
func getResourceRequirements(requests, limits api.ResourceList) api.ResourceRequirements {
res := api.ResourceRequirements{}
res.Requests = requests
res.Limits = limits
return res
}
// createLimitRange creates a limit range with the specified data
func createLimitRange(limitType api.LimitType, min, max, defaultLimit, defaultRequest, maxLimitRequestRatio api.ResourceList) api.LimitRange {
return api.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: "abc",
Namespace: "test",
},
Spec: api.LimitRangeSpec{
Limits: []api.LimitRangeItem{
{
Type: limitType,
Min: min,
Max: max,
Default: defaultLimit,
DefaultRequest: defaultRequest,
MaxLimitRequestRatio: maxLimitRequestRatio,
},
},
},
}
}
func validLimitRange() api.LimitRange {
return api.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: "abc",
Namespace: "test",
},
Spec: api.LimitRangeSpec{
Limits: []api.LimitRangeItem{
{
Type: api.LimitTypePod,
Max: getComputeResourceList("200m", "4Gi"),
Min: getComputeResourceList("50m", "2Mi"),
},
{
Type: api.LimitTypeContainer,
Max: getComputeResourceList("100m", "2Gi"),
Min: getComputeResourceList("25m", "1Mi"),
Default: getComputeResourceList("75m", "10Mi"),
DefaultRequest: getComputeResourceList("50m", "5Mi"),
},
},
},
}
}
func validLimitRangeNoDefaults() api.LimitRange {
return api.LimitRange{
ObjectMeta: metav1.ObjectMeta{
Name: "abc",
Namespace: "test",
},
Spec: api.LimitRangeSpec{
Limits: []api.LimitRangeItem{
{
Type: api.LimitTypePod,
Max: getComputeResourceList("200m", "4Gi"),
Min: getComputeResourceList("50m", "2Mi"),
},
{
Type: api.LimitTypeContainer,
Max: getComputeResourceList("100m", "2Gi"),
Min: getComputeResourceList("25m", "1Mi"),
},
},
},
}
}
func validPod(name string, numContainers int, resources api.ResourceRequirements) api.Pod {
pod := api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
Spec: api.PodSpec{},
}
pod.Spec.Containers = make([]api.Container, 0, numContainers)
for i := 0; i < numContainers; i++ {
pod.Spec.Containers = append(pod.Spec.Containers, api.Container{
Image: "foo:V" + strconv.Itoa(i),
Resources: resources,
Name: "foo-" + strconv.Itoa(i),
})
}
return pod
}
func validPodInit(pod api.Pod, resources ...api.ResourceRequirements) api.Pod {
for i := 0; i < len(resources); i++ {
pod.Spec.InitContainers = append(pod.Spec.InitContainers, api.Container{
Image: "foo:V" + strconv.Itoa(i),
Resources: resources[i],
Name: "foo-" + strconv.Itoa(i),
})
}
return pod
}
func TestDefaultContainerResourceRequirements(t *testing.T) {
limitRange := validLimitRange()
expected := api.ResourceRequirements{
Requests: getComputeResourceList("50m", "5Mi"),
Limits: getComputeResourceList("75m", "10Mi"),
}
actual := defaultContainerResourceRequirements(&limitRange)
if !apiequality.Semantic.DeepEqual(expected, actual) {
t.Errorf("actual.Limits != expected.Limits; %v != %v", actual.Limits, expected.Limits)
t.Errorf("actual.Requests != expected.Requests; %v != %v", actual.Requests, expected.Requests)
t.Errorf("expected != actual; %v != %v", expected, actual)
}
}
func verifyAnnotation(t *testing.T, pod *api.Pod, expected string) {
a, ok := pod.ObjectMeta.Annotations[limitRangerAnnotation]
if !ok {
t.Errorf("No annotation but expected %v", expected)
}
if a != expected {
t.Errorf("Wrong annotation set by Limit Ranger: got %v, expected %v", a, expected)
}
}
func expectNoAnnotation(t *testing.T, pod *api.Pod) {
if a, ok := pod.ObjectMeta.Annotations[limitRangerAnnotation]; ok {
t.Errorf("Expected no annotation but got %v", a)
}
}
func TestMergePodResourceRequirements(t *testing.T) {
limitRange := validLimitRange()
// pod with no resources enumerated should get each resource from default request
expected := getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", ""))
pod := validPod("empty-resources", 1, expected)
defaultRequirements := defaultContainerResourceRequirements(&limitRange)
mergePodResourceRequirements(&pod, &defaultRequirements)
for i := range pod.Spec.Containers {
actual := pod.Spec.Containers[i].Resources
if !apiequality.Semantic.DeepEqual(expected, actual) {
t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, expected, actual)
}
}
verifyAnnotation(t, &pod, "LimitRanger plugin set: cpu, memory request for container foo-0; cpu, memory limit for container foo-0")
// pod with some resources enumerated should only merge empty
input := getResourceRequirements(getComputeResourceList("", "512Mi"), getComputeResourceList("", ""))
pod = validPodInit(validPod("limit-memory", 1, input), input)
expected = api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceCPU: defaultRequirements.Requests[api.ResourceCPU],
api.ResourceMemory: resource.MustParse("512Mi"),
},
Limits: defaultRequirements.Limits,
}
mergePodResourceRequirements(&pod, &defaultRequirements)
for i := range pod.Spec.Containers {
actual := pod.Spec.Containers[i].Resources
if !apiequality.Semantic.DeepEqual(expected, actual) {
t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, expected, actual)
}
}
for i := range pod.Spec.InitContainers {
actual := pod.Spec.InitContainers[i].Resources
if !apiequality.Semantic.DeepEqual(expected, actual) {
t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, expected, actual)
}
}
verifyAnnotation(t, &pod, "LimitRanger plugin set: cpu request for container foo-0; cpu, memory limit for container foo-0")
// pod with all resources enumerated should not merge anything
input = getResourceRequirements(getComputeResourceList("100m", "512Mi"), getComputeResourceList("200m", "1G"))
initInputs := []api.ResourceRequirements{getResourceRequirements(getComputeResourceList("200m", "1G"), getComputeResourceList("400m", "2G"))}
pod = validPodInit(validPod("limit-memory", 1, input), initInputs...)
expected = input
mergePodResourceRequirements(&pod, &defaultRequirements)
for i := range pod.Spec.Containers {
actual := pod.Spec.Containers[i].Resources
if !apiequality.Semantic.DeepEqual(expected, actual) {
t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, expected, actual)
}
}
for i := range pod.Spec.InitContainers {
actual := pod.Spec.InitContainers[i].Resources
if !apiequality.Semantic.DeepEqual(initInputs[i], actual) {
t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, initInputs[i], actual)
}
}
expectNoAnnotation(t, &pod)
}
func TestPodLimitFunc(t *testing.T) {
type testCase struct {
pod api.Pod
limitRange api.LimitRange
}
successCases := []testCase{
{
pod: validPod("ctr-min-cpu-request", 1, getResourceRequirements(getComputeResourceList("100m", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-cpu-request-limit", 1, getResourceRequirements(getComputeResourceList("100m", ""), getComputeResourceList("200m", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-memory-request", 1, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-memory-request-limit", 1, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", "100Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-request-limit", 1, getResourceRequirements(getComputeResourceList("500m", ""), getComputeResourceList("1", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("1", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-mem-request-limit", 1, getResourceRequirements(getComputeResourceList("", "250Mi"), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-ratio", 1, getResourceRequirements(getComputeResourceList("500m", ""), getComputeResourceList("750m", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, getComputeResourceList("1.5", "")),
},
{
pod: validPod("ctr-max-mem-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-cpu-request", 2, getResourceRequirements(getComputeResourceList("75m", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-cpu-request-limit", 2, getResourceRequirements(getComputeResourceList("75m", ""), getComputeResourceList("200m", ""))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-memory-request", 2, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-memory-request-limit", 2, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", "100Mi"))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-min-memory-request", 2, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", ""))),
getResourceRequirements(getComputeResourceList("", "100Mi"), getComputeResourceList("", "")),
),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-min-memory-request-limit", 2, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", "100Mi"))),
getResourceRequirements(getComputeResourceList("", "80Mi"), getComputeResourceList("", "100Mi")),
),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-cpu-request-limit", 2, getResourceRequirements(getComputeResourceList("500m", ""), getComputeResourceList("1", ""))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-cpu-limit", 2, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("1", ""))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-max-cpu-request-limit", 2, getResourceRequirements(getComputeResourceList("500m", ""), getComputeResourceList("1", ""))),
getResourceRequirements(getComputeResourceList("1", ""), getComputeResourceList("2", "")),
getResourceRequirements(getComputeResourceList("1", ""), getComputeResourceList("1", "")),
),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-max-cpu-limit", 2, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("1", ""))),
getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("2", "")),
getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("2", "")),
),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-mem-request-limit", 2, getResourceRequirements(getComputeResourceList("", "250Mi"), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-mem-limit", 2, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-mem-ratio", 3, getResourceRequirements(getComputeResourceList("", "300Mi"), getComputeResourceList("", "450Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "2Gi"), api.ResourceList{}, api.ResourceList{}, getComputeResourceList("", "1.5")),
},
{
pod: validPod("ctr-1-min-local-ephemeral-storage-request", 1, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-min-local-ephemeral-storage-request-limit", 1, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList("100Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-max-local-ephemeral-storage-request-limit", 1, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-max-local-ephemeral-storage-limit", 1, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-min-local-ephemeral-storage-request", 2, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-min-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList("100Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-max-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("600Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-max-local-ephemeral-storage-limit", 2, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("600Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-local-ephemeral-storage-request", 2, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePod, getLocalStorageResourceList("100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList("100Mi"))),
limitRange: createLimitRange(api.LimitTypePod, getLocalStorageResourceList("100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-min-local-ephemeral-storage-request", 2, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList(""))),
getResourceRequirements(getLocalStorageResourceList("100Mi"), getLocalStorageResourceList("")),
),
limitRange: createLimitRange(api.LimitTypePod, getLocalStorageResourceList("100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-min-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList("100Mi"))),
getResourceRequirements(getLocalStorageResourceList("80Mi"), getLocalStorageResourceList("100Mi")),
),
limitRange: createLimitRange(api.LimitTypePod, getLocalStorageResourceList("100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-local-ephemeral-storage-limit", 2, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-local-ephemeral-storage-ratio", 3, getResourceRequirements(getLocalStorageResourceList("300Mi"), getLocalStorageResourceList("450Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("2Gi"), api.ResourceList{}, api.ResourceList{}, getLocalStorageResourceList("1.5")),
},
}
for i := range successCases {
test := successCases[i]
err := PodMutateLimitFunc(&test.limitRange, &test.pod)
if err != nil {
t.Errorf("Unexpected error for pod: %s, %v", test.pod.Name, err)
}
err = PodValidateLimitFunc(&test.limitRange, &test.pod)
if err != nil {
t.Errorf("Unexpected error for pod: %s, %v", test.pod.Name, err)
}
}
errorCases := []testCase{
{
pod: validPod("ctr-min-cpu-request", 1, getResourceRequirements(getComputeResourceList("40m", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-cpu-request-limit", 1, getResourceRequirements(getComputeResourceList("40m", ""), getComputeResourceList("200m", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-cpu-no-request-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-memory-request", 1, getResourceRequirements(getComputeResourceList("", "40Mi"), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-memory-request-limit", 1, getResourceRequirements(getComputeResourceList("", "40Mi"), getComputeResourceList("", "100Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-min-memory-no-request-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, getComputeResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-request-limit", 1, getResourceRequirements(getComputeResourceList("500m", ""), getComputeResourceList("2500m", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("2500m", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-no-request-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-cpu-ratio", 1, getResourceRequirements(getComputeResourceList("1250m", ""), getComputeResourceList("2500m", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, getComputeResourceList("1", "")),
},
{
pod: validPod("ctr-max-mem-request-limit", 1, getResourceRequirements(getComputeResourceList("", "250Mi"), getComputeResourceList("", "2Gi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-mem-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", "2Gi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-max-mem-no-request-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-cpu-request", 1, getResourceRequirements(getComputeResourceList("75m", ""), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-cpu-request-limit", 1, getResourceRequirements(getComputeResourceList("75m", ""), getComputeResourceList("200m", ""))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-memory-request", 1, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", ""))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-memory-request-limit", 1, getResourceRequirements(getComputeResourceList("", "60Mi"), getComputeResourceList("", "100Mi"))),
limitRange: createLimitRange(api.LimitTypePod, getComputeResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-cpu-request-limit", 3, getResourceRequirements(getComputeResourceList("500m", ""), getComputeResourceList("1", ""))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-cpu-limit", 3, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("1", ""))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-mem-request-limit", 3, getResourceRequirements(getComputeResourceList("", "250Mi"), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-mem-limit", 3, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-max-mem-limit", 1, getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", "500Mi"))),
getResourceRequirements(getComputeResourceList("", ""), getComputeResourceList("", "1.5Gi")),
),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-mem-ratio", 3, getResourceRequirements(getComputeResourceList("", "250Mi"), getComputeResourceList("", "500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getComputeResourceList("", "2Gi"), api.ResourceList{}, api.ResourceList{}, getComputeResourceList("", "1.5")),
},
{
pod: validPod("ctr-1-min-local-ephemeral-storage-request", 1, getResourceRequirements(getLocalStorageResourceList("40Mi"), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-min-local-ephemeral-storage-request-limit", 1, getResourceRequirements(getLocalStorageResourceList("40Mi"), getLocalStorageResourceList("100Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-min-local-ephemeral-storage-no-request-limit", 1, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-max-local-ephemeral-storage-request-limit", 1, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("2Gi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-max-local-ephemeral-storage-limit", 1, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("2Gi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-1-max-local-ephemeral-storage-no-request-limit", 1, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-min-local-ephemeral-storage-request", 2, getResourceRequirements(getLocalStorageResourceList("40Mi"), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-min-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("40Mi"), getLocalStorageResourceList("100Mi"))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-min-local-ephemeral-storage-no-request-limit", 2, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, getLocalStorageResourceList("50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-max-local-ephemeral-storage-request-limit", 2, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("2Gi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-max-local-ephemeral-storage-limit", 2, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("2Gi"))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("ctr-2-max-local-ephemeral-storage-no-request-limit", 2, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-local-ephemeral-storage-request", 1, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePod, getLocalStorageResourceList("100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-min-local-ephemeral-storage-request-limit", 1, getResourceRequirements(getLocalStorageResourceList("60Mi"), getLocalStorageResourceList("100Mi"))),
limitRange: createLimitRange(api.LimitTypePod, getLocalStorageResourceList("100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-local-ephemeral-storage-request-limit", 3, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-local-ephemeral-storage-limit", 3, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPodInit(
validPod("pod-init-max-local-ephemeral-storage-limit", 1, getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("500Mi"))),
getResourceRequirements(getLocalStorageResourceList(""), getLocalStorageResourceList("1.5Gi")),
),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pod: validPod("pod-max-local-ephemeral-storage-ratio", 3, getResourceRequirements(getLocalStorageResourceList("250Mi"), getLocalStorageResourceList("500Mi"))),
limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getLocalStorageResourceList("2Gi"), api.ResourceList{}, api.ResourceList{}, getLocalStorageResourceList("1.5")),
},
}
for i := range errorCases {
test := errorCases[i]
err := PodMutateLimitFunc(&test.limitRange, &test.pod)
if err != nil {
t.Errorf("Unexpected error for pod: %s, %v", test.pod.Name, err)
}
err = PodValidateLimitFunc(&test.limitRange, &test.pod)
if err == nil {
t.Errorf("Expected error for pod: %s", test.pod.Name)
}
}
}
func getLocalStorageResourceList(ephemeralStorage string) api.ResourceList {
res := api.ResourceList{}
if ephemeralStorage != "" {
res[api.ResourceEphemeralStorage] = resource.MustParse(ephemeralStorage)
}
return res
}
func TestPodLimitFuncApplyDefault(t *testing.T) {
limitRange := validLimitRange()
testPod := validPodInit(validPod("foo", 1, getResourceRequirements(api.ResourceList{}, api.ResourceList{})), getResourceRequirements(api.ResourceList{}, api.ResourceList{}))
err := PodMutateLimitFunc(&limitRange, &testPod)
if err != nil {
t.Errorf("Unexpected error for valid pod: %s, %v", testPod.Name, err)
}
for i := range testPod.Spec.Containers {
container := testPod.Spec.Containers[i]
limitMemory := container.Resources.Limits.Memory().String()
limitCpu := container.Resources.Limits.Cpu().String()
requestMemory := container.Resources.Requests.Memory().String()
requestCpu := container.Resources.Requests.Cpu().String()
if limitMemory != "10Mi" {
t.Errorf("Unexpected limit memory value %s", limitMemory)
}
if limitCpu != "75m" {
t.Errorf("Unexpected limit cpu value %s", limitCpu)
}
if requestMemory != "5Mi" {
t.Errorf("Unexpected request memory value %s", requestMemory)
}
if requestCpu != "50m" {
t.Errorf("Unexpected request cpu value %s", requestCpu)
}
}
for i := range testPod.Spec.InitContainers {
container := testPod.Spec.InitContainers[i]
limitMemory := container.Resources.Limits.Memory().String()
limitCpu := container.Resources.Limits.Cpu().String()
requestMemory := container.Resources.Requests.Memory().String()
requestCpu := container.Resources.Requests.Cpu().String()
if limitMemory != "10Mi" {
t.Errorf("Unexpected limit memory value %s", limitMemory)
}
if limitCpu != "75m" {
t.Errorf("Unexpected limit cpu value %s", limitCpu)
}
if requestMemory != "5Mi" {
t.Errorf("Unexpected request memory value %s", requestMemory)
}
if requestCpu != "50m" {
t.Errorf("Unexpected request cpu value %s", requestCpu)
}
}
}
func TestLimitRangerIgnoresSubresource(t *testing.T) {
limitRange := validLimitRangeNoDefaults()
mockClient := newMockClientForTest([]api.LimitRange{limitRange})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
testPod := validPod("testPod", 1, api.ResourceRequirements{})
err = handler.Admit(admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if err != nil {
t.Fatal(err)
}
err = handler.Validate(admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if err == nil {
t.Errorf("Expected an error since the pod did not specify resource limits in its update call")
}
err = handler.Validate(admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "status", admission.Update, nil))
if err != nil {
t.Errorf("Should have ignored calls to any subresource of pod %v", err)
}
}
func TestLimitRangerAdmitPod(t *testing.T) {
limitRange := validLimitRangeNoDefaults()
mockClient := newMockClientForTest([]api.LimitRange{limitRange})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
testPod := validPod("testPod", 1, api.ResourceRequirements{})
err = handler.Admit(admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if err != nil {
t.Fatal(err)
}
err = handler.Validate(admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if err == nil {
t.Errorf("Expected an error since the pod did not specify resource limits in its update call")
}
err = handler.Validate(admission.NewAttributesRecord(&testPod, nil, api.Kind("Pod").WithVersion("version"), limitRange.Namespace, "testPod", api.Resource("pods").WithVersion("version"), "status", admission.Update, nil))
if err != nil {
t.Errorf("Should have ignored calls to any subresource of pod %v", err)
}
}
// newMockClientForTest creates a mock client that returns a client configured for the specified list of limit ranges
func newMockClientForTest(limitRanges []api.LimitRange) *fake.Clientset {
mockClient := &fake.Clientset{}
mockClient.AddReactor("list", "limitranges", func(action core.Action) (bool, runtime.Object, error) {
limitRangeList := &api.LimitRangeList{
ListMeta: metav1.ListMeta{
ResourceVersion: fmt.Sprintf("%d", len(limitRanges)),
},
}
for index, value := range limitRanges {
value.ResourceVersion = fmt.Sprintf("%d", index)
limitRangeList.Items = append(limitRangeList.Items, value)
}
return true, limitRangeList, nil
})
return mockClient
}
// newHandlerForTest returns a handler configured for testing.
func newHandlerForTest(c clientset.Interface) (*LimitRanger, informers.SharedInformerFactory, error) {
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
handler, err := NewLimitRanger(&DefaultLimitRangerActions{})
if err != nil {
return nil, f, err
}
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer.Initialize(handler)
err = admission.ValidateInitialization(handler)
return handler, f, err
}
func validPersistentVolumeClaim(name string, resources api.ResourceRequirements) api.PersistentVolumeClaim {
pvc := api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
Spec: api.PersistentVolumeClaimSpec{
Resources: resources,
},
}
return pvc
}
func TestPersistentVolumeClaimLimitFunc(t *testing.T) {
type testCase struct {
pvc api.PersistentVolumeClaim
limitRange api.LimitRange
}
successCases := []testCase{
{
pvc: validPersistentVolumeClaim("pvc-is-min-storage-request", getResourceRequirements(getStorageResourceList("1Gi"), getStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePersistentVolumeClaim, getStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pvc: validPersistentVolumeClaim("pvc-is-max-storage-request", getResourceRequirements(getStorageResourceList("1Gi"), getStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePersistentVolumeClaim, api.ResourceList{}, getStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pvc: validPersistentVolumeClaim("pvc-no-minmax-storage-request", getResourceRequirements(getStorageResourceList("100Gi"), getStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePersistentVolumeClaim, getStorageResourceList(""), getStorageResourceList(""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pvc: validPersistentVolumeClaim("pvc-within-minmax-storage-request", getResourceRequirements(getStorageResourceList("5Gi"), getStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePersistentVolumeClaim, getStorageResourceList("1Gi"), getStorageResourceList("10Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
}
for i := range successCases {
test := successCases[i]
err := PersistentVolumeClaimValidateLimitFunc(&test.limitRange, &test.pvc)
if err != nil {
t.Errorf("Unexpected error for pvc: %s, %v", test.pvc.Name, err)
}
}
errorCases := []testCase{
{
pvc: validPersistentVolumeClaim("pvc-below-min-storage-request", getResourceRequirements(getStorageResourceList("500Mi"), getStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePersistentVolumeClaim, getStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
{
pvc: validPersistentVolumeClaim("pvc-exceeds-max-storage-request", getResourceRequirements(getStorageResourceList("100Gi"), getStorageResourceList(""))),
limitRange: createLimitRange(api.LimitTypePersistentVolumeClaim, getStorageResourceList("1Gi"), getStorageResourceList("1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}),
},
}
for i := range errorCases {
test := errorCases[i]
err := PersistentVolumeClaimValidateLimitFunc(&test.limitRange, &test.pvc)
if err == nil {
t.Errorf("Expected error for pvc: %s", test.pvc.Name)
}
}
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2014 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 limitranger
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
type LimitRangerActions interface {
// MutateLimit is a pluggable function to set limits on the object.
MutateLimit(limitRange *api.LimitRange, kind string, obj runtime.Object) error
// ValidateLimits is a pluggable function to enforce limits on the object.
ValidateLimit(limitRange *api.LimitRange, kind string, obj runtime.Object) error
// SupportsAttributes is a pluggable function to allow overridding what resources the limitranger
// supports.
SupportsAttributes(attr admission.Attributes) bool
// SupportsLimit is a pluggable function to allow ignoring limits that should not be applied
// for any reason.
SupportsLimit(limitRange *api.LimitRange) bool
}

View File

@ -0,0 +1,56 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/kubeapiserver/admission: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/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/kubeapiserver/admission: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/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/testing: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

@ -0,0 +1,119 @@
/*
Copyright 2014 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 autoprovision
import (
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
corelisters "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("NamespaceAutoProvision", func(config io.Reader) (admission.Interface, error) {
return NewProvision(), nil
})
}
// Provision is an implementation of admission.Interface.
// It looks at all incoming requests in a namespace context, and if the namespace does not exist, it creates one.
// It is useful in deployments that do not want to restrict creation of a namespace prior to its usage.
type Provision struct {
*admission.Handler
client internalclientset.Interface
namespaceLister corelisters.NamespaceLister
}
var _ admission.MutationInterface = &Provision{}
var _ = kubeapiserveradmission.WantsInternalKubeInformerFactory(&Provision{})
var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&Provision{})
// Admit makes an admission decision based on the request attributes
func (p *Provision) Admit(a admission.Attributes) error {
// if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do
// if we're here, then the API server has found a route, which means that if we have a non-empty namespace
// its a namespaced resource.
if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") {
return nil
}
// we need to wait for our caches to warm
if !p.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
_, err := p.namespaceLister.Get(a.GetNamespace())
if err == nil {
return nil
}
if !errors.IsNotFound(err) {
return admission.NewForbidden(a, err)
}
namespace := &api.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: a.GetNamespace(),
Namespace: "",
},
Status: api.NamespaceStatus{},
}
_, err = p.client.Core().Namespaces().Create(namespace)
if err != nil && !errors.IsAlreadyExists(err) {
return admission.NewForbidden(a, err)
}
return nil
}
// NewProvision creates a new namespace provision admission control handler
func NewProvision() *Provision {
return &Provision{
Handler: admission.NewHandler(admission.Create),
}
}
// SetInternalKubeClientSet implements the WantsInternalKubeClientSet interface.
func (p *Provision) SetInternalKubeClientSet(client internalclientset.Interface) {
p.client = client
}
// SetInternalKubeInformerFactory implements the WantsInternalKubeInformerFactory interface.
func (p *Provision) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
namespaceInformer := f.Core().InternalVersion().Namespaces()
p.namespaceLister = namespaceInformer.Lister()
p.SetReadyFunc(namespaceInformer.Informer().HasSynced)
}
// ValidateInitialization implements the InitializationValidator interface.
func (p *Provision) ValidateInitialization() error {
if p.namespaceLister == nil {
return fmt.Errorf("missing namespaceLister")
}
if p.client == nil {
return fmt.Errorf("missing client")
}
return nil
}

View File

@ -0,0 +1,172 @@
/*
Copyright 2014 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 autoprovision
import (
"fmt"
"testing"
"time"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
core "k8s.io/client-go/testing"
api "k8s.io/kubernetes/pkg/apis/core"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
// newHandlerForTest returns the admission controller configured for testing.
func newHandlerForTest(c clientset.Interface) (admission.MutationInterface, informers.SharedInformerFactory, error) {
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
handler := NewProvision()
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer.Initialize(handler)
err := admission.ValidateInitialization(handler)
return handler, f, err
}
// newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces.
func newMockClientForTest(namespaces []string) *fake.Clientset {
mockClient := &fake.Clientset{}
mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
namespaceList := &api.NamespaceList{
ListMeta: metav1.ListMeta{
ResourceVersion: fmt.Sprintf("%d", len(namespaces)),
},
}
for i, ns := range namespaces {
namespaceList.Items = append(namespaceList.Items, api.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ns,
ResourceVersion: fmt.Sprintf("%d", i),
},
})
}
return true, namespaceList, nil
})
return mockClient
}
// newPod returns a new pod for the specified namespace
func newPod(namespace string) api.Pod {
return api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
Spec: api.PodSpec{
Volumes: []api.Volume{{Name: "vol"}},
Containers: []api.Container{{Name: "ctr", Image: "image"}},
},
}
}
// hasCreateNamespaceAction returns true if it has the create namespace action
func hasCreateNamespaceAction(mockClient *fake.Clientset) bool {
for _, action := range mockClient.Actions() {
if action.GetVerb() == "create" && action.GetResource().Resource == "namespaces" {
return true
}
}
return false
}
// TestAdmission verifies a namespace is created on create requests for namespace managed resources
func TestAdmission(t *testing.T) {
namespace := "test"
mockClient := newMockClientForTest([]string{})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
pod := newPod(namespace)
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("unexpected error returned from admission handler")
}
if !hasCreateNamespaceAction(mockClient) {
t.Errorf("expected create namespace action")
}
}
// TestAdmissionNamespaceExists verifies that no client call is made when a namespace already exists
func TestAdmissionNamespaceExists(t *testing.T) {
namespace := "test"
mockClient := newMockClientForTest([]string{namespace})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
pod := newPod(namespace)
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("unexpected error returned from admission handler")
}
if hasCreateNamespaceAction(mockClient) {
t.Errorf("unexpected create namespace action")
}
}
// TestIgnoreAdmission validates that a request is ignored if its not a create
func TestIgnoreAdmission(t *testing.T) {
namespace := "test"
mockClient := newMockClientForTest([]string{})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
chainHandler := admission.NewChainHandler(handler)
pod := newPod(namespace)
err = chainHandler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, nil))
if err != nil {
t.Errorf("unexpected error returned from admission handler")
}
if hasCreateNamespaceAction(mockClient) {
t.Errorf("unexpected create namespace action")
}
}
func TestAdmissionWithLatentCache(t *testing.T) {
namespace := "test"
mockClient := newMockClientForTest([]string{})
mockClient.AddReactor("create", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
return true, nil, errors.NewAlreadyExists(api.Resource("namespaces"), namespace)
})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
pod := newPod(namespace)
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("unexpected error returned from admission handler")
}
if !hasCreateNamespaceAction(mockClient) {
t.Errorf("expected create namespace action")
}
}

View File

@ -0,0 +1,55 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/kubeapiserver/admission: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/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/kubeapiserver/admission: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/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/client-go/testing: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

@ -0,0 +1,114 @@
/*
Copyright 2014 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 exists
import (
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
corelisters "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("NamespaceExists", func(config io.Reader) (admission.Interface, error) {
return NewExists(), nil
})
}
// Exists is an implementation of admission.Interface.
// It rejects all incoming requests in a namespace context if the namespace does not exist.
// It is useful in deployments that want to enforce pre-declaration of a Namespace resource.
type Exists struct {
*admission.Handler
client internalclientset.Interface
namespaceLister corelisters.NamespaceLister
}
var _ admission.ValidationInterface = &Exists{}
var _ = kubeapiserveradmission.WantsInternalKubeInformerFactory(&Exists{})
var _ = kubeapiserveradmission.WantsInternalKubeClientSet(&Exists{})
// Validate makes an admission decision based on the request attributes
func (e *Exists) Validate(a admission.Attributes) error {
// if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do
// if we're here, then the API server has found a route, which means that if we have a non-empty namespace
// its a namespaced resource.
if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") {
return nil
}
// we need to wait for our caches to warm
if !e.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
_, err := e.namespaceLister.Get(a.GetNamespace())
if err == nil {
return nil
}
if !errors.IsNotFound(err) {
return errors.NewInternalError(err)
}
// in case of latency in our caches, make a call direct to storage to verify that it truly exists or not
_, err = e.client.Core().Namespaces().Get(a.GetNamespace(), metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
return err
}
return errors.NewInternalError(err)
}
return nil
}
// NewExists creates a new namespace exists admission control handler
func NewExists() *Exists {
return &Exists{
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
}
}
// SetInternalKubeClientSet implements the WantsInternalKubeClientSet interface.
func (e *Exists) SetInternalKubeClientSet(client internalclientset.Interface) {
e.client = client
}
// SetInternalKubeInformerFactory implements the WantsInternalKubeInformerFactory interface.
func (e *Exists) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
namespaceInformer := f.Core().InternalVersion().Namespaces()
e.namespaceLister = namespaceInformer.Lister()
e.SetReadyFunc(namespaceInformer.Informer().HasSynced)
}
// ValidateInitialization implements the InitializationValidator interface.
func (e *Exists) ValidateInitialization() error {
if e.namespaceLister == nil {
return fmt.Errorf("missing namespaceLister")
}
if e.client == nil {
return fmt.Errorf("missing client")
}
return nil
}

View File

@ -0,0 +1,118 @@
/*
Copyright 2014 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 exists
import (
"fmt"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
core "k8s.io/client-go/testing"
api "k8s.io/kubernetes/pkg/apis/core"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
// newHandlerForTest returns the admission controller configured for testing.
func newHandlerForTest(c clientset.Interface) (admission.ValidationInterface, informers.SharedInformerFactory, error) {
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
handler := NewExists()
pluginInitializer := kubeadmission.NewPluginInitializer(c, f, nil, nil, nil)
pluginInitializer.Initialize(handler)
err := admission.ValidateInitialization(handler)
return handler, f, err
}
// newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces.
func newMockClientForTest(namespaces []string) *fake.Clientset {
mockClient := &fake.Clientset{}
mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
namespaceList := &api.NamespaceList{
ListMeta: metav1.ListMeta{
ResourceVersion: fmt.Sprintf("%d", len(namespaces)),
},
}
for i, ns := range namespaces {
namespaceList.Items = append(namespaceList.Items, api.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: ns,
ResourceVersion: fmt.Sprintf("%d", i),
},
})
}
return true, namespaceList, nil
})
return mockClient
}
// newPod returns a new pod for the specified namespace
func newPod(namespace string) api.Pod {
return api.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
Spec: api.PodSpec{
Volumes: []api.Volume{{Name: "vol"}},
Containers: []api.Container{{Name: "ctr", Image: "image"}},
},
}
}
// TestAdmissionNamespaceExists verifies pod is admitted only if namespace exists.
func TestAdmissionNamespaceExists(t *testing.T) {
namespace := "test"
mockClient := newMockClientForTest([]string{namespace})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
pod := newPod(namespace)
err = handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("unexpected error returned from admission handler")
}
}
// TestAdmissionNamespaceDoesNotExist verifies pod is not admitted if namespace does not exist.
func TestAdmissionNamespaceDoesNotExist(t *testing.T) {
namespace := "test"
mockClient := newMockClientForTest([]string{})
mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
return true, nil, fmt.Errorf("nope, out of luck")
})
handler, informerFactory, err := newHandlerForTest(mockClient)
if err != nil {
t.Errorf("unexpected error initializing handler: %v", err)
}
informerFactory.Start(wait.NeverStop)
pod := newPod(namespace)
err = handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
if err == nil {
actions := ""
for _, action := range mockClient.Actions() {
actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", "
}
t.Errorf("expected error returned from admission handler: %v", actions)
}
}

View File

@ -0,0 +1,59 @@
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"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/noderestriction",
deps = [
"//pkg/api/pod:go_default_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/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",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/noderestriction",
library = ":go_default_library",
deps = [
"//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",
"//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",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,8 @@
approvers:
- deads2k
- liggitt
- tallclair
reviewers:
- deads2k
- liggitt
- tallclair

View File

@ -0,0 +1,342 @@
/*
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 noderestriction
import (
"fmt"
"io"
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"
podutil "k8s.io/kubernetes/pkg/api/pod"
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"
"k8s.io/kubernetes/pkg/features"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
const (
PluginName = "NodeRestriction"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
return NewPlugin(nodeidentifier.NewDefaultNodeIdentifier()), nil
})
}
// NewPlugin creates a new NodeRestriction admission plugin.
// This plugin identifies requests from nodes
func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier) *nodePlugin {
return &nodePlugin{
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
nodeIdentifier: nodeIdentifier,
}
}
// nodePlugin holds state for and implements the admission plugin.
type nodePlugin struct {
*admission.Handler
nodeIdentifier nodeidentifier.NodeIdentifier
podsGetter coreinternalversion.PodsGetter
}
var (
_ = admission.Interface(&nodePlugin{})
_ = kubeapiserveradmission.WantsInternalKubeClientSet(&nodePlugin{})
)
func (p *nodePlugin) SetInternalKubeClientSet(f internalclientset.Interface) {
p.podsGetter = f.Core()
}
func (p *nodePlugin) ValidateInitialization() error {
if p.nodeIdentifier == nil {
return fmt.Errorf("%s requires a node identifier", PluginName)
}
if p.podsGetter == nil {
return fmt.Errorf("%s requires a pod getter", PluginName)
}
return nil
}
var (
podResource = api.Resource("pods")
nodeResource = api.Resource("nodes")
pvcResource = api.Resource("persistentvolumeclaims")
)
func (c *nodePlugin) Admit(a admission.Attributes) error {
nodeName, isNode := c.nodeIdentifier.NodeIdentity(a.GetUserInfo())
// Our job is just to restrict nodes
if !isNode {
return nil
}
if len(nodeName) == 0 {
// disallow requests we cannot match to a particular node
return admission.NewForbidden(a, fmt.Errorf("could not determine node from user %q", a.GetUserInfo().GetName()))
}
switch a.GetResource().GroupResource() {
case podResource:
switch a.GetSubresource() {
case "":
return c.admitPod(nodeName, a)
case "status":
return c.admitPodStatus(nodeName, a)
case "eviction":
return c.admitPodEviction(nodeName, a)
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected pod subresource %q", a.GetSubresource()))
}
case nodeResource:
return c.admitNode(nodeName, a)
case pvcResource:
switch a.GetSubresource() {
case "status":
return c.admitPVCStatus(nodeName, a)
default:
return admission.NewForbidden(a, fmt.Errorf("may only update PVC status"))
}
default:
return nil
}
}
func (c *nodePlugin) admitPod(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
case admission.Create:
// require a pod object
pod, ok := a.GetObject().(*api.Pod)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// only allow nodes to create mirror pods
if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; !isMirrorPod {
return admission.NewForbidden(a, fmt.Errorf("pod does not have %q annotation, node %q can only create mirror pods", api.MirrorPodAnnotationKey, nodeName))
}
// only allow nodes to create a pod bound to itself
if pod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %q can only create pods with spec.nodeName set to itself", nodeName))
}
// don't allow a node to create a pod that references any other API objects
if pod.Spec.ServiceAccountName != "" {
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference a service account", nodeName))
}
hasSecrets := false
podutil.VisitPodSecretNames(pod, func(name string) (shouldContinue bool) { hasSecrets = true; return false })
if hasSecrets {
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference secrets", nodeName))
}
hasConfigMaps := false
podutil.VisitPodConfigmapNames(pod, func(name string) (shouldContinue bool) { hasConfigMaps = true; return false })
if hasConfigMaps {
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference configmaps", nodeName))
}
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil {
return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference persistentvolumeclaims", nodeName))
}
}
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"})
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
}
}
if err != nil {
return admission.NewForbidden(a, err)
}
// only allow a node to delete a pod bound to itself
if existingPod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %q can only delete pods with spec.nodeName set to itself", nodeName))
}
return nil
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
}
}
func (c *nodePlugin) admitPodStatus(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
case admission.Update:
// require an existing pod
pod, ok := a.GetOldObject().(*api.Pod)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject()))
}
// only allow a node to update status of a pod bound to itself
if pod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %q can only update pod status for pods with spec.nodeName set to itself", nodeName))
}
return nil
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
}
}
func (c *nodePlugin) admitPodEviction(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
case admission.Create:
// require eviction to an existing pod object
eviction, ok := a.GetObject().(*policy.Eviction)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// use pod name from the admission attributes, if set, rather than from the submitted Eviction object
podName := a.GetName()
if len(podName) == 0 {
if len(eviction.Name) == 0 {
return admission.NewForbidden(a, fmt.Errorf("could not determine pod from request data"))
}
podName = eviction.Name
}
// get the existing pod from the server cache
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(podName, v1.GetOptions{ResourceVersion: "0"})
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
}
}
if err != nil {
return admission.NewForbidden(a, err)
}
// only allow a node to evict a pod bound to itself
if existingPod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %s can only evict pods with spec.nodeName set to itself", nodeName))
}
return nil
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation()))
}
}
func (c *nodePlugin) admitPVCStatus(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
case admission.Update:
if !utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) {
return admission.NewForbidden(a, fmt.Errorf("node %q may not update persistentvolumeclaim metadata", nodeName))
}
oldPVC, ok := a.GetOldObject().(*api.PersistentVolumeClaim)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetOldObject()))
}
newPVC, ok := a.GetObject().(*api.PersistentVolumeClaim)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// make copies for comparison
oldPVC = oldPVC.DeepCopy()
newPVC = newPVC.DeepCopy()
// zero out resourceVersion to avoid comparing differences,
// since the new object could leave it empty to indicate an unconditional update
oldPVC.ObjectMeta.ResourceVersion = ""
newPVC.ObjectMeta.ResourceVersion = ""
oldPVC.Status.Capacity = nil
newPVC.Status.Capacity = nil
oldPVC.Status.Conditions = nil
newPVC.Status.Conditions = nil
// ensure no metadata changed. nodes should not be able to relabel, add finalizers/owners, etc
if !apiequality.Semantic.DeepEqual(oldPVC, newPVC) {
return admission.NewForbidden(a, fmt.Errorf("node %q may not update fields other than status.capacity and status.conditions: %v", nodeName, diff.ObjectReflectDiff(oldPVC, newPVC)))
}
return nil
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %q", a.GetOperation()))
}
}
func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error {
requestedName := a.GetName()
if a.GetOperation() == admission.Create {
node, ok := a.GetObject().(*api.Node)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// Don't allow a node to create its Node API object with the config source set.
// We scope node access to things listed in the Node.Spec, so allowing this would allow a view escalation.
if node.Spec.ConfigSource != nil {
return admission.NewForbidden(a, fmt.Errorf("cannot create with non-nil configSource"))
}
// On create, get name from new object if unset in admission
if len(requestedName) == 0 {
requestedName = node.Name
}
}
if requestedName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %q cannot modify node %q", nodeName, requestedName))
}
if a.GetOperation() == admission.Update {
node, ok := a.GetObject().(*api.Node)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
oldNode, ok := a.GetOldObject().(*api.Node)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// Don't allow a node to update the config source on its Node API object.
// We scope node access to things listed in the Node.Spec, so allowing this would allow a view escalation.
// We only do the check if the new node's configSource is non-nil; old kubelets might drop the field during a status update.
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"))
}
}
return nil
}

View File

@ -0,0 +1,729 @@
/*
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 noderestriction
import (
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/policy"
policyapi "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"
)
func makeTestPod(namespace, name, node string, mirror bool) *api.Pod {
pod := &api.Pod{}
pod.Namespace = namespace
pod.Name = name
pod.Spec.NodeName = node
if mirror {
pod.Annotations = map[string]string{api.MirrorPodAnnotationKey: "true"}
}
return pod
}
func makeTestPodEviction(name string) *policy.Eviction {
eviction := &policy.Eviction{}
eviction.Name = name
return eviction
}
func Test_nodePlugin_Admit(t *testing.T) {
var (
mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}}
bob = &user.DefaultInfo{Name: "bob"}
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"}}}}
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"}}
mymirrorpod = makeTestPod("ns", "mymirrorpod", "mynode", true)
othermirrorpod = makeTestPod("ns", "othermirrorpod", "othernode", true)
unboundmirrorpod = makeTestPod("ns", "unboundmirrorpod", "", true)
mypod = makeTestPod("ns", "mypod", "mynode", false)
otherpod = makeTestPod("ns", "otherpod", "othernode", false)
unboundpod = makeTestPod("ns", "unboundpod", "", false)
unnamedpod = makeTestPod("ns", "", "mynode", false)
mymirrorpodEviction = makeTestPodEviction("mymirrorpod")
othermirrorpodEviction = makeTestPodEviction("othermirrorpod")
unboundmirrorpodEviction = makeTestPodEviction("unboundmirrorpod")
mypodEviction = makeTestPodEviction("mypod")
otherpodEviction = makeTestPodEviction("otherpod")
unboundpodEviction = makeTestPodEviction("unboundpod")
unnamedEviction = makeTestPodEviction("")
configmapResource = api.Resource("configmap").WithVersion("v1")
configmapKind = api.Kind("ConfigMap").WithVersion("v1")
podResource = api.Resource("pods").WithVersion("v1")
podKind = api.Kind("Pod").WithVersion("v1")
evictionKind = policyapi.Kind("Eviction").WithVersion("v1beta1")
nodeResource = api.Resource("nodes").WithVersion("v1")
nodeKind = api.Kind("Node").WithVersion("v1")
noExistingPods = fake.NewSimpleClientset().Core()
existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core()
)
sapod := makeTestPod("ns", "mysapod", "mynode", true)
sapod.Spec.ServiceAccountName = "foo"
secretpod := makeTestPod("ns", "mysecretpod", "mynode", true)
secretpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}}}
configmappod := makeTestPod("ns", "myconfigmappod", "mynode", true)
configmappod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "foo"}}}}}
pvcpod := makeTestPod("ns", "mypvcpod", "mynode", true)
pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}}
tests := []struct {
name string
podsGetter coreinternalversion.PodsGetter
attributes admission.Attributes
err string
}{
// Mirror pods bound to us
{
name: "allow creating a mirror pod bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Create, mynode),
err: "",
},
{
name: "forbid update of mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow delete of mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Delete, mynode),
err: "",
},
{
name: "forbid create of mirror pod status bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow update of mirror pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Update, mynode),
err: "",
},
{
name: "forbid delete of mirror pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow create of eviction for mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpodEviction, nil, evictionKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "eviction", admission.Create, mynode),
err: "",
},
{
name: "forbid update of eviction for mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpodEviction, nil, evictionKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpodEviction, nil, evictionKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow create of unnamed eviction for mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "eviction", admission.Create, mynode),
err: "",
},
// Mirror pods bound to another node
{
name: "forbid creating a mirror pod bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of mirror pod status bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of mirror pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of mirror pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of eviction for mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpodEviction, nil, evictionKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of eviction for mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpodEviction, nil, evictionKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpodEviction, nil, evictionKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of unnamed eviction for mirror pod to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
// Mirror pods not bound to any node
{
name: "forbid creating a mirror pod unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of mirror pod status unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of mirror pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of mirror pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of eviction for mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpodEviction, nil, evictionKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of eviction for mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpodEviction, nil, evictionKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpodEviction, nil, evictionKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of unnamed eviction for mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
// Normal pods bound to us
{
name: "forbid creating a normal pod bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Create, mynode),
err: "can only create mirror pods",
},
{
name: "forbid update of normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow delete of normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Delete, mynode),
err: "",
},
{
name: "forbid create of normal pod status bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow update of normal pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Update, mynode),
err: "",
},
{
name: "forbid delete of normal pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of eviction for normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow create of unnamed eviction for normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Create, mynode),
err: "",
},
// Normal pods bound to another
{
name: "forbid creating a normal pod bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Create, mynode),
err: "can only create mirror pods",
},
{
name: "forbid update of normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of normal pod status bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of normal pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of normal pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of eviction for normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpodEviction, nil, evictionKind, otherpodEviction.Namespace, otherpodEviction.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of eviction for normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpodEviction, nil, evictionKind, otherpodEviction.Namespace, otherpodEviction.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpodEviction, nil, evictionKind, otherpodEviction.Namespace, otherpodEviction.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of 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",
},
// Normal pods not bound to any node
{
name: "forbid creating a normal pod unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, mynode),
err: "can only create mirror pods",
},
{
name: "forbid update of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of normal pod status unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of eviction for normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpodEviction, nil, evictionKind, unboundpod.Namespace, unboundpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of eviction for normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpodEviction, nil, evictionKind, unboundpod.Namespace, unboundpod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpodEviction, nil, evictionKind, unboundpod.Namespace, unboundpod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of unnamed eviction for normal unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, unboundpod.Namespace, unboundpod.Name, podResource, "eviction", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
// Missing pod
{
name: "forbid delete of unknown pod",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode),
err: "not found",
},
{
name: "forbid create of eviction for unknown pod",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Create, mynode),
err: "not found",
},
{
name: "forbid update of eviction for unknown pod",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for unknown pod",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of unnamed eviction for unknown pod",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, mypod.Namespace, mypod.Name, podResource, "eviction", admission.Create, mynode),
err: "not found",
},
// Eviction for unnamed pod
{
name: "allow create of eviction for unnamed pod",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, unnamedpod.Namespace, unnamedpod.Name, podResource, "eviction", admission.Create, mynode),
// use the submitted eviction resource name as the pod name
err: "",
},
{
name: "forbid update of eviction for unnamed pod",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, unnamedpod.Namespace, unnamedpod.Name, podResource, "eviction", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of eviction for unnamed pod",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, unnamedpod.Namespace, unnamedpod.Name, podResource, "eviction", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid create of unnamed eviction for unnamed pod",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, unnamedpod.Namespace, unnamedpod.Name, podResource, "eviction", admission.Create, mynode),
err: "could not determine pod from request data",
},
// Resource pods
{
name: "forbid create of pod referencing service account",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(sapod, nil, podKind, sapod.Namespace, sapod.Name, podResource, "", admission.Create, mynode),
err: "reference a service account",
},
{
name: "forbid create of pod referencing secret",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(secretpod, nil, podKind, secretpod.Namespace, secretpod.Name, podResource, "", admission.Create, mynode),
err: "reference secrets",
},
{
name: "forbid create of pod referencing configmap",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(configmappod, nil, podKind, configmappod.Namespace, configmappod.Name, podResource, "", admission.Create, mynode),
err: "reference configmaps",
},
{
name: "forbid create of pod referencing persistentvolumeclaim",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(pvcpod, nil, podKind, pvcpod.Namespace, pvcpod.Name, podResource, "", admission.Create, mynode),
err: "reference persistentvolumeclaims",
},
// My node object
{
name: "allow create of my node",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow create of my node pulling name from object",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, "", nodeResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow update of my node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "",
},
{
name: "allow delete of my node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Delete, mynode),
err: "",
},
{
name: "allow update of my node status",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "status", admission.Update, mynode),
err: "",
},
{
name: "forbid create of my node with non-nil configSource",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mynodeObjConfigA, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Create, mynode),
err: "create with non-nil configSource",
},
{
name: "forbid update of my node: nil configSource to new non-nil configSource",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObjConfigA, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "update configSource to a new non-nil configSource",
},
{
name: "forbid update of my node: non-nil configSource to new non-nil configSource",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObjConfigB, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "update configSource to a new non-nil configSource",
},
{
name: "allow update of my node: non-nil configSource unchanged",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObjConfigA, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "",
},
{
name: "allow update of my node: non-nil configSource to nil configSource",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "",
},
// Other node object
{
name: "forbid create of other node",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Create, mynode),
err: "cannot modify node",
},
{
name: "forbid create of other node pulling name from object",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, "", nodeResource, "", admission.Create, mynode),
err: "cannot modify node",
},
{
name: "forbid update of other node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "cannot modify node",
},
{
name: "forbid delete of other node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Delete, mynode),
err: "cannot modify node",
},
{
name: "forbid update of other node status",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "status", admission.Update, mynode),
err: "cannot modify node",
},
// Unrelated objects
{
name: "allow create of unrelated object",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(&api.ConfigMap{}, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow update of unrelated object",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(&api.ConfigMap{}, &api.ConfigMap{}, configmapKind, "myns", "mycm", configmapResource, "", admission.Update, mynode),
err: "",
},
{
name: "allow delete of unrelated object",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Delete, mynode),
err: "",
},
// Unrelated user
{
name: "allow unrelated user creating a normal pod unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, bob),
err: "",
},
{
name: "allow unrelated user update of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, bob),
err: "",
},
{
name: "allow unrelated user delete of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, bob),
err: "",
},
{
name: "allow unrelated user create of normal pod status unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, bob),
err: "",
},
{
name: "allow unrelated user update of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, bob),
err: "",
},
{
name: "allow unrelated user delete of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, bob),
err: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier())
c.podsGetter = tt.podsGetter
err := c.Admit(tt.attributes)
if (err == nil) != (len(tt.err) == 0) {
t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err)
return
}
if len(tt.err) > 0 && !strings.Contains(err.Error(), tt.err) {
t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err)
}
})
}
}

View File

@ -0,0 +1,55 @@
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",
"doc.go",
],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/cloudprovider:go_default_library",
"//pkg/cloudprovider/providers/aws:go_default_library",
"//pkg/cloudprovider/providers/gce:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//pkg/kubelet/apis:go_default_library",
"//pkg/volume:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/cloudprovider/providers/aws: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/apimachinery/pkg/types: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

@ -0,0 +1,216 @@
/*
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 label
import (
"bytes"
"fmt"
"io"
"sync"
"github.com/golang/glog"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/cloudprovider/providers/aws"
"k8s.io/kubernetes/pkg/cloudprovider/providers/gce"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
vol "k8s.io/kubernetes/pkg/volume"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register("PersistentVolumeLabel", func(config io.Reader) (admission.Interface, error) {
persistentVolumeLabelAdmission := NewPersistentVolumeLabel()
return persistentVolumeLabelAdmission, nil
})
}
var _ = admission.Interface(&persistentVolumeLabel{})
type persistentVolumeLabel struct {
*admission.Handler
mutex sync.Mutex
ebsVolumes aws.Volumes
cloudConfig []byte
gceCloudProvider *gce.GCECloud
}
var _ admission.MutationInterface = &persistentVolumeLabel{}
var _ kubeapiserveradmission.WantsCloudConfig = &persistentVolumeLabel{}
// 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 {
// 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.
glog.Warning("PersistentVolumeLabel admission controller is deprecated. " +
"Please remove this controller from your configuration files and scripts.")
return &persistentVolumeLabel{
Handler: admission.NewHandler(admission.Create),
}
}
func (l *persistentVolumeLabel) SetCloudConfig(cloudConfig []byte) {
l.cloudConfig = cloudConfig
}
func (l *persistentVolumeLabel) Admit(a admission.Attributes) (err error) {
if a.GetResource().GroupResource() != api.Resource("persistentvolumes") {
return nil
}
obj := a.GetObject()
if obj == nil {
return nil
}
volume, ok := obj.(*api.PersistentVolume)
if !ok {
return nil
}
var volumeLabels map[string]string
if volume.Spec.AWSElasticBlockStore != nil {
labels, err := l.findAWSEBSLabels(volume)
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("error querying AWS EBS volume %s: %v", volume.Spec.AWSElasticBlockStore.VolumeID, err))
}
volumeLabels = labels
}
if volume.Spec.GCEPersistentDisk != nil {
labels, err := l.findGCEPDLabels(volume)
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("error querying GCE PD volume %s: %v", volume.Spec.GCEPersistentDisk.PDName, err))
}
volumeLabels = labels
}
if len(volumeLabels) != 0 {
if volume.Labels == nil {
volume.Labels = make(map[string]string)
}
for k, v := range volumeLabels {
// We (silently) replace labels if they are provided.
// This should be OK because they are in the kubernetes.io namespace
// i.e. we own them
volume.Labels[k] = v
}
}
return nil
}
func (l *persistentVolumeLabel) findAWSEBSLabels(volume *api.PersistentVolume) (map[string]string, error) {
// Ignore any volumes that are being provisioned
if volume.Spec.AWSElasticBlockStore.VolumeID == vol.ProvisionedVolumeName {
return nil, nil
}
ebsVolumes, err := l.getEBSVolumes()
if err != nil {
return nil, err
}
if ebsVolumes == nil {
return nil, fmt.Errorf("unable to build AWS cloud provider for EBS")
}
// TODO: GetVolumeLabels is actually a method on the Volumes interface
// If that gets standardized we can refactor to reduce code duplication
spec := aws.KubernetesVolumeID(volume.Spec.AWSElasticBlockStore.VolumeID)
labels, err := ebsVolumes.GetVolumeLabels(spec)
if err != nil {
return nil, err
}
return labels, nil
}
// getEBSVolumes returns the AWS Volumes interface for ebs
func (l *persistentVolumeLabel) getEBSVolumes() (aws.Volumes, error) {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.ebsVolumes == nil {
var cloudConfigReader io.Reader
if len(l.cloudConfig) > 0 {
cloudConfigReader = bytes.NewReader(l.cloudConfig)
}
cloudProvider, err := cloudprovider.GetCloudProvider("aws", cloudConfigReader)
if err != nil || cloudProvider == nil {
return nil, err
}
awsCloudProvider, ok := cloudProvider.(*aws.Cloud)
if !ok {
// GetCloudProvider has gone very wrong
return nil, fmt.Errorf("error retrieving AWS cloud provider")
}
l.ebsVolumes = awsCloudProvider
}
return l.ebsVolumes, nil
}
func (l *persistentVolumeLabel) findGCEPDLabels(volume *api.PersistentVolume) (map[string]string, error) {
// Ignore any volumes that are being provisioned
if volume.Spec.GCEPersistentDisk.PDName == vol.ProvisionedVolumeName {
return nil, nil
}
provider, err := l.getGCECloudProvider()
if err != nil {
return nil, err
}
if provider == nil {
return nil, fmt.Errorf("unable to build GCE cloud provider for PD")
}
// If the zone is already labeled, honor the hint
zone := volume.Labels[kubeletapis.LabelZoneFailureDomain]
labels, err := provider.GetAutoLabelsForPD(volume.Spec.GCEPersistentDisk.PDName, zone)
if err != nil {
return nil, err
}
return labels, nil
}
// getGCECloudProvider returns the GCE cloud provider, for use for querying volume labels
func (l *persistentVolumeLabel) getGCECloudProvider() (*gce.GCECloud, error) {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.gceCloudProvider == nil {
var cloudConfigReader io.Reader
if len(l.cloudConfig) > 0 {
cloudConfigReader = bytes.NewReader(l.cloudConfig)
}
cloudProvider, err := cloudprovider.GetCloudProvider("gce", cloudConfigReader)
if err != nil || cloudProvider == nil {
return nil, err
}
gceCloudProvider, ok := cloudProvider.(*gce.GCECloud)
if !ok {
// GetCloudProvider has gone very wrong
return nil, fmt.Errorf("error retrieving GCE cloud provider")
}
l.gceCloudProvider = gceCloudProvider
}
return l.gceCloudProvider, nil
}

View File

@ -0,0 +1,176 @@
/*
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 label
import (
"testing"
"fmt"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/cloudprovider/providers/aws"
)
type mockVolumes struct {
volumeLabels map[string]string
volumeLabelsError error
}
var _ aws.Volumes = &mockVolumes{}
func (v *mockVolumes) AttachDisk(diskName aws.KubernetesVolumeID, nodeName types.NodeName, readOnly bool) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (v *mockVolumes) DetachDisk(diskName aws.KubernetesVolumeID, nodeName types.NodeName) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (v *mockVolumes) CreateDisk(volumeOptions *aws.VolumeOptions) (volumeName aws.KubernetesVolumeID, err error) {
return "", fmt.Errorf("not implemented")
}
func (v *mockVolumes) DeleteDisk(volumeName aws.KubernetesVolumeID) (bool, error) {
return false, fmt.Errorf("not implemented")
}
func (v *mockVolumes) GetVolumeLabels(volumeName aws.KubernetesVolumeID) (map[string]string, error) {
return v.volumeLabels, v.volumeLabelsError
}
func (c *mockVolumes) GetDiskPath(volumeName aws.KubernetesVolumeID) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (c *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) {
return nil, fmt.Errorf("not implemented")
}
func (c *mockVolumes) ResizeDisk(
diskName aws.KubernetesVolumeID,
oldSize resource.Quantity,
newSize resource.Quantity) (resource.Quantity, error) {
return oldSize, nil
}
func mockVolumeFailure(err error) *mockVolumes {
return &mockVolumes{volumeLabelsError: err}
}
func mockVolumeLabels(labels map[string]string) *mockVolumes {
return &mockVolumes{volumeLabels: labels}
}
// TestAdmission
func TestAdmission(t *testing.T) {
pvHandler := NewPersistentVolumeLabel()
handler := admission.NewChainHandler(pvHandler)
ignoredPV := api.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
Spec: api.PersistentVolumeSpec{
PersistentVolumeSource: api.PersistentVolumeSource{
HostPath: &api.HostPathVolumeSource{
Path: "/",
},
},
},
}
awsPV := api.PersistentVolume{
ObjectMeta: metav1.ObjectMeta{Name: "noncloud", Namespace: "myns"},
Spec: api.PersistentVolumeSpec{
PersistentVolumeSource: api.PersistentVolumeSource{
AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{
VolumeID: "123",
},
},
},
}
// Non-cloud PVs are ignored
err := handler.Admit(admission.NewAttributesRecord(&ignoredPV, nil, api.Kind("PersistentVolume").WithVersion("version"), ignoredPV.Namespace, ignoredPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("Unexpected error returned from admission handler (on ignored pv): %v", err)
}
// We only add labels on creation
err = handler.Admit(admission.NewAttributesRecord(&awsPV, nil, api.Kind("PersistentVolume").WithVersion("version"), awsPV.Namespace, awsPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Delete, nil))
if err != nil {
t.Errorf("Unexpected error returned from admission handler (when deleting aws pv): %v", err)
}
// Errors from the cloudprovider block creation of the volume
pvHandler.ebsVolumes = mockVolumeFailure(fmt.Errorf("invalid volume"))
err = handler.Admit(admission.NewAttributesRecord(&awsPV, nil, api.Kind("PersistentVolume").WithVersion("version"), awsPV.Namespace, awsPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, nil))
if err == nil {
t.Errorf("Expected error when aws pv info fails")
}
// Don't add labels if the cloudprovider doesn't return any
labels := make(map[string]string)
pvHandler.ebsVolumes = mockVolumeLabels(labels)
err = handler.Admit(admission.NewAttributesRecord(&awsPV, nil, api.Kind("PersistentVolume").WithVersion("version"), awsPV.Namespace, awsPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("Expected no error when creating aws pv")
}
if len(awsPV.ObjectMeta.Labels) != 0 {
t.Errorf("Unexpected number of labels")
}
// Don't panic if the cloudprovider returns nil, nil
pvHandler.ebsVolumes = mockVolumeFailure(nil)
err = handler.Admit(admission.NewAttributesRecord(&awsPV, nil, api.Kind("PersistentVolume").WithVersion("version"), awsPV.Namespace, awsPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("Expected no error when cloud provider returns empty labels")
}
// Labels from the cloudprovider should be applied to the volume
labels = make(map[string]string)
labels["a"] = "1"
labels["b"] = "2"
pvHandler.ebsVolumes = mockVolumeLabels(labels)
err = handler.Admit(admission.NewAttributesRecord(&awsPV, nil, api.Kind("PersistentVolume").WithVersion("version"), awsPV.Namespace, awsPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("Expected no error when creating aws pv")
}
if awsPV.Labels["a"] != "1" || awsPV.Labels["b"] != "2" {
t.Errorf("Expected label a to be added when creating aws pv")
}
// User-provided labels should be honored, but cloudprovider labels replace them when they overlap
awsPV.ObjectMeta.Labels = make(map[string]string)
awsPV.ObjectMeta.Labels["a"] = "not1"
awsPV.ObjectMeta.Labels["c"] = "3"
err = handler.Admit(admission.NewAttributesRecord(&awsPV, nil, api.Kind("PersistentVolume").WithVersion("version"), awsPV.Namespace, awsPV.Name, api.Resource("persistentvolumes").WithVersion("version"), "", admission.Create, nil))
if err != nil {
t.Errorf("Expected no error when creating aws pv")
}
if awsPV.Labels["a"] != "1" || awsPV.Labels["b"] != "2" {
t.Errorf("Expected cloudprovider labels to replace user labels when creating aws pv")
}
if awsPV.Labels["c"] != "3" {
t.Errorf("Expected (non-conflicting) user provided labels to be honored when creating aws pv")
}
}

View File

@ -0,0 +1,19 @@
/*
Copyright 2014 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.
*/
// labels created persistent volumes with zone information
// as provided by the cloud provider
package label // import "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label"

View File

@ -0,0 +1,53 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/resize",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/storage:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/controller: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/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = ["admission.go"],
importpath = "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/resize",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/helper:go_default_library",
"//pkg/client/informers/informers_generated/internalversion:go_default_library",
"//pkg/client/listers/core/internalversion:go_default_library",
"//pkg/client/listers/storage/internalversion:go_default_library",
"//pkg/kubeapiserver/admission: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

@ -0,0 +1,165 @@
/*
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 resize
import (
"fmt"
"io"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
apihelper "k8s.io/kubernetes/pkg/apis/core/helper"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion"
pvlister "k8s.io/kubernetes/pkg/client/listers/core/internalversion"
storagelisters "k8s.io/kubernetes/pkg/client/listers/storage/internalversion"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
const (
// PluginName is the name of pvc resize admission plugin
PluginName = "PersistentVolumeClaimResize"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
plugin := newPlugin()
return plugin, nil
})
}
var _ admission.Interface = &persistentVolumeClaimResize{}
var _ admission.ValidationInterface = &persistentVolumeClaimResize{}
var _ = kubeapiserveradmission.WantsInternalKubeInformerFactory(&persistentVolumeClaimResize{})
type persistentVolumeClaimResize struct {
*admission.Handler
pvLister pvlister.PersistentVolumeLister
scLister storagelisters.StorageClassLister
}
func newPlugin() *persistentVolumeClaimResize {
return &persistentVolumeClaimResize{
Handler: admission.NewHandler(admission.Update),
}
}
func (pvcr *persistentVolumeClaimResize) SetInternalKubeInformerFactory(f informers.SharedInformerFactory) {
pvcInformer := f.Core().InternalVersion().PersistentVolumes()
pvcr.pvLister = pvcInformer.Lister()
scInformer := f.Storage().InternalVersion().StorageClasses()
pvcr.scLister = scInformer.Lister()
pvcr.SetReadyFunc(func() bool {
return pvcInformer.Informer().HasSynced() && scInformer.Informer().HasSynced()
})
}
// ValidateInitialization ensures lister is set.
func (pvcr *persistentVolumeClaimResize) ValidateInitialization() error {
if pvcr.pvLister == nil {
return fmt.Errorf("missing persistent volume lister")
}
if pvcr.scLister == nil {
return fmt.Errorf("missing storageclass lister")
}
return nil
}
func (pvcr *persistentVolumeClaimResize) Validate(a admission.Attributes) error {
if a.GetResource().GroupResource() != api.Resource("persistentvolumeclaims") {
return nil
}
if len(a.GetSubresource()) != 0 {
return nil
}
pvc, ok := a.GetObject().(*api.PersistentVolumeClaim)
// if we can't convert then we don't handle this object so just return
if !ok {
return nil
}
oldPvc, ok := a.GetOldObject().(*api.PersistentVolumeClaim)
if !ok {
return nil
}
oldSize := oldPvc.Spec.Resources.Requests[api.ResourceStorage]
newSize := pvc.Spec.Resources.Requests[api.ResourceStorage]
if newSize.Cmp(oldSize) <= 0 {
return nil
}
if oldPvc.Status.Phase != api.ClaimBound {
return admission.NewForbidden(a, fmt.Errorf("Only bound persistent volume claims can be expanded"))
}
// Growing Persistent volumes is only allowed for PVCs for which their StorageClass
// explicitly allows it
if !pvcr.allowResize(pvc, oldPvc) {
return admission.NewForbidden(a, fmt.Errorf("only dynamically provisioned pvc can be resized and "+
"the storageclass that provisions the pvc must support resize"))
}
// volume plugin must support resize
pv, err := pvcr.pvLister.Get(pvc.Spec.VolumeName)
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("Error updating persistent volume claim because fetching associated persistent volume failed"))
}
if !pvcr.checkVolumePlugin(pv) {
return admission.NewForbidden(a, fmt.Errorf("volume plugin does not support resize"))
}
return nil
}
// Growing Persistent volumes is only allowed for PVCs for which their StorageClass
// explicitly allows it.
func (pvcr *persistentVolumeClaimResize) allowResize(pvc, oldPvc *api.PersistentVolumeClaim) bool {
pvcStorageClass := apihelper.GetPersistentVolumeClaimClass(pvc)
oldPvcStorageClass := apihelper.GetPersistentVolumeClaimClass(oldPvc)
if pvcStorageClass == "" || oldPvcStorageClass == "" || pvcStorageClass != oldPvcStorageClass {
return false
}
sc, err := pvcr.scLister.Get(pvcStorageClass)
if err != nil {
return false
}
if sc.AllowVolumeExpansion != nil {
return *sc.AllowVolumeExpansion
}
return false
}
// 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 {
return true
}
if pv.Spec.GCEPersistentDisk != nil {
return true
}
if pv.Spec.AWSElasticBlockStore != nil {
return true
}
return false
}

Some files were not shown because too many files have changed in this diff Show More