Merge pull request #83849 from draveness/feature/node-locality-as-score-plugin

feat: implement imagelocality as a score plugin
This commit is contained in:
Kubernetes Prow Robot 2019-10-13 06:36:35 -07:00 committed by GitHub
commit dd5cb6426d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 405 additions and 27 deletions

View File

@ -171,7 +171,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
wantPrioritizers: sets.NewString(
"EqualPriority",
"NodeAffinityPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -185,6 +184,9 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "NodeResources"},
{Name: "VolumeRestrictions"},
},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
},
},
},
@ -235,7 +237,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -250,7 +251,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "VolumeRestrictions"},
{Name: "TaintToleration"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
},
@ -305,7 +309,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -322,7 +325,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "VolumeRestrictions"},
{Name: "TaintToleration"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
},
// Do not change this JSON after the corresponding release has been tagged.
@ -386,7 +392,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -403,7 +408,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "VolumeRestrictions"},
{Name: "TaintToleration"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -480,7 +488,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -497,7 +504,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "VolumeRestrictions"},
{Name: "TaintToleration"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -575,7 +585,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -593,7 +602,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -676,7 +688,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -694,7 +705,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -789,7 +803,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -808,7 +821,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -905,7 +921,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -924,7 +939,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -1021,7 +1039,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -1040,7 +1057,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -1141,7 +1161,6 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
),
wantPrioritizers: sets.NewString(
"EqualPriority",
"ImageLocalityPriority",
"LeastRequestedPriority",
"BalancedResourceAllocation",
"SelectorSpreadPriority",
@ -1160,7 +1179,10 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 2}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 2},
{Name: "TaintToleration", Weight: 2},
},
},
wantExtenders: []schedulerapi.ExtenderConfig{{
URLPrefix: "/prefix",
@ -1192,6 +1214,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
}
scoreToPriorityMap := map[string]string{
"TaintToleration": "TaintTolerationPriority",
"ImageLocality": "ImageLocalityPriority",
}
for v, tc := range schedulerFiles {
@ -1245,7 +1268,7 @@ func TestCompatibility_v1_Scheduler(t *testing.T) {
seenPredicates.Insert(filterToPredicateMap[p.Name])
}
for _, p := range gotPlugins["FilterPlugin"] {
for _, p := range gotPlugins["ScorePlugin"] {
seenPriorities.Insert(scoreToPriorityMap[p.Name])
}

View File

@ -10,6 +10,7 @@ go_library(
"//pkg/scheduler/algorithm/predicates:go_default_library",
"//pkg/scheduler/algorithm/priorities:go_default_library",
"//pkg/scheduler/apis/config:go_default_library",
"//pkg/scheduler/framework/plugins/imagelocality:go_default_library",
"//pkg/scheduler/framework/plugins/nodeaffinity:go_default_library",
"//pkg/scheduler/framework/plugins/nodename:go_default_library",
"//pkg/scheduler/framework/plugins/noderesources:go_default_library",
@ -37,6 +38,7 @@ filegroup(
srcs = [
":package-srcs",
"//pkg/scheduler/framework/plugins/examples:all-srcs",
"//pkg/scheduler/framework/plugins/imagelocality:all-srcs",
"//pkg/scheduler/framework/plugins/migration:all-srcs",
"//pkg/scheduler/framework/plugins/nodeaffinity:all-srcs",
"//pkg/scheduler/framework/plugins/nodename:all-srcs",

View File

@ -26,6 +26,7 @@ import (
"k8s.io/kubernetes/pkg/scheduler/algorithm/predicates"
"k8s.io/kubernetes/pkg/scheduler/algorithm/priorities"
"k8s.io/kubernetes/pkg/scheduler/apis/config"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/imagelocality"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodeaffinity"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/nodename"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/noderesources"
@ -56,6 +57,7 @@ type RegistryArgs struct {
// runs custom plugins, can pass a different Registry when initializing the scheduler.
func NewDefaultRegistry(args *RegistryArgs) framework.Registry {
return framework.Registry{
imagelocality.Name: imagelocality.New,
tainttoleration.Name: tainttoleration.New,
noderesources.Name: noderesources.New,
nodename.Name: nodename.New,
@ -128,6 +130,12 @@ func NewDefaultConfigProducerRegistry() *ConfigProducerRegistry {
return
})
registry.RegisterPriority(priorities.ImageLocalityPriority,
func(args ConfigProducerArgs) (plugins config.Plugins, pluginConfig []config.PluginConfig) {
plugins.Score = appendToPluginSet(plugins.Score, imagelocality.Name, &args.Weight)
return
})
return registry
}

View File

@ -0,0 +1,46 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["image_locality.go"],
importpath = "k8s.io/kubernetes/pkg/scheduler/framework/plugins/imagelocality",
visibility = ["//visibility:public"],
deps = [
"//pkg/scheduler/algorithm/priorities:go_default_library",
"//pkg/scheduler/framework/plugins/migration:go_default_library",
"//pkg/scheduler/framework/v1alpha1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["image_locality_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/scheduler/algorithm/priorities:go_default_library",
"//pkg/scheduler/framework/plugins/migration:go_default_library",
"//pkg/scheduler/framework/v1alpha1:go_default_library",
"//pkg/scheduler/nodeinfo:go_default_library",
"//pkg/scheduler/testing:go_default_library",
"//pkg/util/parsers:go_default_library",
"//staging/src/k8s.io/api/apps/v1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1: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,66 @@
/*
Copyright 2019 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 imagelocality
import (
"fmt"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/scheduler/algorithm/priorities"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/migration"
framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
)
var mb int64 = 1024 * 1024
// ImageLocality is a plugin that checks if a pod tolerates a node's taints.
type ImageLocality struct {
handle framework.FrameworkHandle
}
var _ = framework.ScorePlugin(&ImageLocality{})
// Name is the name of the plugin used in the plugin registry and configurations.
const Name = "ImageLocality"
// Name returns name of the plugin. It is used in logs, etc.
func (pl *ImageLocality) Name() string {
return Name
}
// Score invoked at the score extension point.
func (pl *ImageLocality) Score(state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
nodeInfo, exist := pl.handle.NodeInfoSnapshot().NodeInfoMap[nodeName]
if !exist {
return 0, framework.NewStatus(framework.Error, fmt.Sprintf("node %q does not exist in NodeInfoSnapshot", nodeName))
}
meta := migration.PriorityMetadata(state)
s, err := priorities.ImageLocalityPriorityMap(pod, meta, nodeInfo)
return s.Score, migration.ErrorToFrameworkStatus(err)
}
// ScoreExtensions of the Score plugin.
func (pl *ImageLocality) ScoreExtensions() framework.ScoreExtensions {
return nil
}
// New initializes a new plugin and returns it.
func New(_ *runtime.Unknown, h framework.FrameworkHandle) (framework.Plugin, error) {
return &ImageLocality{handle: h}, nil
}

View File

@ -0,0 +1,229 @@
/*
Copyright 2019 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 imagelocality
import (
"reflect"
"testing"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/scheduler/algorithm/priorities"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/migration"
framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
schedulernodeinfo "k8s.io/kubernetes/pkg/scheduler/nodeinfo"
schedulertesting "k8s.io/kubernetes/pkg/scheduler/testing"
"k8s.io/kubernetes/pkg/util/parsers"
)
func TestImageLocalityPriority(t *testing.T) {
test40250 := v1.PodSpec{
Containers: []v1.Container{
{
Image: "gcr.io/40",
},
{
Image: "gcr.io/250",
},
},
}
test40300 := v1.PodSpec{
Containers: []v1.Container{
{
Image: "gcr.io/40",
},
{
Image: "gcr.io/300",
},
},
}
testMinMax := v1.PodSpec{
Containers: []v1.Container{
{
Image: "gcr.io/10",
},
{
Image: "gcr.io/2000",
},
},
}
node403002000 := v1.NodeStatus{
Images: []v1.ContainerImage{
{
Names: []string{
"gcr.io/40:" + parsers.DefaultImageTag,
"gcr.io/40:v1",
"gcr.io/40:v1",
},
SizeBytes: int64(40 * mb),
},
{
Names: []string{
"gcr.io/300:" + parsers.DefaultImageTag,
"gcr.io/300:v1",
},
SizeBytes: int64(300 * mb),
},
{
Names: []string{
"gcr.io/2000:" + parsers.DefaultImageTag,
},
SizeBytes: int64(2000 * mb),
},
},
}
node25010 := v1.NodeStatus{
Images: []v1.ContainerImage{
{
Names: []string{
"gcr.io/250:" + parsers.DefaultImageTag,
},
SizeBytes: int64(250 * mb),
},
{
Names: []string{
"gcr.io/10:" + parsers.DefaultImageTag,
"gcr.io/10:v1",
},
SizeBytes: int64(10 * mb),
},
},
}
nodeWithNoImages := v1.NodeStatus{}
tests := []struct {
pod *v1.Pod
pods []*v1.Pod
nodes []*v1.Node
expectedList framework.NodeScoreList
name string
}{
{
// Pod: gcr.io/40 gcr.io/250
// Node1
// Image: gcr.io/40:latest 40MB
// Score: 0 (40M/2 < 23M, min-threshold)
// Node2
// Image: gcr.io/250:latest 250MB
// Score: 100 * (250M/2 - 23M)/(1000M - 23M) = 100
pod: &v1.Pod{Spec: test40250},
nodes: []*v1.Node{makeImageNode("machine1", node403002000), makeImageNode("machine2", node25010)},
expectedList: []framework.NodeScore{{Name: "machine1", Score: 0}, {Name: "machine2", Score: 10}},
name: "two images spread on two nodes, prefer the larger image one",
},
{
// Pod: gcr.io/40 gcr.io/300
// Node1
// Image: gcr.io/40:latest 40MB, gcr.io/300:latest 300MB
// Score: 100 * ((40M + 300M)/2 - 23M)/(1000M - 23M) = 15
// Node2
// Image: not present
// Score: 0
pod: &v1.Pod{Spec: test40300},
nodes: []*v1.Node{makeImageNode("machine1", node403002000), makeImageNode("machine2", node25010)},
expectedList: []framework.NodeScore{{Name: "machine1", Score: 15}, {Name: "machine2", Score: 0}},
name: "two images on one node, prefer this node",
},
{
// Pod: gcr.io/2000 gcr.io/10
// Node1
// Image: gcr.io/2000:latest 2000MB
// Score: 100 (2000M/2 >= 1000M, max-threshold)
// Node2
// Image: gcr.io/10:latest 10MB
// Score: 0 (10M/2 < 23M, min-threshold)
pod: &v1.Pod{Spec: testMinMax},
nodes: []*v1.Node{makeImageNode("machine1", node403002000), makeImageNode("machine2", node25010)},
expectedList: []framework.NodeScore{{Name: "machine1", Score: framework.MaxNodeScore}, {Name: "machine2", Score: 0}},
name: "if exceed limit, use limit",
},
{
// Pod: gcr.io/2000 gcr.io/10
// Node1
// Image: gcr.io/2000:latest 2000MB
// Score: 100 * (2000M/3 - 23M)/(1000M - 23M) = 65
// Node2
// Image: gcr.io/10:latest 10MB
// Score: 0 (10M/2 < 23M, min-threshold)
// Node3
// Image:
// Score: 0
pod: &v1.Pod{Spec: testMinMax},
nodes: []*v1.Node{makeImageNode("machine1", node403002000), makeImageNode("machine2", node25010), makeImageNode("machine3", nodeWithNoImages)},
expectedList: []framework.NodeScore{{Name: "machine1", Score: 65}, {Name: "machine2", Score: 0}, {Name: "machine3", Score: 0}},
name: "if exceed limit, use limit (with node which has no images present)",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
metaDataProducer := priorities.NewPriorityMetadataFactory(
schedulertesting.FakeServiceLister([]*v1.Service{}),
schedulertesting.FakeControllerLister([]*v1.ReplicationController{}),
schedulertesting.FakeReplicaSetLister([]*apps.ReplicaSet{}),
schedulertesting.FakeStatefulSetLister([]*apps.StatefulSet{}))
nodeNameToInfo := schedulernodeinfo.CreateNodeNameToInfoMap(nil, test.nodes)
meta := metaDataProducer(test.pod, nodeNameToInfo)
state := framework.NewCycleState()
state.Write(migration.PrioritiesStateKey, &migration.PrioritiesStateData{Reference: meta})
fh, _ := framework.NewFramework(nil, nil, nil)
snapshot := fh.NodeInfoSnapshot()
snapshot.NodeInfoMap = schedulernodeinfo.CreateNodeNameToInfoMap(nil, test.nodes)
p, _ := New(nil, fh)
var gotList framework.NodeScoreList
for _, n := range test.nodes {
nodeName := n.ObjectMeta.Name
score, status := p.(framework.ScorePlugin).Score(state, test.pod, nodeName)
if !status.IsSuccess() {
t.Errorf("unexpected error: %v", status)
}
gotList = append(gotList, framework.NodeScore{Name: nodeName, Score: score})
}
if !reflect.DeepEqual(test.expectedList, gotList) {
t.Errorf("expected:\n\t%+v,\ngot:\n\t%+v", test.expectedList, gotList)
}
})
}
}
func makeImageNode(node string, status v1.NodeStatus) *v1.Node {
return &v1.Node{
ObjectMeta: metav1.ObjectMeta{Name: node},
Status: status,
}
}

View File

@ -19,7 +19,7 @@ package tainttoleration
import (
"fmt"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/scheduler/algorithm/predicates"
"k8s.io/kubernetes/pkg/scheduler/algorithm/priorities"

View File

@ -144,7 +144,6 @@ func TestSchedulerCreationFromConfigMap(t *testing.T) {
"NodeAffinityPriority",
"NodePreferAvoidPodsPriority",
"SelectorSpreadPriority",
"ImageLocalityPriority",
),
expectedPlugins: map[string][]kubeschedulerconfig.Plugin{
"FilterPlugin": {
@ -152,7 +151,10 @@ func TestSchedulerCreationFromConfigMap(t *testing.T) {
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 1}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 1},
{Name: "TaintToleration", Weight: 1},
},
},
},
{
@ -213,7 +215,6 @@ kind: Policy
"NodeAffinityPriority",
"NodePreferAvoidPodsPriority",
"SelectorSpreadPriority",
"ImageLocalityPriority",
),
expectedPlugins: map[string][]kubeschedulerconfig.Plugin{
"FilterPlugin": {
@ -221,7 +222,10 @@ kind: Policy
{Name: "TaintToleration"},
{Name: "VolumeBinding"},
},
"ScorePlugin": {{Name: "TaintToleration", Weight: 1}},
"ScorePlugin": {
{Name: "ImageLocality", Weight: 1},
{Name: "TaintToleration", Weight: 1},
},
},
},
{