diff --git a/.github/scripts/.helm-tests/default/result.yaml b/.github/scripts/.helm-tests/default/result.yaml index 7e2da92fa5..585b255b00 100644 --- a/.github/scripts/.helm-tests/default/result.yaml +++ b/.github/scripts/.helm-tests/default/result.yaml @@ -158,135 +158,162 @@ spec: singular: analysis scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .spec.analysisDefinition.name - name: AnalysisDefinition - type: string - - jsonPath: .status.state - name: State - type: string - - jsonPath: .status.warning - name: Warning - type: string - - jsonPath: .status.pass - name: Pass - type: string - name: v1alpha3 - schema: - openAPIV3Schema: - description: Analysis is the Schema for the analyses API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation + - additionalPrinterColumns: + - jsonPath: .spec.analysisDefinition.name + name: AnalysisDefinition + type: string + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.warning + name: Warning + type: string + - jsonPath: .status.pass + name: Pass + type: string + name: v1alpha3 + schema: + openAPIV3Schema: + description: Analysis is the Schema for the analyses API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: AnalysisSpec defines the desired state of Analysis - properties: - analysisDefinition: - description: AnalysisDefinition refers to the AnalysisDefinition, - a CRD that stores the AnalysisValuesTemplates - properties: - name: - description: Name defines the name of the referenced object - type: string - namespace: - description: Namespace defines the namespace of the referenced - object + type: string + metadata: + type: object + spec: + description: AnalysisSpec defines the desired state of Analysis + properties: + analysisDefinition: + description: AnalysisDefinition refers to the AnalysisDefinition, + a CRD that stores the AnalysisValuesTemplates + properties: + name: + description: Name defines the name of the referenced object + type: string + namespace: + description: Namespace defines the namespace of the referenced + object + type: string + required: + - name + type: object + args: + additionalProperties: type: string - required: - - name - type: object - args: - additionalProperties: + description: Args corresponds to a map of key/value pairs that can + be used to substitute placeholders in the AnalysisValueTemplate + query. i.e. for args foo:bar the query could be "query:percentile(95)?scope=tag(my_foo_label:)". + type: object + timeframe: + description: Timeframe specifies the range for the corresponding query + in the AnalysisValueTemplate. Please note that either a combination + of 'from' and 'to' or the 'recent' property may be set. If neither + is set, the Analysis can not be added to the cluster. + properties: + from: + description: From is the time of start for the query. This field + follows RFC3339 time format + format: date-time + type: string + recent: + description: Recent describes a recent timeframe using a duration + string. E.g. Setting this to '5m' provides an Analysis for the + last five minutes + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + to: + description: To is the time of end for the query. This field follows + RFC3339 time format + format: date-time + type: string + type: object + required: + - analysisDefinition + - timeframe + type: object + status: + description: AnalysisStatus stores the status of the overall analysis + returns also pass or warnings + properties: + pass: + description: Pass returns whether the SLO is satisfied + type: boolean + raw: + description: Raw contains the raw result of the SLO computation type: string - description: Args corresponds to a map of key/value pairs that can - be used to substitute placeholders in the AnalysisValueTemplate - query. i.e. for args foo:bar the query could be "query:percentile(95)?scope=tag(my_foo_label:)". - type: object - timeframe: - description: Timeframe specifies the range for the corresponding query - in the AnalysisValueTemplate - properties: - from: - description: From is the time of start for the query, this field - follows RFC3339 time format - format: date-time - type: string - to: - description: To is the time of end for the query, this field follows - RFC3339 time format - format: date-time - type: string - required: - - from - - to - type: object - required: - - analysisDefinition - - timeframe - type: object - status: - description: AnalysisStatus stores the status of the overall analysis - returns also pass or warnings - properties: - pass: - description: Pass returns whether the SLO is satisfied - type: boolean - raw: - description: Raw contains the raw result of the SLO computation - type: string - state: - description: State describes the current state of the Analysis (Pending/Progressing/Completed) - type: string - storedValues: - additionalProperties: - description: ProviderResult stores reference of already collected - provider query associated to its objective template + state: + description: State describes the current state of the Analysis (Pending/Progressing/Completed) + type: string + storedValues: + additionalProperties: + description: ProviderResult stores reference of already collected + provider query associated to its objective template + properties: + errMsg: + description: ErrMsg stores any possible error at retrieval time + type: string + objectiveReference: + description: Objective store reference to corresponding objective + template + properties: + name: + description: Name defines the name of the referenced object + type: string + namespace: + description: Namespace defines the namespace of the referenced + object + type: string + required: + - name + type: object + value: + description: Value is the value the provider returned + type: string + type: object + description: StoredValues contains all analysis values that have already + been retrieved successfully + type: object + timeframe: + description: Timeframe describes the time frame which is evaluated + by the Analysis properties: - errMsg: - description: ErrMsg stores any possible error at retrieval time + from: + description: From is the time of start for the query. This field + follows RFC3339 time format + format: date-time type: string - objectiveReference: - description: Objective store reference to corresponding objective - template - properties: - name: - description: Name defines the name of the referenced object - type: string - namespace: - description: Namespace defines the namespace of the referenced - object - type: string - required: - - name - type: object - value: - description: Value is the value the provider returned + recent: + description: Recent describes a recent timeframe using a duration + string. E.g. Setting this to '5m' provides an Analysis for the + last five minutes + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + to: + description: To is the time of end for the query. This field follows + RFC3339 time format + format: date-time type: string type: object - description: StoredValues contains all analysis values that have already - been retrieved successfully - type: object - warning: - description: Warning returns whether the analysis returned a warning - type: boolean - required: - - state - type: object - type: object - served: true - storage: true - subresources: - status: {} + warning: + description: Warning returns whether the analysis returned a warning + type: boolean + required: + - state + - timeframe + type: object + type: object + served: true + storage: true + subresources: + status: {} --- # Source: klt/templates/analysisdefinition-crd.yaml apiVersion: apiextensions.k8s.io/v1 @@ -8344,6 +8371,26 @@ webhooks: resources: - keptnmetrics sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: 'metrics-webhook-service' + namespace: 'helmtests' + path: /validate-metrics-keptn-sh-v1alpha3-analysis + failurePolicy: Fail + name: vanalysis.kb.io + rules: + - apiGroups: + - metrics.keptn.sh + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - analyses + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/docs/content/en/docs/crd-ref/metrics/v1alpha3/_index.md b/docs/content/en/docs/crd-ref/metrics/v1alpha3/_index.md index 1ba59dd26b..5f25c214ce 100644 --- a/docs/content/en/docs/crd-ref/metrics/v1alpha3/_index.md +++ b/docs/content/en/docs/crd-ref/metrics/v1alpha3/_index.md @@ -120,7 +120,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `timeframe` _[Timeframe](#timeframe)_ | Timeframe specifies the range for the corresponding query in the AnalysisValueTemplate | +| `timeframe` _[Timeframe](#timeframe)_ | Timeframe specifies the range for the corresponding query in the AnalysisValueTemplate. Please note that either a combination of 'from' and 'to' or the 'recent' property may be set. If neither is set, the Analysis can not be added to the cluster. | | `args` _object (keys:string, values:string)_ | Args corresponds to a map of key/value pairs that can be used to substitute placeholders in the AnalysisValueTemplate query. i.e. for args foo:bar the query could be "query:percentile(95)?scope=tag(my_foo_label:{{.foo}})". | | `analysisDefinition` _[ObjectReference](#objectreference)_ | AnalysisDefinition refers to the AnalysisDefinition, a CRD that stores the AnalysisValuesTemplates | @@ -147,6 +147,7 @@ _Appears in:_ | Field | Description | | --- | --- | +| `timeframe` _[Timeframe](#timeframe)_ | Timeframe describes the time frame which is evaluated by the Analysis | | `raw` _string_ | Raw contains the raw result of the SLO computation | | `pass` _boolean_ | Pass returns whether the SLO is satisfied | | `warning` _boolean_ | Warning returns whether the analysis returned a warning | @@ -494,11 +495,13 @@ _Appears in:_ _Appears in:_ - [AnalysisSpec](#analysisspec) +- [AnalysisStatus](#analysisstatus) | Field | Description | | --- | --- | -| `from` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta)_ | From is the time of start for the query, this field follows RFC3339 time format | -| `to` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta)_ | To is the time of end for the query, this field follows RFC3339 time format | +| `from` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta)_ | From is the time of start for the query. This field follows RFC3339 time format | +| `to` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta)_ | To is the time of end for the query. This field follows RFC3339 time format | +| `recent` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#duration-v1-meta)_ | Recent describes a recent timeframe using a duration string. E.g. Setting this to '5m' provides an Analysis for the last five minutes | #### TotalScore diff --git a/helm/chart/templates/analysis-crd.yaml b/helm/chart/templates/analysis-crd.yaml index 8b3f9ceb93..fdb3468c59 100644 --- a/helm/chart/templates/analysis-crd.yaml +++ b/helm/chart/templates/analysis-crd.yaml @@ -19,132 +19,159 @@ spec: singular: analysis scope: Namespaced versions: - - additionalPrinterColumns: - - jsonPath: .spec.analysisDefinition.name - name: AnalysisDefinition - type: string - - jsonPath: .status.state - name: State - type: string - - jsonPath: .status.warning - name: Warning - type: string - - jsonPath: .status.pass - name: Pass - type: string - name: v1alpha3 - schema: - openAPIV3Schema: - description: Analysis is the Schema for the analyses API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation + - additionalPrinterColumns: + - jsonPath: .spec.analysisDefinition.name + name: AnalysisDefinition + type: string + - jsonPath: .status.state + name: State + type: string + - jsonPath: .status.warning + name: Warning + type: string + - jsonPath: .status.pass + name: Pass + type: string + name: v1alpha3 + schema: + openAPIV3Schema: + description: Analysis is the Schema for the analyses API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: AnalysisSpec defines the desired state of Analysis - properties: - analysisDefinition: - description: AnalysisDefinition refers to the AnalysisDefinition, - a CRD that stores the AnalysisValuesTemplates - properties: - name: - description: Name defines the name of the referenced object - type: string - namespace: - description: Namespace defines the namespace of the referenced - object + type: string + metadata: + type: object + spec: + description: AnalysisSpec defines the desired state of Analysis + properties: + analysisDefinition: + description: AnalysisDefinition refers to the AnalysisDefinition, + a CRD that stores the AnalysisValuesTemplates + properties: + name: + description: Name defines the name of the referenced object + type: string + namespace: + description: Namespace defines the namespace of the referenced + object + type: string + required: + - name + type: object + args: + additionalProperties: type: string - required: - - name - type: object - args: - additionalProperties: + description: Args corresponds to a map of key/value pairs that can + be used to substitute placeholders in the AnalysisValueTemplate + query. i.e. for args foo:bar the query could be "query:percentile(95)?scope=tag(my_foo_label:{{.foo}})". + type: object + timeframe: + description: Timeframe specifies the range for the corresponding query + in the AnalysisValueTemplate. Please note that either a combination + of 'from' and 'to' or the 'recent' property may be set. If neither + is set, the Analysis can not be added to the cluster. + properties: + from: + description: From is the time of start for the query. This field + follows RFC3339 time format + format: date-time + type: string + recent: + description: Recent describes a recent timeframe using a duration + string. E.g. Setting this to '5m' provides an Analysis for the + last five minutes + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + to: + description: To is the time of end for the query. This field follows + RFC3339 time format + format: date-time + type: string + type: object + required: + - analysisDefinition + - timeframe + type: object + status: + description: AnalysisStatus stores the status of the overall analysis + returns also pass or warnings + properties: + pass: + description: Pass returns whether the SLO is satisfied + type: boolean + raw: + description: Raw contains the raw result of the SLO computation type: string - description: Args corresponds to a map of key/value pairs that can - be used to substitute placeholders in the AnalysisValueTemplate - query. i.e. for args foo:bar the query could be "query:percentile(95)?scope=tag(my_foo_label:{{.foo}})". - type: object - timeframe: - description: Timeframe specifies the range for the corresponding query - in the AnalysisValueTemplate - properties: - from: - description: From is the time of start for the query, this field - follows RFC3339 time format - format: date-time - type: string - to: - description: To is the time of end for the query, this field follows - RFC3339 time format - format: date-time - type: string - required: - - from - - to - type: object - required: - - analysisDefinition - - timeframe - type: object - status: - description: AnalysisStatus stores the status of the overall analysis - returns also pass or warnings - properties: - pass: - description: Pass returns whether the SLO is satisfied - type: boolean - raw: - description: Raw contains the raw result of the SLO computation - type: string - state: - description: State describes the current state of the Analysis (Pending/Progressing/Completed) - type: string - storedValues: - additionalProperties: - description: ProviderResult stores reference of already collected - provider query associated to its objective template + state: + description: State describes the current state of the Analysis (Pending/Progressing/Completed) + type: string + storedValues: + additionalProperties: + description: ProviderResult stores reference of already collected + provider query associated to its objective template + properties: + errMsg: + description: ErrMsg stores any possible error at retrieval time + type: string + objectiveReference: + description: Objective store reference to corresponding objective + template + properties: + name: + description: Name defines the name of the referenced object + type: string + namespace: + description: Namespace defines the namespace of the referenced + object + type: string + required: + - name + type: object + value: + description: Value is the value the provider returned + type: string + type: object + description: StoredValues contains all analysis values that have already + been retrieved successfully + type: object + timeframe: + description: Timeframe describes the time frame which is evaluated + by the Analysis properties: - errMsg: - description: ErrMsg stores any possible error at retrieval time + from: + description: From is the time of start for the query. This field + follows RFC3339 time format + format: date-time + type: string + recent: + description: Recent describes a recent timeframe using a duration + string. E.g. Setting this to '5m' provides an Analysis for the + last five minutes + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ type: string - objectiveReference: - description: Objective store reference to corresponding objective - template - properties: - name: - description: Name defines the name of the referenced object - type: string - namespace: - description: Namespace defines the namespace of the referenced - object - type: string - required: - - name - type: object - value: - description: Value is the value the provider returned + to: + description: To is the time of end for the query. This field follows + RFC3339 time format + format: date-time type: string type: object - description: StoredValues contains all analysis values that have already - been retrieved successfully - type: object - warning: - description: Warning returns whether the analysis returned a warning - type: boolean - required: - - state - type: object - type: object - served: true - storage: true - subresources: - status: {} + warning: + description: Warning returns whether the analysis returned a warning + type: boolean + required: + - state + - timeframe + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/chart/templates/metrics-validating-webhook-configuration.yaml b/helm/chart/templates/metrics-validating-webhook-configuration.yaml index c5f2306a8e..d188313680 100644 --- a/helm/chart/templates/metrics-validating-webhook-configuration.yaml +++ b/helm/chart/templates/metrics-validating-webhook-configuration.yaml @@ -28,6 +28,26 @@ webhooks: resources: - keptnmetrics sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: 'metrics-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-metrics-keptn-sh-v1alpha3-analysis + failurePolicy: Fail + name: vanalysis.kb.io + rules: + - apiGroups: + - metrics.keptn.sh + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - analyses + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go b/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go index bcaa16a452..a4c7ee86e0 100644 --- a/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go +++ b/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go @@ -47,8 +47,8 @@ type ISchedulingGatesHandlerMock struct { WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance } } - lockEnabled sync.RWMutex - lockRemoveGates sync.RWMutex + lockEnabled sync.RWMutex + lockRemoveGates sync.RWMutex } // Enabled calls EnabledFunc. diff --git a/metrics-operator/api/v1alpha3/analysis_types.go b/metrics-operator/api/v1alpha3/analysis_types.go index a9bae70f05..2be35bfdbc 100644 --- a/metrics-operator/api/v1alpha3/analysis_types.go +++ b/metrics-operator/api/v1alpha3/analysis_types.go @@ -17,12 +17,16 @@ limitations under the License. package v1alpha3 import ( + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // AnalysisSpec defines the desired state of Analysis type AnalysisSpec struct { - //Timeframe specifies the range for the corresponding query in the AnalysisValueTemplate + // Timeframe specifies the range for the corresponding query in the AnalysisValueTemplate. Please note that either + // a combination of 'from' and 'to' or the 'recent' property may be set. If neither is set, the Analysis can + // not be added to the cluster. Timeframe `json:"timeframe"` // Args corresponds to a map of key/value pairs that can be used to substitute placeholders in the AnalysisValueTemplate query. i.e. for args foo:bar the query could be "query:percentile(95)?scope=tag(my_foo_label:{{.foo}})". Args map[string]string `json:"args,omitempty"` @@ -42,6 +46,8 @@ type ProviderResult struct { // AnalysisStatus stores the status of the overall analysis returns also pass or warnings type AnalysisStatus struct { + // Timeframe describes the time frame which is evaluated by the Analysis + Timeframe Timeframe `json:"timeframe"` // Raw contains the raw result of the SLO computation Raw string `json:"raw,omitempty"` // Pass returns whether the SLO is satisfied @@ -80,10 +86,59 @@ type AnalysisList struct { } type Timeframe struct { - // From is the time of start for the query, this field follows RFC3339 time format - From metav1.Time `json:"from"` - // To is the time of end for the query, this field follows RFC3339 time format - To metav1.Time `json:"to"` + // From is the time of start for the query. This field follows RFC3339 time format + From metav1.Time `json:"from,omitempty"` + // To is the time of end for the query. This field follows RFC3339 time format + To metav1.Time `json:"to,omitempty"` + // Recent describes a recent timeframe using a duration string. E.g. Setting this to '5m' provides an Analysis + // for the last five minutes + // +optional + // +kubebuilder:validation:Pattern="^0|([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$" + // +kubebuilder:validation:Type:=string + // +optional + Recent metav1.Duration `json:"recent,omitempty"` +} + +// GetFrom returns the 'from' timestamp from the status of the Analysis. +// This function has been added to provide a clear way of retrieving the correct timestamp +// to use, which is the one from the Status. +func (a *Analysis) GetFrom() time.Time { + return a.Status.Timeframe.GetFrom() +} + +// GetTo returns the 'from' timestamp from the status of the Analysis. +// This function has been added to provide a clear way of retrieving the correct timestamp +// to use, which is the one from the Status. +func (a *Analysis) GetTo() time.Time { + return a.Status.Timeframe.GetTo() +} + +func (a *Analysis) EnsureTimeframeIsSet() { + // make sure the correct time frame is set in the status - once an Analysis with a duration string specifying the + // time frame is triggered, the time frame derived from that duration should stay the same and not shift over the course + // of multiple reconciliation loops + if a.Status.Timeframe.From.IsZero() || a.Status.Timeframe.To.IsZero() { + a.Status.Timeframe.From = metav1.Time{ + Time: a.Spec.GetFrom(), + } + a.Status.Timeframe.To = metav1.Time{ + Time: a.Spec.GetTo(), + } + } +} + +func (t *Timeframe) GetFrom() time.Time { + if t.Recent.Duration > 0 { + return time.Now().UTC().Add(-t.Recent.Duration) + } + return t.From.Time +} + +func (t *Timeframe) GetTo() time.Time { + if t.Recent.Duration > 0 { + return time.Now().UTC() + } + return t.To.Time } func init() { diff --git a/metrics-operator/api/v1alpha3/analysis_types_test.go b/metrics-operator/api/v1alpha3/analysis_types_test.go new file mode 100644 index 0000000000..1778006dc8 --- /dev/null +++ b/metrics-operator/api/v1alpha3/analysis_types_test.go @@ -0,0 +1,80 @@ +package v1alpha3 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAnalysis_SetAndRetrieveTime(t *testing.T) { + now := time.Now().UTC() + before := now.Add(-5 * time.Minute) + type fields struct { + TypeMeta v1.TypeMeta + ObjectMeta v1.ObjectMeta + Spec AnalysisSpec + Status AnalysisStatus + } + tests := []struct { + name string + fields fields + wantFrom time.Time + wantTo time.Time + }{ + { + name: "from and to timestamps set", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: before, + }, + To: v1.Time{ + Time: now, + }, + }, + }, + }, + wantFrom: before, + wantTo: now, + }, + { + name: "'recent' set", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + Recent: v1.Duration{ + Duration: 3 * time.Minute, + }, + }, + }, + }, + wantFrom: now.Add(-3 * time.Minute), + wantTo: now, + }, + { + name: "nothing set", + fields: fields{ + Spec: AnalysisSpec{}, + }, + wantFrom: time.Time{}, + wantTo: time.Time{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Analysis{ + TypeMeta: tt.fields.TypeMeta, + ObjectMeta: tt.fields.ObjectMeta, + Spec: tt.fields.Spec, + Status: tt.fields.Status, + } + a.EnsureTimeframeIsSet() + + require.WithinDuration(t, tt.wantFrom, a.GetFrom(), 1*time.Minute) + require.WithinDuration(t, tt.wantTo, a.GetTo(), 1*time.Minute) + }) + } +} diff --git a/metrics-operator/api/v1alpha3/analysis_webhook.go b/metrics-operator/api/v1alpha3/analysis_webhook.go new file mode 100644 index 0000000000..e15df15e4c --- /dev/null +++ b/metrics-operator/api/v1alpha3/analysis_webhook.go @@ -0,0 +1,100 @@ +/* +Copyright 2023. + +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 v1alpha3 + +import ( + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// Analysislog is for logging in this package. +var Analysislog = logf.Log.WithName("analysis-webhook") + +func (a *Analysis) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(a). + Complete() +} + +//+kubebuilder:webhook:path=/validate-metrics-keptn-sh-v1alpha3-analysis,mutating=false,failurePolicy=fail,sideEffects=None,groups=metrics.keptn.sh,resources=analyses,verbs=create;update,versions=v1alpha3,name=analysis.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Analysis{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (a *Analysis) ValidateCreate() (admission.Warnings, error) { + Analysislog.Info("validate create", "name", a.Name, "namespace", a.Namespace) + + if err := a.validateTimeframe(); err != nil { + return admission.Warnings{}, err + } + + return admission.Warnings{}, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (a *Analysis) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + Analysislog.Info("validate update", "name", a.Name, "namespace", a.Namespace) + + if err := a.validateTimeframe(); err != nil { + return admission.Warnings{}, err + } + + return admission.Warnings{}, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (a *Analysis) ValidateDelete() (admission.Warnings, error) { + Analysislog.Info("validate delete", "name", a.Name, "namespace", a.Namespace) + + return admission.Warnings{}, nil +} + +func (a *Analysis) validateTimeframe() error { + // if 'Recent' is set, this must be the only field + if a.Spec.Timeframe.Recent.Duration != 0 { + if !a.Spec.Timeframe.From.IsZero() || !a.Spec.Timeframe.To.IsZero() { + return field.Invalid( + field.NewPath("spec").Child("timeframe"), + a.Spec.Timeframe, + errors.New("the field 'recent' can not be used in conjunction with 'from'/'to'").Error(), + ) + } + return nil + } + // if 'Recent' is not set, both 'From' and 'To' must be set + if a.Spec.Timeframe.From.IsZero() || a.Spec.Timeframe.To.IsZero() { + return field.Invalid( + field.NewPath("spec").Child("timeframe"), + a.Spec.Timeframe, + errors.New("either 'recent' or both 'from' and 'to' must be set").Error(), + ) + } + if !a.Spec.Timeframe.To.After(a.Spec.Timeframe.From.Time) { + return field.Invalid( + field.NewPath("spec").Child("timeframe"), + a.Spec.Timeframe, + errors.New("value of 'to' must be a timestamp later than 'from'").Error(), + ) + } + + return nil +} diff --git a/metrics-operator/api/v1alpha3/analysis_webhook_test.go b/metrics-operator/api/v1alpha3/analysis_webhook_test.go new file mode 100644 index 0000000000..b2ceea2192 --- /dev/null +++ b/metrics-operator/api/v1alpha3/analysis_webhook_test.go @@ -0,0 +1,291 @@ +package v1alpha3 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestAnalysis_Validation(t *testing.T) { + type fields struct { + TypeMeta v1.TypeMeta + ObjectMeta v1.ObjectMeta + Spec AnalysisSpec + Status AnalysisStatus + } + tests := []struct { + name string + verb string + fields fields + want admission.Warnings + wantErr bool + }{ + // CREATE + { + name: "valid Analysis with from/to timestamps", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now(), + }, + To: v1.Time{ + Time: time.Now().Add(1 * time.Second), + }, + }, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: false, + }, + { + name: "valid Analysis with 'recent' being set'", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + Recent: v1.Duration{ + Duration: 5 * time.Second, + }, + }, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: false, + }, + { + name: "invalid Analysis with from timestamp greater than to timestamps", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now().Add(1 * time.Second), + }, + To: v1.Time{ + Time: time.Now(), + }, + }, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with 'from' being nil", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + To: v1.Time{ + Time: time.Now(), + }, + }, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with 'to' being nil", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now(), + }, + }, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with 'recent' ad 'from'/'to' being set", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now(), + }, + Recent: v1.Duration{ + Duration: 1 * time.Second, + }, + }, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with no timeframe info set", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{}, + }, + }, + verb: "create", + want: admission.Warnings{}, + wantErr: true, + }, + // UPDATE + { + name: "valid Analysis with from/to timestamps - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now(), + }, + To: v1.Time{ + Time: time.Now().Add(1 * time.Second), + }, + }, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: false, + }, + { + name: "valid Analysis with 'recent' being set' - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + Recent: v1.Duration{ + Duration: 5 * time.Second, + }, + }, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: false, + }, + { + name: "invalid Analysis with from timestamp greater than to timestamps - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now().Add(1 * time.Second), + }, + To: v1.Time{ + Time: time.Now(), + }, + }, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with 'from' being nil - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + To: v1.Time{ + Time: time.Now(), + }, + }, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with 'to' being nil - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now(), + }, + }, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with 'recent' ad 'from'/'to' being set - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{ + From: v1.Time{ + Time: time.Now(), + }, + Recent: v1.Duration{ + Duration: 1 * time.Second, + }, + }, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: true, + }, + { + name: "invalid Analysis with no timeframe info set - update", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{}, + }, + }, + verb: "update", + want: admission.Warnings{}, + wantErr: true, + }, + // DELETE + { + name: "delete analysis", + fields: fields{ + Spec: AnalysisSpec{ + Timeframe: Timeframe{}, + }, + }, + verb: "delete", + want: admission.Warnings{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Analysis{ + TypeMeta: tt.fields.TypeMeta, + ObjectMeta: tt.fields.ObjectMeta, + Spec: tt.fields.Spec, + Status: tt.fields.Status, + } + var got []string + var err error + + switch tt.verb { + case "create": + got, err = a.ValidateCreate() + case "update": + got, err = a.ValidateUpdate(&Analysis{}) + case "delete": + got, err = a.ValidateDelete() + default: + got, err = a.ValidateCreate() + } + + if !tt.wantErr { + require.Nil(t, err) + } else { + require.NotNil(t, err) + } + require.EqualValues(t, tt.want, got) + }) + } +} diff --git a/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go b/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go index 455e123a21..a3a7a97d42 100644 --- a/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go +++ b/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go @@ -191,6 +191,7 @@ func (in *AnalysisSpec) DeepCopy() *AnalysisSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AnalysisStatus) DeepCopyInto(out *AnalysisStatus) { *out = *in + in.Timeframe.DeepCopyInto(&out.Timeframe) if in.StoredValues != nil { in, out := &in.StoredValues, &out.StoredValues *out = make(map[string]ProviderResult, len(*in)) @@ -678,6 +679,7 @@ func (in *Timeframe) DeepCopyInto(out *Timeframe) { *out = *in in.From.DeepCopyInto(&out.From) in.To.DeepCopyInto(&out.To) + out.Recent = in.Recent } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Timeframe. diff --git a/metrics-operator/config/crd/bases/metrics.keptn.sh_analyses.yaml b/metrics-operator/config/crd/bases/metrics.keptn.sh_analyses.yaml index 6ecd3242f1..c5af7dd7b0 100644 --- a/metrics-operator/config/crd/bases/metrics.keptn.sh_analyses.yaml +++ b/metrics-operator/config/crd/bases/metrics.keptn.sh_analyses.yaml @@ -70,21 +70,26 @@ spec: type: object timeframe: description: Timeframe specifies the range for the corresponding query - in the AnalysisValueTemplate + in the AnalysisValueTemplate. Please note that either a combination + of 'from' and 'to' or the 'recent' property may be set. If neither + is set, the Analysis can not be added to the cluster. properties: from: - description: From is the time of start for the query, this field + description: From is the time of start for the query. This field follows RFC3339 time format format: date-time type: string + recent: + description: Recent describes a recent timeframe using a duration + string. E.g. Setting this to '5m' provides an Analysis for the + last five minutes + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string to: - description: To is the time of end for the query, this field follows + description: To is the time of end for the query. This field follows RFC3339 time format format: date-time type: string - required: - - from - - to type: object required: - analysisDefinition @@ -132,11 +137,33 @@ spec: description: StoredValues contains all analysis values that have already been retrieved successfully type: object + timeframe: + description: Timeframe describes the time frame which is evaluated + by the Analysis + properties: + from: + description: From is the time of start for the query. This field + follows RFC3339 time format + format: date-time + type: string + recent: + description: Recent describes a recent timeframe using a duration + string. E.g. Setting this to '5m' provides an Analysis for the + last five minutes + pattern: ^0|([0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$ + type: string + to: + description: To is the time of end for the query. This field follows + RFC3339 time format + format: date-time + type: string + type: object warning: description: Warning returns whether the analysis returned a warning type: boolean required: - state + - timeframe type: object type: object served: true diff --git a/metrics-operator/config/webhook/manifests.yaml b/metrics-operator/config/webhook/manifests.yaml index d855e38641..48c007b331 100644 --- a/metrics-operator/config/webhook/manifests.yaml +++ b/metrics-operator/config/webhook/manifests.yaml @@ -47,3 +47,23 @@ webhooks: resources: - analysisdefinitions sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metrics-webhook-service + namespace: system + path: /validate-metrics-keptn-sh-v1alpha3-analysis + failurePolicy: Fail + name: vanalysis.kb.io + rules: + - apiGroups: + - metrics.keptn.sh + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - analyses + sideEffects: None diff --git a/metrics-operator/controllers/analysis/controller.go b/metrics-operator/controllers/analysis/controller.go index b6cf1fb1e3..a42d5549bb 100644 --- a/metrics-operator/controllers/analysis/controller.go +++ b/metrics-operator/controllers/analysis/controller.go @@ -78,12 +78,14 @@ func (a *AnalysisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, nil } + analysis.EnsureTimeframeIsSet() + //find AnalysisDefinition to have the collection of Objectives analysisDef, err := a.retrieveAnalysisDefinition(ctx, analysis) if err != nil { // do not return error, as here we should always try to fetch the definition again // in the next reconcile loop - return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, nil + return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, a.updateStatus(ctx, analysis) } if analysis.Status.State.IsPending() { diff --git a/metrics-operator/controllers/analysis/controller_test.go b/metrics-operator/controllers/analysis/controller_test.go index 40b1049a18..a3c8d2acfc 100644 --- a/metrics-operator/controllers/analysis/controller_test.go +++ b/metrics-operator/controllers/analysis/controller_test.go @@ -76,6 +76,7 @@ func TestAnalysisReconciler_Reconcile_BasicControlLoop(t *testing.T) { analysis, analysisDef, template, _ := getTestCRDs() + currentTime := time.Now().Round(time.Minute) analysis2 := metricsapi.Analysis{ ObjectMeta: metav1.ObjectMeta{ Name: "my-analysis", @@ -84,10 +85,10 @@ func TestAnalysisReconciler_Reconcile_BasicControlLoop(t *testing.T) { Spec: metricsapi.AnalysisSpec{ Timeframe: metricsapi.Timeframe{ From: metav1.Time{ - Time: time.Now(), + Time: currentTime, }, To: metav1.Time{ - Time: time.Now(), + Time: currentTime, }, }, Args: map[string]string{ @@ -190,6 +191,10 @@ func TestAnalysisReconciler_Reconcile_BasicControlLoop(t *testing.T) { want: controllerruntime.Result{Requeue: true, RequeueAfter: 10 * time.Second}, wantErr: false, status: &metricsapi.AnalysisStatus{ + Timeframe: metricsapi.Timeframe{ + From: analysis.Spec.From, + To: analysis.Spec.To, + }, State: metricsapi.StatePending, }, res: metricstypes.AnalysisResult{Pass: false}, @@ -200,6 +205,10 @@ func TestAnalysisReconciler_Reconcile_BasicControlLoop(t *testing.T) { want: controllerruntime.Result{Requeue: true, RequeueAfter: 10 * time.Second}, wantErr: false, status: &metricsapi.AnalysisStatus{ + Timeframe: metricsapi.Timeframe{ + From: analysis.Spec.From, + To: analysis.Spec.To, + }, State: metricsapi.StateProgressing, }, res: metricstypes.AnalysisResult{Pass: false}, @@ -212,11 +221,19 @@ func TestAnalysisReconciler_Reconcile_BasicControlLoop(t *testing.T) { return ctx, &mymock }, }, { - name: "succeeded, status updated", - client: fake2.NewClient(&analysis, &analysisDef, &template), - want: controllerruntime.Result{}, - wantErr: false, - status: &metricsapi.AnalysisStatus{Raw: "{\"objectiveResults\":null,\"totalScore\":0,\"maximumScore\":0,\"pass\":true,\"warning\":false}", Pass: true, State: metricsapi.StateCompleted}, + name: "succeeded, status updated", + client: fake2.NewClient(&analysis, &analysisDef, &template), + want: controllerruntime.Result{}, + wantErr: false, + status: &metricsapi.AnalysisStatus{ + Timeframe: metricsapi.Timeframe{ + From: analysis.Spec.From, + To: analysis.Spec.To, + }, + Raw: "{\"objectiveResults\":null,\"totalScore\":0,\"maximumScore\":0,\"pass\":true,\"warning\":false}", + Pass: true, + State: metricsapi.StateCompleted, + }, res: metricstypes.AnalysisResult{Pass: true}, mockFactory: mockFactory, }, { @@ -225,11 +242,19 @@ func TestAnalysisReconciler_Reconcile_BasicControlLoop(t *testing.T) { want: controllerruntime.Result{}, wantErr: false, }, { - name: "succeeded - analysis in different namespace, status updated", - client: fake2.NewClient(&analysis2, &analysisDef2, &template), - want: controllerruntime.Result{}, - wantErr: false, - status: &metricsapi.AnalysisStatus{Raw: "{\"objectiveResults\":null,\"totalScore\":0,\"maximumScore\":0,\"pass\":true,\"warning\":false}", Pass: true, State: metricsapi.StateCompleted}, + name: "succeeded - analysis in different namespace, status updated", + client: fake2.NewClient(&analysis2, &analysisDef2, &template), + want: controllerruntime.Result{}, + wantErr: false, + status: &metricsapi.AnalysisStatus{ + Timeframe: metricsapi.Timeframe{ + From: analysis.Spec.From, + To: analysis.Spec.To, + }, + Raw: "{\"objectiveResults\":null,\"totalScore\":0,\"maximumScore\":0,\"pass\":true,\"warning\":false}", + Pass: true, + State: metricsapi.StateCompleted, + }, res: metricstypes.AnalysisResult{Pass: true}, mockFactory: mockFactory, }, @@ -307,7 +332,15 @@ func TestAnalysisReconciler_ExistingAnalysisStatusIsFlushedWhenEvaluationFinishe NamespacedName: types.NamespacedName{Namespace: "default", Name: "my-analysis"}, } - status := &metricsapi.AnalysisStatus{Raw: "{\"objectiveResults\":null,\"totalScore\":0,\"maximumScore\":0,\"pass\":true,\"warning\":false}", Pass: true, State: metricsapi.StateCompleted} + status := &metricsapi.AnalysisStatus{ + Timeframe: metricsapi.Timeframe{ + From: analysis.Spec.From, + To: analysis.Spec.To, + }, + Raw: "{\"objectiveResults\":null,\"totalScore\":0,\"maximumScore\":0,\"pass\":true,\"warning\":false}", + Pass: true, + State: metricsapi.StateCompleted, + } got, err := a.Reconcile(context.TODO(), req) @@ -321,7 +354,72 @@ func TestAnalysisReconciler_ExistingAnalysisStatusIsFlushedWhenEvaluationFinishe } +func TestAnalysisReconciler_AnalysisTimeframeIsDerivedFromDurationString(t *testing.T) { + analysis := metricsapi.Analysis{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-analysis", + Namespace: "default", + }, + Spec: metricsapi.AnalysisSpec{ + Timeframe: metricsapi.Timeframe{Recent: metav1.Duration{Duration: 5 * time.Minute}}, + Args: map[string]string{ + "good": "good", + "dot": ".", + }, + AnalysisDefinition: metricsapi.ObjectReference{ + Name: "my-analysis-def", + Namespace: "default", + }, + }, + Status: metricsapi.AnalysisStatus{ + State: metricsapi.StatePending, + }, + } + + mockFactory := func(ctx context.Context, analysisMoqParam *metricsapi.Analysis, obj []metricsapi.Objective, numWorkers int, c client.Client, log logr.Logger, namespace string) (context.Context, IAnalysisPool) { + mymock := fake.IAnalysisPoolMock{ + DispatchAndCollectFunc: func(ctx context.Context) (map[string]metricsapi.ProviderResult, error) { + return map[string]metricsapi.ProviderResult{}, nil + }, + } + return ctx, &mymock + } + + fclient := fake2.NewClient(&analysis) + a := &AnalysisReconciler{ + Client: fclient, + Scheme: fclient.Scheme(), + Log: testr.New(t), + MaxWorkers: 2, + NewWorkersPoolFactory: mockFactory, + IAnalysisEvaluator: &fakeEvaluator.IAnalysisEvaluatorMock{ + EvaluateFunc: func(values map[string]metricsapi.ProviderResult, ad *metricsapi.AnalysisDefinition) metricstypes.AnalysisResult { + return metricstypes.AnalysisResult{Pass: true} + }}, + } + + req := controllerruntime.Request{ + NamespacedName: types.NamespacedName{Namespace: "default", Name: "my-analysis"}, + } + + got, err := a.Reconcile(context.TODO(), req) + + // expect to be re-queued, since the AnalysisDefinition was not there, but the from/to timestamps should be set + // as soon as the reconciliation has started + require.Nil(t, err) + require.True(t, got.Requeue) + resAnalysis := metricsapi.Analysis{} + err = fclient.Get(context.TODO(), req.NamespacedName, &resAnalysis) + require.Nil(t, err) + + currentTime := time.Now().UTC() + require.WithinDuration(t, currentTime, resAnalysis.Status.Timeframe.GetTo(), time.Minute) + require.WithinDuration(t, currentTime.Add(-5*time.Minute), resAnalysis.Status.Timeframe.GetFrom(), time.Minute) + +} + func getTestCRDs() (metricsapi.Analysis, metricsapi.AnalysisDefinition, metricsapi.AnalysisValueTemplate, metricsapi.KeptnMetricsProvider) { + currentTime := time.Now().Round(time.Minute) analysis := metricsapi.Analysis{ ObjectMeta: metav1.ObjectMeta{ Name: "my-analysis", @@ -330,10 +428,10 @@ func getTestCRDs() (metricsapi.Analysis, metricsapi.AnalysisDefinition, metricsa Spec: metricsapi.AnalysisSpec{ Timeframe: metricsapi.Timeframe{ From: metav1.Time{ - Time: time.Now(), + Time: currentTime, }, To: metav1.Time{ - Time: time.Now(), + Time: currentTime, }, }, Args: map[string]string{ diff --git a/metrics-operator/controllers/analysis/objectives_evaluator.go b/metrics-operator/controllers/analysis/objectives_evaluator.go index a4d0cd8c88..3464335a26 100644 --- a/metrics-operator/controllers/analysis/objectives_evaluator.go +++ b/metrics-operator/controllers/analysis/objectives_evaluator.go @@ -33,7 +33,7 @@ func (oe ObjectivesEvaluator) Evaluate(ctx context.Context, providerType string, for o := range obj { value := "" var strErr string - value, err = provider.FetchAnalysisValue(ctx, o.Query, oe.Analysis.Spec, o.Provider) + value, err = provider.FetchAnalysisValue(ctx, o.Query, *oe.Analysis, o.Provider) if err != nil { strErr = err.Error() diff --git a/metrics-operator/controllers/analysis/objectives_evaluator_test.go b/metrics-operator/controllers/analysis/objectives_evaluator_test.go index 1e7a1550a2..480aa97484 100644 --- a/metrics-operator/controllers/analysis/objectives_evaluator_test.go +++ b/metrics-operator/controllers/analysis/objectives_evaluator_test.go @@ -30,7 +30,7 @@ func TestEvaluate(t *testing.T) { name: "SuccessfulEvaluation", providerType: "mockProvider", mockProvider: &fake2.KeptnSLIProviderMock{ - FetchAnalysisValueFunc: func(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { + FetchAnalysisValueFunc: func(ctx context.Context, query string, spec metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { return "10", nil }, }, @@ -58,7 +58,7 @@ func TestEvaluate(t *testing.T) { name: "FailedEvaluation", providerType: "mockProvider", mockProvider: &fake2.KeptnSLIProviderMock{ - FetchAnalysisValueFunc: func(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { + FetchAnalysisValueFunc: func(ctx context.Context, query string, spec metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { return "", fmt.Errorf("something bad") }, }, diff --git a/metrics-operator/controllers/common/providers/datadog/datadog.go b/metrics-operator/controllers/common/providers/datadog/datadog.go index 93adbafd8a..221a932a5d 100644 --- a/metrics-operator/controllers/common/providers/datadog/datadog.go +++ b/metrics-operator/controllers/common/providers/datadog/datadog.go @@ -26,10 +26,10 @@ type KeptnDataDogProvider struct { K8sClient client.Client } -func (d *KeptnDataDogProvider) FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { +func (d *KeptnDataDogProvider) FetchAnalysisValue(ctx context.Context, query string, analysis metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { ctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() - res, _, err := d.query(ctx, query, *provider, spec.From.Unix(), spec.To.Unix()) + res, _, err := d.query(ctx, query, *provider, analysis.GetFrom().Unix(), analysis.GetTo().Unix()) return res, err } diff --git a/metrics-operator/controllers/common/providers/datadog/datadog_test.go b/metrics-operator/controllers/common/providers/datadog/datadog_test.go index 73412c76f3..c871c4349c 100644 --- a/metrics-operator/controllers/common/providers/datadog/datadog_test.go +++ b/metrics-operator/controllers/common/providers/datadog/datadog_test.go @@ -954,14 +954,16 @@ func TestFetchAnalysisValue_HappyPath(t *testing.T) { }, } - spec := metricsapi.AnalysisSpec{ - Timeframe: metricsapi.Timeframe{ - From: metav1.Time{Time: time.Now()}, - To: metav1.Time{Time: time.Now()}, + analysis := metricsapi.Analysis{ + Spec: metricsapi.AnalysisSpec{ + Timeframe: metricsapi.Timeframe{ + From: metav1.Time{Time: time.Now()}, + To: metav1.Time{Time: time.Now()}, + }, }, } - r, e := kdd.FetchAnalysisValue(context.TODO(), "system.cpu.idle{*}", spec, &p) + r, e := kdd.FetchAnalysisValue(context.TODO(), "system.cpu.idle{*}", analysis, &p) require.Nil(t, e) require.Equal(t, fmt.Sprintf("%.3f", 89.116), r) diff --git a/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go b/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go index 41887ca43e..872e82715f 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go +++ b/metrics-operator/controllers/common/providers/dynatrace/dynatrace.go @@ -38,10 +38,10 @@ type DynatraceData struct { Values []*float64 `json:"values"` } -func (d *KeptnDynatraceProvider) FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { +func (d *KeptnDynatraceProvider) FetchAnalysisValue(ctx context.Context, query string, analysis metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { baseURL := d.normalizeAPIURL(provider.Spec.TargetServer) escapedQ := urlEncodeQuery(query) - qURL := baseURL + "v2/metrics/query?metricSelector=" + escapedQ + "&from=" + spec.From.String() + "&to=" + spec.To.String() + qURL := baseURL + "v2/metrics/query?metricSelector=" + escapedQ + "&from=" + analysis.GetFrom().String() + "&to=" + analysis.GetTo().String() res, _, err := d.runQuery(ctx, qURL, *provider) return res, err } diff --git a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go index 22c75cd8b0..ba531bb8aa 100644 --- a/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go +++ b/metrics-operator/controllers/common/providers/dynatrace/dynatrace_dql.go @@ -31,7 +31,7 @@ type keptnDynatraceDQLProvider struct { clock clock.Clock } -func (d *keptnDynatraceDQLProvider) FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { +func (d *keptnDynatraceDQLProvider) FetchAnalysisValue(ctx context.Context, query string, analysis metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { //TODO implement me panic("implement me") } diff --git a/metrics-operator/controllers/common/providers/fake/provider_mock.go b/metrics-operator/controllers/common/providers/fake/provider_mock.go index f0773337f9..c8201d388d 100644 --- a/metrics-operator/controllers/common/providers/fake/provider_mock.go +++ b/metrics-operator/controllers/common/providers/fake/provider_mock.go @@ -21,7 +21,7 @@ import ( // EvaluateQueryForStepFunc: func(ctx context.Context, metric metricsapi.KeptnMetric, provider metricsapi.KeptnMetricsProvider) ([]string, []byte, error) { // panic("mock out the EvaluateQueryForStep method") // }, -// FetchAnalysisValueFunc: func(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { +// FetchAnalysisValueFunc: func(ctx context.Context, query string, spec metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { // panic("mock out the FetchAnalysisValue method") // }, // } @@ -38,7 +38,7 @@ type KeptnSLIProviderMock struct { EvaluateQueryForStepFunc func(ctx context.Context, metric metricsapi.KeptnMetric, provider metricsapi.KeptnMetricsProvider) ([]string, []byte, error) // FetchAnalysisValueFunc mocks the FetchAnalysisValue method. - FetchAnalysisValueFunc func(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) + FetchAnalysisValueFunc func(ctx context.Context, query string, spec metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) // calls tracks calls to the methods. calls struct { @@ -67,7 +67,7 @@ type KeptnSLIProviderMock struct { // Query is the query argument value. Query string // Spec is the spec argument value. - Spec metricsapi.AnalysisSpec + Spec metricsapi.Analysis // Provider is the provider argument value. Provider *metricsapi.KeptnMetricsProvider } @@ -158,14 +158,14 @@ func (mock *KeptnSLIProviderMock) EvaluateQueryForStepCalls() []struct { } // FetchAnalysisValue calls FetchAnalysisValueFunc. -func (mock *KeptnSLIProviderMock) FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { +func (mock *KeptnSLIProviderMock) FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { if mock.FetchAnalysisValueFunc == nil { panic("KeptnSLIProviderMock.FetchAnalysisValueFunc: method is nil but KeptnSLIProvider.FetchAnalysisValue was just called") } callInfo := struct { Ctx context.Context Query string - Spec metricsapi.AnalysisSpec + Spec metricsapi.Analysis Provider *metricsapi.KeptnMetricsProvider }{ Ctx: ctx, @@ -186,13 +186,13 @@ func (mock *KeptnSLIProviderMock) FetchAnalysisValue(ctx context.Context, query func (mock *KeptnSLIProviderMock) FetchAnalysisValueCalls() []struct { Ctx context.Context Query string - Spec metricsapi.AnalysisSpec + Spec metricsapi.Analysis Provider *metricsapi.KeptnMetricsProvider } { var calls []struct { Ctx context.Context Query string - Spec metricsapi.AnalysisSpec + Spec metricsapi.Analysis Provider *metricsapi.KeptnMetricsProvider } mock.lockFetchAnalysisValue.RLock() diff --git a/metrics-operator/controllers/common/providers/prometheus/prometheus.go b/metrics-operator/controllers/common/providers/prometheus/prometheus.go index 93190a153a..79de2f955f 100644 --- a/metrics-operator/controllers/common/providers/prometheus/prometheus.go +++ b/metrics-operator/controllers/common/providers/prometheus/prometheus.go @@ -23,7 +23,7 @@ type KeptnPrometheusProvider struct { HttpClient http.Client } -func (r *KeptnPrometheusProvider) FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) { +func (r *KeptnPrometheusProvider) FetchAnalysisValue(ctx context.Context, query string, analysis metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) { ctx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() @@ -35,11 +35,11 @@ func (r *KeptnPrometheusProvider) FetchAnalysisValue(ctx context.Context, query r.Log.Info(fmt.Sprintf( "Running query: /api/v1/query_range?query=%s&start=%d&end=%d", query, - spec.From.Unix(), spec.To.Unix(), + analysis.GetFrom().Unix(), analysis.GetTo().Unix(), )) queryRange := prometheus.Range{ - Start: spec.From.Time, - End: spec.To.Time, + Start: analysis.GetFrom(), + End: analysis.GetTo(), Step: time.Minute, } result, warnings, err := api.QueryRange( diff --git a/metrics-operator/controllers/common/providers/prometheus/prometheus_test.go b/metrics-operator/controllers/common/providers/prometheus/prometheus_test.go index 7d527fb385..4c673d7c2b 100644 --- a/metrics-operator/controllers/common/providers/prometheus/prometheus_test.go +++ b/metrics-operator/controllers/common/providers/prometheus/prometheus_test.go @@ -346,21 +346,23 @@ func TestFetchAnalysisValue(t *testing.T) { // Prepare the analysis spec now := time.Now() - analysisSpec := metricsapi.AnalysisSpec{ - Timeframe: metricsapi.Timeframe{ - From: metav1.Time{ - Time: now.Add(-time.Hour), - }, - To: metav1.Time{ - Time: now, - }}, + analysis := metricsapi.Analysis{ + Spec: metricsapi.AnalysisSpec{ + Timeframe: metricsapi.Timeframe{ + From: metav1.Time{ + Time: now.Add(-time.Hour), + }, + To: metav1.Time{ + Time: now, + }}, + }, } // Prepare the expected result expectedResult := "1" // Call the function - result, err := provider.FetchAnalysisValue(context.Background(), "your_query_string_here", analysisSpec, mockProvider) + result, err := provider.FetchAnalysisValue(context.Background(), "your_query_string_here", analysis, mockProvider) // Assertions require.NoError(t, err) diff --git a/metrics-operator/controllers/common/providers/provider.go b/metrics-operator/controllers/common/providers/provider.go index b2e6d549c4..b05289f50f 100644 --- a/metrics-operator/controllers/common/providers/provider.go +++ b/metrics-operator/controllers/common/providers/provider.go @@ -20,7 +20,7 @@ import ( type KeptnSLIProvider interface { EvaluateQuery(ctx context.Context, metric metricsapi.KeptnMetric, provider metricsapi.KeptnMetricsProvider) (string, []byte, error) EvaluateQueryForStep(ctx context.Context, metric metricsapi.KeptnMetric, provider metricsapi.KeptnMetricsProvider) ([]string, []byte, error) - FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.AnalysisSpec, provider *metricsapi.KeptnMetricsProvider) (string, error) + FetchAnalysisValue(ctx context.Context, query string, spec metricsapi.Analysis, provider *metricsapi.KeptnMetricsProvider) (string, error) } type ProviderFactory func(providerType string, log logr.Logger, k8sClient client.Client) (KeptnSLIProvider, error) diff --git a/metrics-operator/main.go b/metrics-operator/main.go index 12621b0bb7..f94cfafa2b 100644 --- a/metrics-operator/main.go +++ b/metrics-operator/main.go @@ -260,6 +260,10 @@ func setupValidationWebhooks(mgr manager.Manager) { setupLog.Error(err, "unable to create webhook", "webhook", "AnalysisDefinition") os.Exit(1) } + if err := (&metricsv1alpha3.Analysis{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Analysis") + os.Exit(1) + } } func setupProbes(mgr manager.Manager) { diff --git a/test/testanalysis/analysis-controller-existing-status/00-install.yaml b/test/testanalysis/analysis-controller-existing-status/00-install.yaml index 5a75c37dc8..1eea1b0750 100644 --- a/test/testanalysis/analysis-controller-existing-status/00-install.yaml +++ b/test/testanalysis/analysis-controller-existing-status/00-install.yaml @@ -45,7 +45,7 @@ metadata: spec: timeframe: from: 2023-09-14T07:33:19Z - to: 2023-09-14T07:33:19Z + to: 2023-09-14T08:33:19Z args: "ns": "keptn-lifecycle-toolkit-system" analysisDefinition: diff --git a/test/testanalysis/analysis-controller-multiple-providers/install.yaml b/test/testanalysis/analysis-controller-multiple-providers/install.yaml index 5ab468af4b..c1daa6a7d4 100644 --- a/test/testanalysis/analysis-controller-multiple-providers/install.yaml +++ b/test/testanalysis/analysis-controller-multiple-providers/install.yaml @@ -73,7 +73,7 @@ metadata: spec: timeframe: from: 2023-09-14T07:33:19Z - to: 2023-09-14T07:33:19Z + to: 2023-09-14T08:33:19Z args: "ns": "keptn-lifecycle-toolkit-system" analysisDefinition: diff --git a/test/testanalysis/analysis-controller-with-duration-timeframe/00-teststep-template.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/00-teststep-template.yaml new file mode 100644 index 0000000000..519913ae4f --- /dev/null +++ b/test/testanalysis/analysis-controller-with-duration-timeframe/00-teststep-template.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - script: | + kubectl apply -f mock-server.yaml -n $NAMESPACE + - script: | + envsubst < install.yaml | kubectl apply -f - -n $NAMESPACE diff --git a/test/testanalysis/analysis-controller-with-duration-timeframe/01-assert.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/01-assert.yaml new file mode 100644 index 0000000000..ea6d053565 --- /dev/null +++ b/test/testanalysis/analysis-controller-with-duration-timeframe/01-assert.yaml @@ -0,0 +1,12 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: Analysis +metadata: + name: analysis-sample +spec: + analysisDefinition: + name: ed-my-proj-dev-svc1 +status: + pass: true + state: Completed + # yamllint disable-line rule:line-length + raw: '{"objectiveResults":[{"result":{"failResult":{"operator":{"lessThan":{"fixedValue":"2"}},"fulfilled":false},"warnResult":{"operator":{"lessThan":{"fixedValue":"3"}},"fulfilled":false},"warning":false,"pass":true},"objective":{"analysisValueTemplateRef":{"name":"ready"},"target":{"failure":{"lessThan":{"fixedValue":"2"}},"warning":{"lessThan":{"fixedValue":"3"}}},"weight":1},"value":4,"score":1}],"totalScore":1,"maximumScore":1,"pass":true,"warning":false}' diff --git a/test/testanalysis/analysis-controller-with-duration-timeframe/install.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/install.yaml new file mode 100644 index 0000000000..59a2fe529b --- /dev/null +++ b/test/testanalysis/analysis-controller-with-duration-timeframe/install.yaml @@ -0,0 +1,49 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: AnalysisValueTemplate +metadata: + name: ready +spec: + provider: + name: my-mocked-provider + query: 'sum(kube_pod_container_status_ready{namespace="{{.ns}}"})' +--- +apiVersion: metrics.keptn.sh/v1alpha3 +kind: AnalysisDefinition +metadata: + name: ed-my-proj-dev-svc1 +spec: + objectives: + - analysisValueTemplateRef: + name: ready + target: + failure: + lessThan: + fixedValue: 2 + warning: + lessThan: + fixedValue: 3 + weight: 1 + keyObjective: false + totalScore: + passPercentage: 90 + warningPercentage: 75 +--- +apiVersion: metrics.keptn.sh/v1alpha3 +kind: Analysis +metadata: + name: analysis-sample +spec: + timeframe: + recent: 5m + args: + "ns": "keptn-lifecycle-toolkit-system" + analysisDefinition: + name: ed-my-proj-dev-svc1 +--- +apiVersion: metrics.keptn.sh/v1alpha3 +kind: KeptnMetricsProvider +metadata: + name: my-mocked-provider +spec: + type: prometheus + targetServer: "http://mockserver.$NAMESPACE.svc.cluster.local:1080" diff --git a/test/testanalysis/analysis-controller-with-duration-timeframe/mock-server.yaml b/test/testanalysis/analysis-controller-with-duration-timeframe/mock-server.yaml new file mode 100644 index 0000000000..b01cda4fb3 --- /dev/null +++ b/test/testanalysis/analysis-controller-with-duration-timeframe/mock-server.yaml @@ -0,0 +1,140 @@ +apiVersion: v1 +kind: Service +metadata: + name: mockserver +spec: + ports: + - name: serviceport + port: 1080 + protocol: TCP + targetPort: serviceport + selector: + app: mockserver + sessionAffinity: None + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: mockserver + name: mockserver +spec: + replicas: 1 + selector: + matchLabels: + app: mockserver + template: + metadata: + labels: + app: mockserver + name: mockserver + spec: + containers: + - env: + - name: MOCKSERVER_LOG_LEVEL + value: INFO + - name: SERVER_PORT + value: "1080" + image: mockserver/mockserver:mockserver-5.13.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 10 + initialDelaySeconds: 10 + periodSeconds: 5 + successThreshold: 1 + tcpSocket: + port: serviceport + timeoutSeconds: 1 + name: mockserver + ports: + - containerPort: 1080 + name: serviceport + protocol: TCP + readinessProbe: + failureThreshold: 10 + initialDelaySeconds: 2 + periodSeconds: 2 + successThreshold: 1 + tcpSocket: + port: serviceport + timeoutSeconds: 1 + volumeMounts: + - mountPath: /config + name: config-volume + - mountPath: /libs + name: libs-volume + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + name: mockserver-config + optional: true + name: config-volume + - configMap: + defaultMode: 420 + name: mockserver-config + optional: true + name: libs-volume +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: mockserver-config +data: + initializerJson.json: |- + [ + { + "httpRequest": { + "path": "/api/v1/query_range", + "method": "POST" + }, + "httpResponse": { + "body": { + "status": "success", + "data": { + "resultType": "matrix", + "result": [ + { + "metric": { + "__name__": "metric-name", + "job": "", + "instance": "" + }, + "values": [[1669714193.275, "4"]] + } + ] + } + }, + "statusCode": 200 + } + } + ] + mockserver.properties: |- + ############################### + # MockServer & Proxy Settings # + ############################### + # Socket & Port Settings + # socket timeout in milliseconds (default 120000) + mockserver.maxSocketTimeout=120000 + # Certificate Generation + # dynamically generated CA key pair (if they don't already exist in + specified directory) + mockserver.dynamicallyCreateCertificateAuthorityCertificate=true + # save dynamically generated CA key pair in working directory + mockserver.directoryToSaveDynamicSSLCertificate=. + # certificate domain name (default "localhost") + mockserver.sslCertificateDomainName=localhost + # comma separated list of ip addresses for Subject Alternative Name domain + names (default empty list) + mockserver.sslSubjectAlternativeNameDomains=www.example.com,www.another.com + # comma separated list of ip addresses for Subject Alternative Name ips + (default empty list) + mockserver.sslSubjectAlternativeNameIps=127.0.0.1 + # CORS + # enable CORS for MockServer REST API + mockserver.enableCORSForAPI=true + # enable CORS for all responses + mockserver.enableCORSForAllResponses=true + # Json Initialization + mockserver.initializationJsonPath=/config/initializerJson.json diff --git a/test/testanalysis/analysis-controller/install.yaml b/test/testanalysis/analysis-controller/install.yaml index 7b1986fcb4..59a2fe529b 100644 --- a/test/testanalysis/analysis-controller/install.yaml +++ b/test/testanalysis/analysis-controller/install.yaml @@ -34,8 +34,7 @@ metadata: name: analysis-sample spec: timeframe: - from: 2023-09-14T07:33:19Z - to: 2023-09-14T07:33:19Z + recent: 5m args: "ns": "keptn-lifecycle-toolkit-system" analysisDefinition: diff --git a/test/testanalysis/analysis-resources/00-teststep-install.yaml b/test/testanalysis/analysis-resources/00-teststep-install.yaml new file mode 100644 index 0000000000..3515a11b6f --- /dev/null +++ b/test/testanalysis/analysis-resources/00-teststep-install.yaml @@ -0,0 +1,12 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - valid-analysis-1.yaml + - valid-analysis-2.yaml +commands: + - command: kubectl apply -f invalid-analysis-1.yaml + ignoreFailure: true # we must install ignoring the validating webhook error to proceed with the test + - command: kubectl apply -f invalid-analysis-2.yaml + ignoreFailure: true # we must install ignoring the validating webhook error to proceed with the test + - command: kubectl apply -f invalid-analysis-3.yaml + ignoreFailure: true # we must install ignoring the validating webhook error to proceed with the test diff --git a/test/testanalysis/analysis-resources/01-teststep-assert.yaml b/test/testanalysis/analysis-resources/01-teststep-assert.yaml new file mode 100644 index 0000000000..150a6a0b8c --- /dev/null +++ b/test/testanalysis/analysis-resources/01-teststep-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +error: # this checks that kubectl get resource fails, AKA bad CRD not added + - invalid-analysis-1.yaml + - invalid-analysis-2.yaml + - invalid-analysis-3.yaml +assert: # this checks that kubectl get resource succeeds + - valid-analysis-1.yaml + - valid-analysis-2.yaml diff --git a/test/testanalysis/analysis/00-assert.yaml b/test/testanalysis/analysis-resources/invalid-analysis-1.yaml similarity index 55% rename from test/testanalysis/analysis/00-assert.yaml rename to test/testanalysis/analysis-resources/invalid-analysis-1.yaml index a58ee8466b..16466b1cd4 100644 --- a/test/testanalysis/analysis/00-assert.yaml +++ b/test/testanalysis/analysis-resources/invalid-analysis-1.yaml @@ -1,15 +1,11 @@ apiVersion: metrics.keptn.sh/v1alpha3 kind: Analysis metadata: - labels: - app.kubernetes.io/name: analysis - app.kubernetes.io/instance: analysis-sample - app.kubernetes.io/part-of: metrics-operator - app.kuberentes.io/managed-by: kustomize - app.kubernetes.io/created-by: metrics-operator - name: analysis-sample + name: invalid-analysis-1 spec: timeframe: + # using 'recent' and 'from'/'to' at the same time + recent: 5m from: 2023-05-05T05:05:05Z to: 2023-05-05T10:10:10Z args: diff --git a/test/testanalysis/analysis-resources/invalid-analysis-2.yaml b/test/testanalysis/analysis-resources/invalid-analysis-2.yaml new file mode 100644 index 0000000000..d5ccdf0b1e --- /dev/null +++ b/test/testanalysis/analysis-resources/invalid-analysis-2.yaml @@ -0,0 +1,15 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: Analysis +metadata: + name: invalid-analysis-2 +spec: + timeframe: + # using invalid 'recent' value + recent: "cinque minuti" + args: + project: my-project + stage: dev + service: svc1 + foo: bar # can be any key/value pair; NOT only project/stage/service + analysisDefinition: + name: ed-my-proj-dev-svc1 diff --git a/test/testanalysis/analysis-resources/invalid-analysis-3.yaml b/test/testanalysis/analysis-resources/invalid-analysis-3.yaml new file mode 100644 index 0000000000..193304fcd9 --- /dev/null +++ b/test/testanalysis/analysis-resources/invalid-analysis-3.yaml @@ -0,0 +1,16 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: Analysis +metadata: + name: invalid-analysis-3 +spec: + timeframe: + # 'from' is before 'to' + to: 2023-05-05T05:05:05Z + from: 2023-05-05T10:10:10Z + args: + project: my-project + stage: dev + service: svc1 + foo: bar # can be any key/value pair; NOT only project/stage/service + analysisDefinition: + name: ed-my-proj-dev-svc1 diff --git a/test/testanalysis/analysis/00-install.yaml b/test/testanalysis/analysis-resources/valid-analysis-1.yaml similarity index 55% rename from test/testanalysis/analysis/00-install.yaml rename to test/testanalysis/analysis-resources/valid-analysis-1.yaml index a58ee8466b..375480e6dc 100644 --- a/test/testanalysis/analysis/00-install.yaml +++ b/test/testanalysis/analysis-resources/valid-analysis-1.yaml @@ -1,15 +1,10 @@ apiVersion: metrics.keptn.sh/v1alpha3 kind: Analysis metadata: - labels: - app.kubernetes.io/name: analysis - app.kubernetes.io/instance: analysis-sample - app.kubernetes.io/part-of: metrics-operator - app.kuberentes.io/managed-by: kustomize - app.kubernetes.io/created-by: metrics-operator - name: analysis-sample + name: valid-analysis-1 spec: timeframe: + # using from/to timestamps from: 2023-05-05T05:05:05Z to: 2023-05-05T10:10:10Z args: diff --git a/test/testanalysis/analysis-resources/valid-analysis-2.yaml b/test/testanalysis/analysis-resources/valid-analysis-2.yaml new file mode 100644 index 0000000000..73513d9f8b --- /dev/null +++ b/test/testanalysis/analysis-resources/valid-analysis-2.yaml @@ -0,0 +1,15 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: Analysis +metadata: + name: valid-analysis-2 +spec: + timeframe: + # using 'recent' + recent: 5m + args: + project: my-project + stage: dev + service: svc1 + foo: bar # can be any key/value pair; NOT only project/stage/service + analysisDefinition: + name: ed-my-proj-dev-svc1