package clusterresourceoverride import ( "fmt" "io" oadmission "github.com/openshift/origin/pkg/cmd/server/admission" configlatest "github.com/openshift/origin/pkg/cmd/server/api/latest" "github.com/openshift/origin/pkg/project/cache" "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api" "github.com/openshift/origin/pkg/quota/admission/clusterresourceoverride/api/validation" "k8s.io/kubernetes/pkg/admission" kapi "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/plugin/pkg/admission/limitranger" "github.com/golang/glog" "speter.net/go/exp/math/dec/inf" ) const ( clusterResourceOverrideAnnotation = "quota.openshift.io/cluster-resource-override-enabled" cpuBaseScaleFactor = 1000.0 / (1024.0 * 1024.0 * 1024.0) // 1000 milliCores per 1GiB ) var ( zeroDec = inf.NewDec(0, 0) miDec = inf.NewDec(1024*1024, 0) cpuFloor = resource.MustParse("1m") memFloor = resource.MustParse("1Mi") ) func init() { admission.RegisterPlugin(api.PluginName, func(client clientset.Interface, config io.Reader) (admission.Interface, error) { return newClusterResourceOverride(client, config) }) } type internalConfig struct { limitCPUToMemoryRatio *inf.Dec cpuRequestToLimitRatio *inf.Dec memoryRequestToLimitRatio *inf.Dec } type clusterResourceOverridePlugin struct { *admission.Handler config *internalConfig ProjectCache *cache.ProjectCache LimitRanger admission.Interface } type limitRangerActions struct{} var _ = oadmission.WantsProjectCache(&clusterResourceOverridePlugin{}) var _ = oadmission.Validator(&clusterResourceOverridePlugin{}) var _ = limitranger.LimitRangerActions(&limitRangerActions{}) // newClusterResourceOverride returns an admission controller for containers that // configurably overrides container resource request/limits func newClusterResourceOverride(client clientset.Interface, config io.Reader) (admission.Interface, error) { parsed, err := ReadConfig(config) if err != nil { glog.V(5).Infof("%s admission controller loaded with error: (%T) %[2]v", api.PluginName, err) return nil, err } if errs := validation.Validate(parsed); len(errs) > 0 { return nil, errs.ToAggregate() } // we can't intercept and return a nil value or the upstream code will panic. Changing the admission initialization API is // too big for now. // TODO fix properly if parsed == nil { glog.V(2).Infof("%s admission controller has no configuration, return no-op", api.PluginName) return &clusterResourceOverridePlugin{ Handler: admission.NewHandler(), }, nil } glog.V(2).Infof("%s admission controller loaded with config: %v", api.PluginName, parsed) var internal *internalConfig if parsed != nil { internal = &internalConfig{ limitCPUToMemoryRatio: inf.NewDec(parsed.LimitCPUToMemoryPercent, 2), cpuRequestToLimitRatio: inf.NewDec(parsed.CPURequestToLimitPercent, 2), memoryRequestToLimitRatio: inf.NewDec(parsed.MemoryRequestToLimitPercent, 2), } } limitRanger, err := limitranger.NewLimitRanger(client, &limitRangerActions{}) if err != nil { return nil, err } return &clusterResourceOverridePlugin{ Handler: admission.NewHandler(admission.Create), config: internal, LimitRanger: limitRanger, }, nil } // these serve to satisfy the interface so that our kept LimitRanger limits nothing and only provides defaults. func (d *limitRangerActions) SupportsAttributes(a admission.Attributes) bool { return true } func (d *limitRangerActions) SupportsLimit(limitRange *kapi.LimitRange) bool { return true } func (d *limitRangerActions) Limit(limitRange *kapi.LimitRange, resourceName string, obj runtime.Object) error { return nil } func (a *clusterResourceOverridePlugin) SetProjectCache(projectCache *cache.ProjectCache) { a.ProjectCache = projectCache } func ReadConfig(configFile io.Reader) (*api.ClusterResourceOverrideConfig, error) { obj, err := configlatest.ReadYAML(configFile) if err != nil { glog.V(5).Infof("%s error reading config: %v", api.PluginName, err) return nil, err } if obj == nil { return nil, nil } config, ok := obj.(*api.ClusterResourceOverrideConfig) if !ok { return nil, fmt.Errorf("unexpected config object: %#v", obj) } glog.V(5).Infof("%s config is: %v", api.PluginName, config) return config, nil } func (a *clusterResourceOverridePlugin) Validate() error { if a.ProjectCache == nil { return fmt.Errorf("%s did not get a project cache", api.PluginName) } return nil } // TODO this will need to update when we have pod requests/limits func (a *clusterResourceOverridePlugin) Admit(attr admission.Attributes) error { glog.V(6).Infof("%s admission controller is invoked", api.PluginName) if a.config == nil || attr.GetResource().GroupResource() != kapi.Resource("pods") || attr.GetSubresource() != "" { return nil // not applicable } pod, ok := attr.GetObject().(*kapi.Pod) if !ok { return admission.NewForbidden(attr, fmt.Errorf("unexpected object: %#v", attr.GetObject())) } glog.V(5).Infof("%s is looking at creating pod %s in project %s", api.PluginName, pod.Name, attr.GetNamespace()) // allow annotations on project to override if ns, err := a.ProjectCache.GetNamespace(attr.GetNamespace()); err != nil { glog.Warningf("%s got an error retrieving namespace: %v", api.PluginName, err) return admission.NewForbidden(attr, err) // this should not happen though } else { projectEnabledPlugin, exists := ns.Annotations[clusterResourceOverrideAnnotation] if exists && projectEnabledPlugin != "true" { glog.V(5).Infof("%s is disabled for project %s", api.PluginName, attr.GetNamespace()) return nil // disabled for this project, do nothing } } // Reuse LimitRanger logic to apply limit/req defaults from the project. Ignore validation // errors, assume that LimitRanger will run after this plugin to validate. glog.V(5).Infof("%s: initial pod limits are: %#v", api.PluginName, pod.Spec.Containers[0].Resources) if err := a.LimitRanger.Admit(attr); err != nil { glog.V(5).Infof("%s: error from LimitRanger: %#v", api.PluginName, err) } glog.V(5).Infof("%s: pod limits after LimitRanger are: %#v", api.PluginName, pod.Spec.Containers[0].Resources) for _, container := range pod.Spec.Containers { resources := container.Resources memLimit, memFound := resources.Limits[kapi.ResourceMemory] if memFound && a.config.memoryRequestToLimitRatio.Cmp(zeroDec) != 0 { // memory is measured in whole bytes. // the plugin rounds down to the nearest MiB rather than bytes to improve ease of use for end-users. amount := multiply(memLimit.Amount, a.config.memoryRequestToLimitRatio) roundDownToNearestMi := multiply(divide(amount, miDec, 0, inf.RoundDown), miDec) value := resource.Quantity{Amount: roundDownToNearestMi, Format: resource.BinarySI} if memFloor.Cmp(value) > 0 { value = *(memFloor.Copy()) } resources.Requests[kapi.ResourceMemory] = value } if memFound && a.config.limitCPUToMemoryRatio.Cmp(zeroDec) != 0 { // float math is necessary here as there is no way to create an inf.Dec to represent cpuBaseScaleFactor < 0.001 // cpu is measured in millicores, so we need to scale and round down the value to nearest millicore scale amount := multiply(inf.NewDec(int64(float64(memLimit.Value())*cpuBaseScaleFactor), 3), a.config.limitCPUToMemoryRatio) amount.Round(amount, 3, inf.RoundDown) value := resource.Quantity{Amount: amount, Format: resource.DecimalSI} if cpuFloor.Cmp(value) > 0 { value = *(cpuFloor.Copy()) } resources.Limits[kapi.ResourceCPU] = value } cpuLimit, cpuFound := resources.Limits[kapi.ResourceCPU] if cpuFound && a.config.cpuRequestToLimitRatio.Cmp(zeroDec) != 0 { // cpu is measured in millicores, so we need to scale and round down the value to nearest millicore scale amount := multiply(cpuLimit.Amount, a.config.cpuRequestToLimitRatio) amount.Round(amount, 3, inf.RoundDown) value := resource.Quantity{Amount: amount, Format: resource.DecimalSI} if cpuFloor.Cmp(value) > 0 { value = *(cpuFloor.Copy()) } resources.Requests[kapi.ResourceCPU] = value } } glog.V(5).Infof("%s: pod limits after overrides are: %#v", api.PluginName, pod.Spec.Containers[0].Resources) return nil } func multiply(x *inf.Dec, y *inf.Dec) *inf.Dec { return inf.NewDec(0, 0).Mul(x, y) } func divide(x *inf.Dec, y *inf.Dec, s inf.Scale, r inf.Rounder) *inf.Dec { return inf.NewDec(0, 0).QuoRound(x, y, s, r) }