-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Tomek Urbaszek <[email protected]>
- Loading branch information
Showing
5 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package scalers | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
kedautil "github.com/kedacore/keda/pkg/util" | ||
"github.com/tidwall/gjson" | ||
"io/ioutil" | ||
"k8s.io/api/autoscaling/v2beta2" | ||
"k8s.io/apimachinery/pkg/api/resource" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/labels" | ||
"k8s.io/metrics/pkg/apis/external_metrics" | ||
"net/http" | ||
logf "sigs.k8s.io/controller-runtime/pkg/log" | ||
"strconv" | ||
) | ||
|
||
type metricsAPIScaler struct { | ||
metadata *metricsAPIScalerMetadata | ||
} | ||
|
||
type metricsAPIScalerMetadata struct { | ||
targetValue int | ||
url string | ||
valueLocation string | ||
} | ||
|
||
var httpLog = logf.Log.WithName("metrics_api_scaler") | ||
|
||
// NewMetricsAPIScaler creates a new HTTP scaler | ||
func NewMetricsAPIScaler(resolvedEnv, metadata, authParams map[string]string) (Scaler, error) { | ||
meta, err := metricsAPIMetadata(resolvedEnv, metadata, authParams) | ||
if err != nil { | ||
return nil, fmt.Errorf("error parsing metric API metadata: %s", err) | ||
} | ||
return &metricsAPIScaler{metadata: meta}, nil | ||
} | ||
|
||
func metricsAPIMetadata(resolvedEnv, metadata, authParams map[string]string) (*metricsAPIScalerMetadata, error) { | ||
meta := metricsAPIScalerMetadata{} | ||
|
||
if val, ok := metadata["targetValue"]; ok { | ||
targetValue, err := strconv.Atoi(val) | ||
if err != nil { | ||
return nil, fmt.Errorf("targetValue parsing error %s", err.Error()) | ||
} | ||
meta.targetValue = targetValue | ||
} else { | ||
return nil, fmt.Errorf("no targetValue given in metadata") | ||
} | ||
|
||
if val, ok := metadata["url"]; ok { | ||
meta.url = val | ||
} else { | ||
return nil, fmt.Errorf("no url given in metadata") | ||
} | ||
|
||
if val, ok := metadata["valueLocation"]; ok { | ||
meta.valueLocation = val | ||
} else { | ||
return nil, fmt.Errorf("no valueLocation given in metadata") | ||
} | ||
|
||
return &meta, nil | ||
} | ||
|
||
// GetValueFromResponse uses provided valueLocation to access the numeric value in provided body | ||
func GetValueFromResponse(body []byte, valueLocation string) (int64, error) { | ||
r := gjson.GetBytes(body, valueLocation) | ||
if r.Type != gjson.Number { | ||
msg := fmt.Sprintf("valueLocation must point to value of type number got: %s", r.Type.String()) | ||
return 0, errors.New(msg) | ||
} | ||
return int64(r.Num), nil | ||
} | ||
|
||
func (s *metricsAPIScaler) getMetricValue() (int64, error) { | ||
r, err := http.Get(s.metadata.url) | ||
if err != nil { | ||
return 0, err | ||
} | ||
defer r.Body.Close() | ||
b, err := ioutil.ReadAll(r.Body) | ||
if err != nil { | ||
return 0, err | ||
} | ||
v, err := GetValueFromResponse(b, s.metadata.valueLocation) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return v, nil | ||
} | ||
|
||
// Close does nothing in case of metricsAPIScaler | ||
func (s *metricsAPIScaler) Close() error { | ||
return nil | ||
} | ||
|
||
// IsActive returns true if there are pending messages to be processed | ||
func (s *metricsAPIScaler) IsActive(ctx context.Context) (bool, error) { | ||
v, err := s.getMetricValue() | ||
if err != nil { | ||
httpLog.Error(err, fmt.Sprintf("Error when checking metric value: %s", err)) | ||
return false, err | ||
} | ||
|
||
return v > 0.0, nil | ||
} | ||
|
||
// GetMetricSpecForScaling returns the MetricSpec for the Horizontal Pod Autoscaler | ||
func (s *metricsAPIScaler) GetMetricSpecForScaling() []v2beta2.MetricSpec { | ||
targetValue := resource.NewQuantity(int64(s.metadata.targetValue), resource.DecimalSI) | ||
metricName := fmt.Sprintf("%s-%s-%s", "http", kedautil.NormalizeString(s.metadata.url), s.metadata.valueLocation) | ||
externalMetric := &v2beta2.ExternalMetricSource{ | ||
Metric: v2beta2.MetricIdentifier{ | ||
Name: metricName, | ||
}, | ||
Target: v2beta2.MetricTarget{ | ||
Type: v2beta2.AverageValueMetricType, | ||
AverageValue: targetValue, | ||
}, | ||
} | ||
metricSpec := v2beta2.MetricSpec{ | ||
External: externalMetric, Type: externalMetricType, | ||
} | ||
return []v2beta2.MetricSpec{metricSpec} | ||
} | ||
|
||
// GetMetrics returns value for a supported metric and an error if there is a problem getting the metric | ||
func (s *metricsAPIScaler) GetMetrics(ctx context.Context, metricName string, metricSelector labels.Selector) ([]external_metrics.ExternalMetricValue, error) { | ||
v, err := s.getMetricValue() | ||
if err != nil { | ||
return []external_metrics.ExternalMetricValue{}, fmt.Errorf("error requesting metrics endpoint: %s", err) | ||
} | ||
|
||
metric := external_metrics.ExternalMetricValue{ | ||
MetricName: metricName, | ||
Value: *resource.NewQuantity(v, resource.DecimalSI), | ||
Timestamp: metav1.Now(), | ||
} | ||
|
||
return append([]external_metrics.ExternalMetricValue{}, metric), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package scalers | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
var metricsAPIResolvedEnv = map[string]string{} | ||
var authParams = map[string]string{} | ||
|
||
type metricsAPIMetadataTestData struct { | ||
metadata map[string]string | ||
raisesError bool | ||
} | ||
|
||
var testMetricsAPIMetadata = []metricsAPIMetadataTestData{ | ||
// No metadata | ||
{metadata: map[string]string{}, raisesError: true}, | ||
// OK | ||
{metadata: map[string]string{"url": "http://dummy:1230/api/v1/", "valueLocation": "metric", "targetValue": "42"}, raisesError: false}, | ||
// Target not an int | ||
{metadata: map[string]string{"url": "http://dummy:1230/api/v1/", "valueLocation": "metric", "targetValue": "aa"}, raisesError: true}, | ||
// Missing metric name | ||
{metadata: map[string]string{"url": "http://dummy:1230/api/v1/", "targetValue": "aa"}, raisesError: true}, | ||
// Missing url | ||
{metadata: map[string]string{"valueLocation": "metric", "targetValue": "aa"}, raisesError: true}, | ||
// Missing targetValue | ||
{metadata: map[string]string{"url": "http://dummy:1230/api/v1/", "valueLocation": "metric"}, raisesError: true}, | ||
} | ||
|
||
func TestParseMetricsAPIMetadata(t *testing.T) { | ||
for _, testData := range testMetricsAPIMetadata { | ||
_, err := metricsAPIMetadata(metricsAPIResolvedEnv, testData.metadata, authParams) | ||
if err != nil && !testData.raisesError { | ||
t.Error("Expected success but got error", err) | ||
} | ||
if err == nil && testData.raisesError { | ||
t.Error("Expected error but got success") | ||
} | ||
} | ||
} | ||
|
||
func TestGetValueFromResponse(t *testing.T) { | ||
d := []byte(`{"components":[{"id": "82328e93e", "tasks": 32}],"count":2.43}`) | ||
v, err := GetValueFromResponse(d, "components.0.tasks") | ||
if err != nil { | ||
t.Error("Expected success but got error", err) | ||
} | ||
if v != 32 { | ||
t.Errorf("Expected %d got %d", 32, v) | ||
} | ||
|
||
v, err = GetValueFromResponse(d, "count") | ||
if err != nil { | ||
t.Error("Expected success but got error", err) | ||
} | ||
if v != 2 { | ||
t.Errorf("Expected %d got %d", 2, v) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters