Skip to content
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

pass-through strategy plugin #433

Merged
merged 6 commits into from
Mar 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions plugins/builtin/strategy/pass-through/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/plugins"
passthrough "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/pass-through/plugin"
)

func main() {
plugins.Serve(factory)
}

// factory returns a new instance of the PassThrough Strategy plugin.
func factory(log hclog.Logger) interface{} {
return passthrough.NewPassThroughPlugin(log)
}
96 changes: 96 additions & 0 deletions plugins/builtin/strategy/pass-through/plugin/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package plugin

import (
"fmt"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/plugins"
"github.com/hashicorp/nomad-autoscaler/plugins/base"
"github.com/hashicorp/nomad-autoscaler/plugins/strategy"
"github.com/hashicorp/nomad-autoscaler/sdk"
)

const (
// pluginName is the unique name of the this plugin amongst strategy
// plugins.
pluginName = "pass-through"
)

var (
PluginID = plugins.PluginID{
Name: pluginName,
PluginType: sdk.PluginTypeStrategy,
}

PluginConfig = &plugins.InternalPluginConfig{
Factory: func(l hclog.Logger) interface{} { return NewPassThroughPlugin(l) },
}

pluginInfo = &base.PluginInfo{
Name: pluginName,
PluginType: sdk.PluginTypeStrategy,
}
)

// Assert that StrategyPlugin meets the strategy.Strategy interface.
var _ strategy.Strategy = (*StrategyPlugin)(nil)

// StrategyPlugin is the None implementation of the strategy.Strategy
// interface.
type StrategyPlugin struct {
logger hclog.Logger
}

// NewPassThroughPlugin returns the Pass Through implementation of the
// strategy.Strategy interface.
func NewPassThroughPlugin(log hclog.Logger) strategy.Strategy {
return &StrategyPlugin{
logger: log,
}
}

// SetConfig satisfies the SetConfig function on the base.Base interface.
func (s *StrategyPlugin) SetConfig(config map[string]string) error {
return nil
}

// PluginInfo satisfies the PluginInfo function on the base.Base interface.
func (s *StrategyPlugin) PluginInfo() (*base.PluginInfo, error) {
return pluginInfo, nil
}

// Run satisfies the Run function on the strategy.Strategy interface.
func (s *StrategyPlugin) Run(eval *sdk.ScalingCheckEvaluation, count int64) (*sdk.ScalingCheckEvaluation, error) {

// This shouldn't happen, but check it just in case.
if len(eval.Metrics) == 0 {
eval.Action.Direction = sdk.ScaleDirectionNone
return eval, nil
}

// Use only the latest value for now.
metric := eval.Metrics[len(eval.Metrics)-1]

// Identify the direction of scaling, if any.
eval.Action.Direction = s.calculateDirection(count, metric.Value)
if eval.Action.Direction == sdk.ScaleDirectionNone {
return eval, nil
}

eval.Action.Count = int64(metric.Value)
eval.Action.Reason = fmt.Sprintf("scaling %s because metric is %d", eval.Action.Direction, eval.Action.Count)

return eval, nil
}

// calculateDirection is used to calculate the direction of scaling that should
// occur, if any at all.
func (s *StrategyPlugin) calculateDirection(count int64, metric float64) sdk.ScaleDirection {
if metric > float64(count) {
return sdk.ScaleDirectionUp
} else if metric < float64(count) {
return sdk.ScaleDirectionDown
} else {
return sdk.ScaleDirectionNone
}
}
168 changes: 168 additions & 0 deletions plugins/builtin/strategy/pass-through/plugin/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package plugin

import (
"testing"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad-autoscaler/plugins/base"
"github.com/hashicorp/nomad-autoscaler/sdk"
"github.com/stretchr/testify/assert"
)

func TestStrategyPlugin_SetConfig(t *testing.T) {
s := &StrategyPlugin{}
expectedOutput := map[string]string{"example-item": "example-value"}
err := s.SetConfig(expectedOutput)
assert.Nil(t, err)
}

func TestStrategyPlugin_PluginInfo(t *testing.T) {
s := &StrategyPlugin{}
expectedOutput := &base.PluginInfo{Name: "pass-through", PluginType: "strategy"}
actualOutput, err := s.PluginInfo()
assert.Nil(t, err)
assert.Equal(t, expectedOutput, actualOutput)
}

func TestStrategyPlugin_Run(t *testing.T) {
testCases := []struct {
inputEval *sdk.ScalingCheckEvaluation
inputCount int64
expectedResp *sdk.ScalingCheckEvaluation
expectedError error
name string
}{
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 13}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 2,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 13}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{
Count: 13,
Direction: sdk.ScaleDirectionUp,
Reason: "scaling up because metric is 13",
},
},
expectedError: nil,
name: "pass-through scale up",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 0}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 2,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 0}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{
Count: 0,
Direction: sdk.ScaleDirectionDown,
Reason: "scaling down because metric is 0",
},
},
expectedError: nil,
name: "pass-through scale down to 0",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 10}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 10,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{sdk.TimestampedMetric{Value: 10}},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{
Direction: sdk.ScaleDirectionNone,
},
},
expectedError: nil,
name: "no scaling - current count same as metric",
},
{
inputEval: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{},
},
inputCount: 10,
expectedResp: &sdk.ScalingCheckEvaluation{
Metrics: sdk.TimestampedMetrics{},
Check: &sdk.ScalingPolicyCheck{
Strategy: &sdk.ScalingPolicyStrategy{
Config: map[string]string{},
},
},
Action: &sdk.ScalingAction{
Direction: sdk.ScaleDirectionNone,
},
},
expectedError: nil,
name: "no scaling - empty metric timeseries",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := &StrategyPlugin{logger: hclog.NewNullLogger()}
actualResp, actualError := s.Run(tc.inputEval, tc.inputCount)
assert.Equal(t, tc.expectedResp, actualResp, tc.name)
assert.Equal(t, tc.expectedError, actualError, tc.name)
})
}
}

func TestStrategyPlugin_calculateDirection(t *testing.T) {
testCases := []struct {
inputCount int64
inputMetric float64
threshold float64
expectedOutput sdk.ScaleDirection
}{
{inputCount: 0, inputMetric: 1, expectedOutput: sdk.ScaleDirectionUp},
{inputCount: 5, inputMetric: 5, expectedOutput: sdk.ScaleDirectionNone},
{inputCount: 4, inputMetric: 0, expectedOutput: sdk.ScaleDirectionDown},
}

s := &StrategyPlugin{}

for _, tc := range testCases {
assert.Equal(t, tc.expectedOutput, s.calculateDirection(tc.inputCount, tc.inputMetric))
}
}
5 changes: 5 additions & 0 deletions plugins/manager/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
datadog "github.com/hashicorp/nomad-autoscaler/plugins/builtin/apm/datadog/plugin"
nomadAPM "github.com/hashicorp/nomad-autoscaler/plugins/builtin/apm/nomad/plugin"
prometheus "github.com/hashicorp/nomad-autoscaler/plugins/builtin/apm/prometheus/plugin"
passthrough "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/pass-through/plugin"
targetValue "github.com/hashicorp/nomad-autoscaler/plugins/builtin/strategy/target-value/plugin"
awsASG "github.com/hashicorp/nomad-autoscaler/plugins/builtin/target/aws-asg/plugin"
azureVMSS "github.com/hashicorp/nomad-autoscaler/plugins/builtin/target/azure-vmss/plugin"
Expand All @@ -29,6 +30,9 @@ func (pm *PluginManager) loadInternalPlugin(cfg *config.Plugin, pluginType strin
case plugins.InternalTargetNomad:
info.factory = nomadTarget.PluginConfig.Factory
info.driver = "nomad-target"
case plugins.InternalStrategyPassThrough:
info.factory = passthrough.PluginConfig.Factory
info.driver = "pass-through"
case plugins.InternalStrategyTargetValue:
info.factory = targetValue.PluginConfig.Factory
info.driver = "target-value"
Expand Down Expand Up @@ -86,6 +90,7 @@ func (pm *PluginManager) useInternal(plugin string) bool {
case plugins.InternalAPMNomad,
plugins.InternalTargetNomad,
plugins.InternalAPMPrometheus,
plugins.InternalStrategyPassThrough,
plugins.InternalStrategyTargetValue,
plugins.InternalTargetAWSASG,
plugins.InternalTargetAzureVMSS,
Expand Down
3 changes: 3 additions & 0 deletions plugins/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const (
// InternalAPMPrometheus is the Prometheus APM internal plugin name.
InternalAPMPrometheus = "prometheus"

// InternalStrategyPassThrough is the Pass Through strategy internal plugin name.
InternalStrategyPassThrough = "pass-through"

// InternalStrategyTargetValue is the Target Value Strategy internal plugin
// name.
InternalStrategyTargetValue = "target-value"
Expand Down