-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow specification of multiple expanders #4233
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
Copyright 2021 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 factory | ||
|
||
import ( | ||
"k8s.io/autoscaler/cluster-autoscaler/expander" | ||
|
||
schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework" | ||
) | ||
|
||
type chainStrategy struct { | ||
filters []expander.Filter | ||
fallback expander.Strategy | ||
} | ||
|
||
func newChainStrategy(filters []expander.Filter, fallback expander.Strategy) expander.Strategy { | ||
return &chainStrategy{ | ||
filters: filters, | ||
fallback: fallback, | ||
} | ||
} | ||
|
||
func (c *chainStrategy) BestOption(options []expander.Option, nodeInfo map[string]*schedulerframework.NodeInfo) *expander.Option { | ||
filteredOptions := options | ||
for _, filter := range c.filters { | ||
filteredOptions = filter.BestOptions(filteredOptions, nodeInfo) | ||
if len(filteredOptions) == 1 { | ||
return &filteredOptions[0] | ||
} | ||
} | ||
return c.fallback.BestOption(filteredOptions, nodeInfo) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/* | ||
Copyright 2021 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 factory | ||
|
||
import ( | ||
"k8s.io/autoscaler/cluster-autoscaler/expander" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework" | ||
) | ||
|
||
type substringTestFilterStrategy struct { | ||
substring string | ||
} | ||
|
||
func newSubstringTestFilterStrategy(substring string) *substringTestFilterStrategy { | ||
return &substringTestFilterStrategy{ | ||
substring: substring, | ||
} | ||
} | ||
|
||
func (s *substringTestFilterStrategy) BestOptions(expansionOptions []expander.Option, nodeInfo map[string]*schedulerframework.NodeInfo) []expander.Option { | ||
var ret []expander.Option | ||
for _, option := range expansionOptions { | ||
if strings.Contains(option.Debug, s.substring) { | ||
ret = append(ret, option) | ||
} | ||
} | ||
return ret | ||
|
||
} | ||
|
||
func (s *substringTestFilterStrategy) BestOption(expansionOptions []expander.Option, nodeInfo map[string]*schedulerframework.NodeInfo) *expander.Option { | ||
ret := s.BestOptions(expansionOptions, nodeInfo) | ||
if len(ret) == 0 { | ||
return nil | ||
} | ||
return &ret[0] | ||
} | ||
|
||
func TestChainStrategy_BestOption(t *testing.T) { | ||
for name, tc := range map[string]struct { | ||
filters []expander.Filter | ||
fallback expander.Strategy | ||
options []expander.Option | ||
expected *expander.Option | ||
}{ | ||
"selects with no filters": { | ||
filters: []expander.Filter{}, | ||
fallback: newSubstringTestFilterStrategy("a"), | ||
options: []expander.Option{ | ||
*newOption("b"), | ||
*newOption("a"), | ||
}, | ||
expected: newOption("a"), | ||
}, | ||
"filters with one filter": { | ||
filters: []expander.Filter{ | ||
newSubstringTestFilterStrategy("a"), | ||
}, | ||
fallback: newSubstringTestFilterStrategy("b"), | ||
options: []expander.Option{ | ||
*newOption("ab"), | ||
*newOption("b"), | ||
}, | ||
expected: newOption("ab"), | ||
}, | ||
"filters with multiple filters": { | ||
filters: []expander.Filter{ | ||
newSubstringTestFilterStrategy("a"), | ||
newSubstringTestFilterStrategy("b"), | ||
}, | ||
fallback: newSubstringTestFilterStrategy("x"), | ||
options: []expander.Option{ | ||
*newOption("xab"), | ||
*newOption("xa"), | ||
*newOption("x"), | ||
}, | ||
expected: newOption("xab"), | ||
}, | ||
"selects from multiple after filters": { | ||
filters: []expander.Filter{ | ||
newSubstringTestFilterStrategy("x"), | ||
}, | ||
fallback: newSubstringTestFilterStrategy("a"), | ||
options: []expander.Option{ | ||
*newOption("xc"), | ||
*newOption("xaa"), | ||
*newOption("xab"), | ||
}, | ||
expected: newOption("xaa"), | ||
}, | ||
"short circuits": { | ||
filters: []expander.Filter{ | ||
newSubstringTestFilterStrategy("a"), | ||
newSubstringTestFilterStrategy("b"), | ||
}, | ||
fallback: newSubstringTestFilterStrategy("x"), | ||
options: []expander.Option{ | ||
*newOption("a"), | ||
}, | ||
expected: newOption("a"), | ||
}, | ||
} { | ||
t.Run(name, func(t *testing.T) { | ||
subject := newChainStrategy(tc.filters, tc.fallback) | ||
actual := subject.BestOption(tc.options, nil) | ||
assert.Equal(t, tc.expected, actual) | ||
}) | ||
} | ||
} | ||
|
||
func newOption(debug string) *expander.Option { | ||
return &expander.Option{ | ||
Debug: debug, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,30 +31,48 @@ import ( | |
kube_client "k8s.io/client-go/kubernetes" | ||
) | ||
|
||
// ExpanderStrategyFromString creates an expander.Strategy according to its name | ||
func ExpanderStrategyFromString(expanderFlag string, cloudProvider cloudprovider.CloudProvider, | ||
// ExpanderStrategyFromStrings creates an expander.Strategy according to the names of the expanders passed in | ||
func ExpanderStrategyFromStrings(expanderFlags []string, cloudProvider cloudprovider.CloudProvider, | ||
autoscalingKubeClients *context.AutoscalingKubeClients, kubeClient kube_client.Interface, | ||
configNamespace string) (expander.Strategy, errors.AutoscalerError) { | ||
switch expanderFlag { | ||
case expander.RandomExpanderName: | ||
return random.NewStrategy(), nil | ||
case expander.MostPodsExpanderName: | ||
return mostpods.NewStrategy(), nil | ||
case expander.LeastWasteExpanderName: | ||
return waste.NewStrategy(), nil | ||
case expander.PriceBasedExpanderName: | ||
if _, err := cloudProvider.Pricing(); err != nil { | ||
return nil, err | ||
var filters []expander.Filter | ||
seenExpanders := map[string]struct{}{} | ||
strategySeen := false | ||
for i, expanderFlag := range expanderFlags { | ||
if _, ok := seenExpanders[expanderFlag]; ok { | ||
return nil, errors.NewAutoscalerError(errors.InternalError, "Expander %s was specified multiple times, each expander must not be specified more than once", expanderFlag) | ||
} | ||
if strategySeen { | ||
return nil, errors.NewAutoscalerError(errors.InternalError, "Expander %s came after an expander %s that will always return only one result, this is not allowed since %s will never be used", expanderFlag, expanderFlags[i-1], expanderFlag) | ||
} | ||
seenExpanders[expanderFlag] = struct{}{} | ||
|
||
switch expanderFlag { | ||
case expander.RandomExpanderName: | ||
filters = append(filters, random.NewFilter()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be an error to specify an expander that's guaranteed to provide a single option with a fallback? E.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True that the configuration is not a reasonable one, but I felt that catching this case wasn't worth the effort as it would require disambiguating between expanders that
My thinking is that since it won't create problems then we can keep the simplicity of allowing this. Happy to be overruled on this though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's what it would look like: https://github.com/ryanmcnamara/autoscaler/compare/rm/expander-chain...ryanmcnamara:rm/expander-chain-demo?expand=1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, something like this. I'm not insisting on this one, but since you're generating an error for one class of invalid configs (duplicated entries), I thought it may make sense to go a step further and disallow bad ordering as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok addressed in this pr now! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neat, thanks! Maybe worth adding a comment about this behavior? May be surprising for anyone adding a new expander in the future. Please also squash commits before merging. |
||
case expander.MostPodsExpanderName: | ||
filters = append(filters, mostpods.NewFilter()) | ||
case expander.LeastWasteExpanderName: | ||
filters = append(filters, waste.NewFilter()) | ||
case expander.PriceBasedExpanderName: | ||
if _, err := cloudProvider.Pricing(); err != nil { | ||
return nil, err | ||
} | ||
filters = append(filters, price.NewFilter(cloudProvider, | ||
price.NewSimplePreferredNodeProvider(autoscalingKubeClients.AllNodeLister()), | ||
price.SimpleNodeUnfitness)) | ||
case expander.PriorityBasedExpanderName: | ||
// It seems other listers do the same here - they never receive the termination msg on the ch. | ||
// This should be currently OK. | ||
stopChannel := make(chan struct{}) | ||
lister := kubernetes.NewConfigMapListerForNamespace(kubeClient, stopChannel, configNamespace) | ||
filters = append(filters, priority.NewFilter(lister.ConfigMaps(configNamespace), autoscalingKubeClients.Recorder)) | ||
default: | ||
return nil, errors.NewAutoscalerError(errors.InternalError, "Expander %s not supported", expanderFlag) | ||
} | ||
if _, ok := filters[len(filters)-1].(expander.Strategy); ok { | ||
strategySeen = true | ||
} | ||
return price.NewStrategy(cloudProvider, | ||
price.NewSimplePreferredNodeProvider(autoscalingKubeClients.AllNodeLister()), | ||
price.SimpleNodeUnfitness), nil | ||
case expander.PriorityBasedExpanderName: | ||
// It seems other listers do the same here - they never receive the termination msg on the ch. | ||
// This should be currently OK. | ||
stopChannel := make(chan struct{}) | ||
lister := kubernetes.NewConfigMapListerForNamespace(kubeClient, stopChannel, configNamespace) | ||
return priority.NewStrategy(lister.ConfigMaps(configNamespace), autoscalingKubeClients.Recorder) | ||
} | ||
return nil, errors.NewAutoscalerError(errors.InternalError, "Expander %s not supported", expanderFlag) | ||
return newChainStrategy(filters, random.NewStrategy()), nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is non-trivial logic behind BestOptions call sometime, so maybe worth returning earlier if there's just one option left?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, will do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done