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: Reference AnalysisTemplates inside an AnalysisTemplate #3353

Merged
merged 10 commits into from
Mar 29, 2024
178 changes: 178 additions & 0 deletions docs/features/analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,184 @@ templates together. The controller combines the `metrics` and `args` fields of a
* Multiple metrics in the templates have the same name
* Two arguments with the same name have different default values no matter the argument value in Rollout

## Analysis Template referencing other Analysis Templates

AnalysisTemplates and ClusterAnalysisTemplates may reference other templates.

They can be combined with other metrics:

=== "AnalysisTemplate"

```yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: error-rate
spec:
args:
- name: service-name
metrics:
- name: error-rate
interval: 5m
successCondition: result[0] <= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: |
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code=~"5.*"}[5m]
)) /
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]
))
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: rates
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 5m
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: |
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code!~"5.*"}[5m]
)) /
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]
))
templates:
- templateName: error-rate
clusterScope: false
```

Or without additional metrics:

=== "AnalysisTemplate"

```yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 5m
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: |
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code!~"5.*"}[5m]
)) /
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]
))
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: error-rate
spec:
args:
- name: service-name
metrics:
- name: error-rate
interval: 5m
successCondition: result[0] <= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: |
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code=~"5.*"}[5m]
)) /
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]
))
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: rates
spec:
args:
- name: service-name
templates:
- templateName: success-rate
clusterScope: false
- templateName: error-rate
clusterScope: false
```

The result in the AnalysisRun will have the aggregation of metrics of each template:

=== "AnalysisRun"

```yaml
# NOTE: Generated AnalysisRun from a single template referencing several templates
apiVersion: argoproj.io/v1alpha1
kind: AnalysisRun
metadata:
name: guestbook-CurrentPodHash-templates-in-template
spec:
args:
- name: service-name
value: guestbook-svc.default.svc.cluster.local
metrics:
- name: success-rate
interval: 5m
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: |
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code!~"5.*"}[5m]
)) /
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]
))
- name: error-rate
interval: 5m
successCondition: result[0] <= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.example.com:9090
query: |
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code=~"5.*"}[5m]
)) /
sum(irate(
istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}"}[5m]
))
```

!!! note
The same limitations as for the multiple templates feature apply.
The controller will error when merging the templates if:

* Multiple metrics in the templates have the same name
* Two arguments with the same name have different default values no matter the argument value in Rollout

However, if the same AnalysisTemplate is referenced several times along the chain of references, the controller will only keep it once and discard the other references.

## Analysis Template Arguments

AnalysisTemplates may declare a set of arguments that can be passed by Rollouts. The args can then be used as in metrics configuration and are resolved at the time the AnalysisRun is created. Argument placeholders are defined as
Expand Down
38 changes: 32 additions & 6 deletions docs/features/kustomize/rollout_cr_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9250,11 +9250,24 @@
"type": "array",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"templates": {
"items": {
"properties": {
"clusterScope": {
"type": "boolean"
},
"templateName": {
"type": "string"
}
},
"type": "object"
},
"type": "array",
"x-kubernetes-patch-merge-key": "templateName",
"x-kubernetes-patch-strategy": "merge"
}
},
"required": [
"metrics"
],
"type": "object"
}
},
Expand Down Expand Up @@ -13883,11 +13896,24 @@
"type": "array",
"x-kubernetes-patch-merge-key": "name",
"x-kubernetes-patch-strategy": "merge"
},
"templates": {
"items": {
"properties": {
"clusterScope": {
"type": "boolean"
},
"templateName": {
"type": "string"
}
},
"type": "object"
},
"type": "array",
"x-kubernetes-patch-merge-key": "templateName",
"x-kubernetes-patch-strategy": "merge"
}
},
"required": [
"metrics"
],
"type": "object"
}
},
Expand Down
16 changes: 12 additions & 4 deletions experiments/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func generateRSName(ex *v1alpha1.Experiment, template v1alpha1.TemplateSpec) str
return fmt.Sprintf("%s-%s", ex.Name, template.Name)
}

func calculatePatch(ex *v1alpha1.Experiment, patch string, templates []v1alpha1.TemplateStatus, condition *v1alpha1.ExperimentCondition) string {
func calculatePatch(ex *v1alpha1.Experiment, patch string, templates []v1alpha1.TemplateStatus, condition *v1alpha1.ExperimentCondition, analysisRuns []*v1alpha1.ExperimentAnalysisRunStatus, message string) string {
patchMap := make(map[string]any)
err := json.Unmarshal([]byte(patch), &patchMap)
if err != nil {
Expand All @@ -318,6 +318,14 @@ func calculatePatch(ex *v1alpha1.Experiment, patch string, templates []v1alpha1.
newStatus["conditions"] = []v1alpha1.ExperimentCondition{*condition}
patchMap["status"] = newStatus
}
if analysisRuns != nil {
newStatus["analysisRuns"] = analysisRuns
patchMap["status"] = newStatus
}
if message != "" {
newStatus["message"] = message
patchMap["status"] = newStatus
}

patchBytes, err := json.Marshal(patchMap)
if err != nil {
Expand Down Expand Up @@ -806,7 +814,7 @@ func TestAddInvalidSpec(t *testing.T) {
expectedPatch := calculatePatch(e, `{
"status":{
}
}`, nil, cond)
}`, nil, cond, nil, "")
assert.JSONEq(t, expectedPatch, patch)
}

Expand Down Expand Up @@ -853,7 +861,7 @@ func TestUpdateInvalidSpec(t *testing.T) {
expectedPatch := calculatePatch(e, `{
"status":{
}
}`, nil, cond)
}`, nil, cond, nil, "")
assert.JSONEq(t, expectedPatch, patch)

}
Expand Down Expand Up @@ -893,7 +901,7 @@ func TestRemoveInvalidSpec(t *testing.T) {
expectedPatch := calculatePatch(e, `{
"status":{
}
}`, templateStatus, cond)
}`, templateStatus, cond, nil, "")
assert.JSONEq(t, expectedPatch, patch)
}

Expand Down
Loading
Loading