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

feat(publicApi): Allow CRUD Operations for Custom Metrics Strategy via Public APIs #3317

Merged
merged 6 commits into from
Dec 4, 2024
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
4 changes: 2 additions & 2 deletions api/application-metric-api.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.0.0
info:
title: Application Metric API
description: |
List aggregated metrics of an application. AutoScaler collects the instances' metrics of an
List aggregated metrics of an application. AutoScaler collects the instances metrics of an
application, and aggregate the raw data into an accumulated value for evaluation.

This API is used to return the aggregated metric result of an application.
Expand All @@ -22,7 +22,7 @@ paths:
in: path
required: true
description: |
The GUID identifying the application for which the scaling history is fetched.
The GUID identifying the application for which the aggregated metric histories is fetched.

It can be found in the `application_id` property of the JSON object stored in the
`VCAP_APPLICATION` environment variable.
Expand Down
26 changes: 23 additions & 3 deletions api/policy-api.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ paths:
in: path
required: true
description: |
The GUID identifying the application for which the scaling history is fetched.
The GUID identifying the application for which the scaling policy is fetched.

It can be found in the `application_id` property of the JSON object stored in the
`VCAP_APPLICATION` environment variable.
schema:
$ref: "./shared_definitions.yaml#/schemas/GUID"
put:
summary: Retrieves the Policy
summary: Create the Policy
description: This API is used to create the policy
tags:
- Create Policy API V1
Expand Down Expand Up @@ -78,8 +78,11 @@ paths:
components:
schemas:
Policy:
description: Object containing Policy
description: Object containing policy and optional configuration
type: object
required:
- instance_min_count
- instance_max_count
properties:
instance_min_count:
type: integer
Expand All @@ -95,6 +98,23 @@ components:
type: array
items:
$ref: '#/components/schemas/ScalingRule'
configuration:
type: object
properties:
custom_metrics:
type: object
properties:
metric_submission_strategy:
type: object
properties:
allow_from:
type: string
enum:
- bound_app
required:
- allow_from
required:
- metric_submission_strategy
ScalingRule:
type: object
required:
Expand Down
88 changes: 88 additions & 0 deletions docs/public-api.restclient
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
######
# Collection of rest endpoints offered by Application Autoscaler public api server
# These endpoints allows CRUD operations for the policy object
# This endpoints works with vs code rest client extension
########

@system_domain = app-runtime-interfaces.ci.cloudfoundry.org
@baseUrl = https://autoscaler.{{system_domain}}
@auth_token = bearer <cf oauth-token>

### app guid
@go_app_guid = 3e4d6cd4-08a3-4289-b09e-16333e1895c1

### check server health
GET {{baseUrl}}/health


### show current app policy
GET {{baseUrl}}/v1/apps/{{go_app_guid}}/policy
Authorization: {{auth_token}}

### create a policy of a given app with configuration object
PUT {{baseUrl}}/v1/apps/{{consumer_guid}}/policy
content-type: application/json
Authorization: {{auth_token}}

{
"configuration": {
"custom_metrics": {
"metric_submission_strategy": {
"allow_from": "bound_app"
}
}
},
"instance_min_count": 1,
"instance_max_count": 4,
"scaling_rules": [
{
"metric_type": "test_metric",
"breach_duration_secs": 60,
"threshold": 200,
"operator": ">=",
"cool_down_secs": 60,
"adjustment": "+1"
},
{
"metric_type": "test_metric",
"breach_duration_secs": 60,
"threshold": 100,
"operator": "<=",
"cool_down_secs": 60,
"adjustment": "-1"
}
]
}

### update a policy of a given app - wihout configuration
PUT {{baseUrl}}/v1/apps/{{go_app_guid}}/policy
content-type: application/json
Authorization: {{auth_token}}

{
"instance_min_count": 1,
"instance_max_count": 4,
"scaling_rules": [
{
"metric_type": "test_metric",
"breach_duration_secs": 60,
"threshold": 200,
"operator": ">=",
"cool_down_secs": 60,
"adjustment": "+1"
},
{
"metric_type": "test_metric",
"breach_duration_secs": 60,
"threshold": 100,
"operator": "<=",
"cool_down_secs": 60,
"adjustment": "-1"
}
]
}

### Delete a policy of an app
DELETE {{baseUrl}}/v1/apps/{{go_app_guid}}/policy
Authorization: {{auth_token}}

68 changes: 66 additions & 2 deletions src/acceptance/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,30 @@ var _ = Describe("AutoScaler Public API", func() {
Expect(string(response)).Should(ContainSubstring(`[{"context":"(root).instance_min_count","description":"Must be greater than or equal to 1"}]`))
})

It("should fail to create an invalid custom metrics submission", func() {
By("creating custom metrics submission with invalid string")
response, status := createPolicy(GenerateBindingsWithScalingPolicy("invalid-value", 1, 2, "memoryused", 30, 100))
Expect(string(response)).Should(MatchJSON(`[{"context":"(root).configuration.custom_metrics.metric_submission_strategy.allow_from","description":"configuration.custom_metrics.metric_submission_strategy.allow_from must be one of the following: \"bound_app\""}]`))
Expect(status).To(Equal(400))

By("creating custom metrics submission with empty value ' '")
policy := GenerateBindingsWithScalingPolicy("", 1, 2, "memoryused", 30, 100)
newPolicy, status := createPolicy(policy)
Expect(string(newPolicy)).Should(MatchJSON(`[{"context":"(root).configuration.custom_metrics.metric_submission_strategy.allow_from","description":"configuration.custom_metrics.metric_submission_strategy.allow_from must be one of the following: \"bound_app\""}]`))
Expect(status).To(Equal(400))
})

It("should succeed to create an valid custom metrics submission", func() {
By("creating custom metrics submission with 'bound_app'")
policy := GenerateBindingsWithScalingPolicy("bound_app", 1, 2, "memoryused", 30, 100)
response, status := createPolicy(policy)
Expect(string(response)).Should(MatchJSON(policy))
Expect(status).To(Or(Equal(200), Equal(201)))
})

})

When("a scaling policy is set", func() {
When("a scaling policy is set without custom metric strategy", func() {
memThreshold := int64(10)
var policy string

Expand Down Expand Up @@ -204,7 +225,6 @@ var _ = Describe("AutoScaler Public API", func() {
BeforeEach(func() {
UnbindServiceFromApp(cfg, appName, instanceName)
})

It("should not be possible to get information from the API", func() {
By("getting the policy")
_, status := getPolicy()
Expand All @@ -219,7 +239,51 @@ var _ = Describe("AutoScaler Public API", func() {
Expect(status).To(Equal(http.StatusForbidden))
})
})
})

When("a scaling policy is set with custom metric strategy", func() {
var status int
var expectedPolicy string
var actualPolicy []byte
BeforeEach(func() {
BindServiceToApp(cfg, appName, instanceName)
expectedPolicy = GenerateBindingsWithScalingPolicy("bound_app", 1, 2, "memoryused", 30, 100)
actualPolicy, status = createPolicy(expectedPolicy)
Expect(status).To(Equal(200))
})
It("should succeed to delete a custom metrics strategy", func() {
_, status = deletePolicy()
Expect(status).To(Equal(200))
})
It("should succeed to get a custom metrics strategy", func() {
actualPolicy, status = getPolicy()
Expect(string(actualPolicy)).Should(MatchJSON(expectedPolicy))
Expect(status).To(Equal(200))
})
It("should fail to update an invalid custom metrics strategy", func() {
expectedPolicy = GenerateBindingsWithScalingPolicy("invalid-update", 1, 2, "memoryused", 30, 100)
actualPolicy, status = createPolicy(expectedPolicy)
Expect(string(actualPolicy)).Should(MatchJSON(`[{"context":"(root).configuration.custom_metrics.metric_submission_strategy.allow_from","description":"configuration.custom_metrics.metric_submission_strategy.allow_from must be one of the following: \"bound_app\""}]`))
Expect(status).To(Equal(400))
})
It("should succeed to update a valid custom metrics strategy", func() {

Expect(string(actualPolicy)).Should(MatchJSON(expectedPolicy))
Expect(status).To(Equal(200))
})

When("custom metrics strategy is removed from the existing policy", func() {
It("should removed the custom metrics strategy and displays policy only", func() {
Expect(string(actualPolicy)).Should(MatchJSON(expectedPolicy), "policy and custom metrics strategy should be present")
Expect(status).To(Equal(200))

By("updating policy without custom metrics strategy")
expectedPolicy = GenerateDynamicScaleOutPolicy(1, 2, "memoryused", 30)
actualPolicy, status = createPolicy(expectedPolicy)
Expect(string(actualPolicy)).Should(MatchJSON(expectedPolicy), "policy should be present only")
Expect(status).To(Equal(200))
})
})
})
})

Expand Down
15 changes: 9 additions & 6 deletions src/acceptance/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,26 @@ type BindingConfig struct {
Configuration Configuration `json:"configuration"`
ScalingPolicy
}

type Configuration struct {
CustomMetrics CustomMetricsConfig `json:"custom_metrics"`
}

type CustomMetricsConfig struct {
Auth Auth `json:"auth,omitempty"`
MetricSubmissionStrategy MetricsSubmissionStrategy `json:"metric_submission_strategy"`
}

type Auth struct {
CredentialType string `json:"credential_type"`
}
type MetricsSubmissionStrategy struct {
AllowFrom string `json:"allow_from"`
}

func (b *BindingConfig) GetCustomMetricsStrategy() string {
return b.Configuration.CustomMetrics.MetricSubmissionStrategy.AllowFrom
}

func (b *BindingConfig) SetCustomMetricsStrategy(allowFrom string) {
b.Configuration.CustomMetrics.MetricSubmissionStrategy.AllowFrom = allowFrom
}

type ScalingPolicy struct {
InstanceMin int `json:"instance_min_count"`
InstanceMax int `json:"instance_max_count"`
Expand Down Expand Up @@ -184,7 +187,7 @@ func ServicePlansUrl(cfg *config.Config, spaceGuid string) string {
}

func GenerateBindingsWithScalingPolicy(allowFrom string, instanceMin, instanceMax int, metricName string, scaleInThreshold, scaleOutThreshold int64) string {
bindingConfig := BindingConfig{
bindingConfig := &BindingConfig{
Configuration: Configuration{CustomMetrics: CustomMetricsConfig{
MetricSubmissionStrategy: MetricsSubmissionStrategy{AllowFrom: allowFrom},
}},
Expand Down
57 changes: 15 additions & 42 deletions src/autoscaler/api/broker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (b *Broker) getPolicyFromJsonRawMessage(policyJson json.RawMessage, instanc
}

func (b *Broker) validateAndCheckPolicy(rawJson json.RawMessage, instanceID string, planID string) (*models.ScalingPolicy, error) {
policy, errResults := b.policyValidator.ValidatePolicy(rawJson)
policy, errResults := b.policyValidator.ParseAndValidatePolicy(rawJson)
logger := b.logger.Session("validate-and-check-policy", lager.Data{"instanceID": instanceID, "policy": policy, "planID": planID, "errResults": errResults})

if errResults != nil {
Expand Down Expand Up @@ -503,16 +503,19 @@ func (b *Broker) Bind(ctx context.Context, instanceID string, bindingID string,
logger.Error("get-binding-configuration-from-request", err)
return result, err
}
bindingConfiguration, err = b.validateOrGetDefaultCustomMetricsStrategy(bindingConfiguration, logger)
if err != nil {
logger.Error("validate-or-get-default-custom-metric-strategy", err)
return result, err
}
policy, err := b.getPolicyFromJsonRawMessage(policyJson, instanceID, details.PlanID)
if err != nil {
logger.Error("get-default-policy", err)
return result, err
}
bindingConfiguration, err = bindingConfiguration.ValidateOrGetDefaultCustomMetricsStrategy()
if err != nil {
actionName := "validate-or-get-default-custom-metric-strategy"
logger.Error(actionName, err)
return result, apiresponses.NewFailureResponseBuilder(
err, http.StatusBadRequest, actionName).
WithErrorKey(actionName).Build()
}
defaultCustomMetricsCredentialType := b.conf.DefaultCustomMetricsCredentialType
customMetricsBindingAuthScheme, err := getOrDefaultCredentialType(policyJson, defaultCustomMetricsCredentialType, logger)
if err != nil {
Expand Down Expand Up @@ -593,21 +596,6 @@ func (b *Broker) Bind(ctx context.Context, instanceID string, bindingID string,
return result, nil
}

func (b *Broker) validateOrGetDefaultCustomMetricsStrategy(bindingConfiguration *models.BindingConfig, logger lager.Logger) (*models.BindingConfig, error) {
strategy := bindingConfiguration.GetCustomMetricsStrategy()
if strategy == "" {
bindingConfiguration.SetCustomMetricsStrategy(models.CustomMetricsSameApp)
} else if strategy != models.CustomMetricsBoundApp {
actionName := "verify-custom-metrics-strategy"
return bindingConfiguration, apiresponses.NewFailureResponseBuilder(
ErrInvalidCustomMetricsStrategy, http.StatusBadRequest, actionName).
WithErrorKey(actionName).
Build()
}
logger.Info("binding-configuration", lager.Data{"bindingConfiguration": bindingConfiguration})
return bindingConfiguration, nil
}

func (b *Broker) getBindingConfigurationFromRequest(policyJson json.RawMessage, logger lager.Logger) (*models.BindingConfig, error) {
bindingConfiguration := &models.BindingConfig{}
var err error
Expand Down Expand Up @@ -739,31 +727,16 @@ func (b *Broker) GetBinding(ctx context.Context, instanceID string, bindingID st
b.logger.Error("get-binding", err, lager.Data{"instanceID": instanceID, "bindingID": bindingID, "fetchBindingDetails": details})
return domain.GetBindingSpec{}, apiresponses.NewFailureResponse(errors.New("failed to retrieve scaling policy"), http.StatusInternalServerError, "get-policy")
}
combinedConfig, bindingConfig := b.buildConfigurationIfPresent(serviceBinding.CustomMetricsStrategy)
if policy != nil && combinedConfig != nil { //both are present
combinedConfig.ScalingPolicy = *policy
combinedConfig.BindingConfig = *bindingConfig
result.Parameters = combinedConfig
} else if policy != nil { // only policy was given
result.Parameters = policy
} else if combinedConfig != nil { // only configuration was given
result.Parameters = bindingConfig
if policy != nil {
policyAndBinding, err := models.GetBindingConfigAndPolicy(policy, serviceBinding.CustomMetricsStrategy)
if err != nil {
return domain.GetBindingSpec{}, apiresponses.NewFailureResponse(errors.New("failed to build policy and config object"), http.StatusInternalServerError, "determine-BindingConfig-and-Policy")
}
result.Parameters = policyAndBinding
}
return result, nil
}

func (b *Broker) buildConfigurationIfPresent(customMetricsStrategy string) (*models.BindingConfigWithScaling, *models.BindingConfig) {
var combinedConfig *models.BindingConfigWithScaling
var bindingConfig *models.BindingConfig

if customMetricsStrategy != "" && customMetricsStrategy != models.CustomMetricsSameApp { //if custom metric was given in the binding process
combinedConfig = &models.BindingConfigWithScaling{}
bindingConfig = &models.BindingConfig{}
bindingConfig.SetCustomMetricsStrategy(customMetricsStrategy)
combinedConfig.BindingConfig = *bindingConfig
}
return combinedConfig, bindingConfig
}
func (b *Broker) getServiceBinding(ctx context.Context, bindingID string) (*models.ServiceBinding, error) {
logger := b.logger.Session("get-service-binding", lager.Data{"bindingID": bindingID})

Expand Down
Loading
Loading