diff --git a/helm/chart/values.yaml b/helm/chart/values.yaml index 17624eb281..4e48f78ead 100644 --- a/helm/chart/values.yaml +++ b/helm/chart/values.yaml @@ -15,7 +15,7 @@ certificateOperator: labelSelectorValue: "true" image: repository: ghcr.io/keptn/certificate-operator - tag: v0.7.1 + tag: v0.8.0 imagePullPolicy: Always livenessProbe: httpGet: @@ -67,7 +67,7 @@ lifecycleOperator: seccompProfile: type: RuntimeDefault env: - functionRunnerImage: ghcr.io/keptn/functions-runtime:v0.7.1 + functionRunnerImage: ghcr.io/keptn/functions-runtime:v0.8.0 keptnAppControllerLogLevel: "0" keptnAppCreationRequestControllerLogLevel: "0" keptnAppVersionControllerLogLevel: "0" @@ -78,10 +78,10 @@ lifecycleOperator: keptnWorkloadInstanceControllerLogLevel: "0" optionsControllerLogLevel: "0" otelCollectorUrl: otel-collector:4317 - pythonRunnerImage: ghcr.io/keptn/python-runtime:v0.0.0 + pythonRunnerImage: ghcr.io/keptn/python-runtime:v0.8.0 image: repository: ghcr.io/keptn/lifecycle-operator - tag: v0.7.1 + tag: v0.8.0 imagePullPolicy: Always livenessProbe: httpGet: @@ -148,7 +148,7 @@ metricsOperator: metricsControllerLogLevel: "0" image: repository: ghcr.io/keptn/metrics-operator - tag: v0.7.1 + tag: v0.8.0 livenessProbe: httpGet: path: /healthz @@ -211,7 +211,7 @@ scheduler: otelCollectorUrl: otel-collector:4317 image: repository: ghcr.io/keptn/scheduler - tag: v0.7.1 + tag: v0.8.0 imagePullPolicy: Always livenessProbe: httpGet: diff --git a/metrics-operator/PROJECT b/metrics-operator/PROJECT index 34eb56af69..35b682db16 100644 --- a/metrics-operator/PROJECT +++ b/metrics-operator/PROJECT @@ -41,6 +41,9 @@ resources: kind: KeptnMetric path: github.com/keptn/lifecycle-toolkit/metrics-operator/api/v1alpha3 version: v1alpha3 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/metrics-operator/api/v1alpha3/keptnmetric_types.go b/metrics-operator/api/v1alpha3/keptnmetric_types.go index 5b4e507609..9135e783b3 100644 --- a/metrics-operator/api/v1alpha3/keptnmetric_types.go +++ b/metrics-operator/api/v1alpha3/keptnmetric_types.go @@ -31,6 +31,8 @@ type KeptnMetricSpec struct { Query string `json:"query"` // FetchIntervalSeconds represents the update frequency in seconds that is used to update the metric FetchIntervalSeconds uint `json:"fetchIntervalSeconds"` + // Range represents the time range for which data is to be queried + Range *RangeSpec `json:"range,omitempty"` } // KeptnMetricStatus defines the observed state of KeptnMetric @@ -51,6 +53,13 @@ type ProviderRef struct { Name string `json:"name"` } +// RangeSpec defines the time range for which data is to be queried +type RangeSpec struct { + // Interval specifies the duration of the time interval for the data query + // +kubebuilder:default:="5m" + Interval string `json:"interval,omitempty"` +} + // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.provider.name` diff --git a/metrics-operator/api/v1alpha3/keptnmetric_webhook.go b/metrics-operator/api/v1alpha3/keptnmetric_webhook.go index 7e99002772..eeaba7df38 100644 --- a/metrics-operator/api/v1alpha3/keptnmetric_webhook.go +++ b/metrics-operator/api/v1alpha3/keptnmetric_webhook.go @@ -17,11 +17,79 @@ limitations under the License. package v1alpha3 import ( + "time" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "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" ) +// log is for logging in this package. +var keptnmetriclog = logf.Log.WithName("keptnmetric-resource") + func (r *KeptnMetric) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() } + +//+kubebuilder:webhook:path=/validate-metrics-keptn-sh-v1alpha3-keptnmetric,mutating=false,failurePolicy=fail,sideEffects=None,groups=metrics.keptn.sh,resources=keptnmetrics,verbs=create;update,versions=v1alpha3,name=vkeptnmetric.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &KeptnMetric{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *KeptnMetric) ValidateCreate() error { + keptnmetriclog.Info("validate create", "name", r.Name) + + return r.validateKeptnMetric() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *KeptnMetric) ValidateUpdate(old runtime.Object) error { + keptnmetriclog.Info("validate update", "name", r.Name) + + return r.validateKeptnMetric() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *KeptnMetric) ValidateDelete() error { + keptnmetriclog.Info("validate delete", "name", r.Name) + + return nil +} + +func (s *KeptnMetric) validateKeptnMetric() error { + var allErrs field.ErrorList //defined as a list to allow returning multiple validation errors + var err *field.Error + if err = s.validateRangeInterval(); err != nil { + allErrs = append(allErrs, err) + } + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + schema.GroupKind{Group: "metrics.keptn.sh", Kind: "KeptnMetric"}, + s.Name, + allErrs) +} + +func (s *KeptnMetric) validateRangeInterval() *field.Error { + if s.Spec.Range == nil { + return nil + } + _, err := time.ParseDuration(s.Spec.Range.Interval) + if err != nil { + return field.Invalid( + field.NewPath("spec").Child("range").Child("interval"), + s.Spec.Range.Interval, + errors.New("Forbidden! The time interval cannot be parsed. Please check for suitable conventions").Error(), + ) + } + return nil +} diff --git a/metrics-operator/api/v1alpha3/keptnmetric_webhook_test.go b/metrics-operator/api/v1alpha3/keptnmetric_webhook_test.go new file mode 100644 index 0000000000..6561dc0f16 --- /dev/null +++ b/metrics-operator/api/v1alpha3/keptnmetric_webhook_test.go @@ -0,0 +1,143 @@ +package v1alpha3 + +import ( + "testing" + + "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestKeptnMetric_validateRangeInterval(t *testing.T) { + + tests := []struct { + name string + verb string + Spec KeptnMetricSpec + want error + oldSpec runtime.Object + }{ + { + name: "create-with-nil-range", + verb: "create", + Spec: KeptnMetricSpec{ + Range: nil, + }, + }, + { + name: "create-with-wrong-interval", + verb: "create", + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: "5mins"}, + }, + want: apierrors.NewInvalid( + schema.GroupKind{Group: "metrics.keptn.sh", Kind: "KeptnMetric"}, + "create-with-wrong-interval", + field.ErrorList{ + field.Invalid( + field.NewPath("spec").Child("range").Child("interval"), + "5mins", + "Forbidden! The time interval cannot be parsed. Please check for suitable conventions", + ), + }, + ), + }, + { + name: "create-with-empty-interval", + verb: "create", + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: ""}, + }, + want: apierrors.NewInvalid( + schema.GroupKind{Group: "metrics.keptn.sh", Kind: "KeptnMetric"}, + "create-with-empty-interval", + field.ErrorList{ + field.Invalid( + field.NewPath("spec").Child("range").Child("interval"), + "", + "Forbidden! The time interval cannot be parsed. Please check for suitable conventions", + ), + }, + ), + }, + { + name: "create-with-right-interval", + verb: "create", + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: "5m"}, + }, + }, + { + name: "update-with-right-interval", + verb: "update", + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: "5m"}, + }, + oldSpec: &KeptnMetric{ + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: "5mins"}, + }, + }, + }, + { + name: "update-with-wrong-interval", + verb: "update", + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: "5mins"}, + }, + want: apierrors.NewInvalid( + schema.GroupKind{Group: "metrics.keptn.sh", Kind: "KeptnMetric"}, + "update-with-wrong-interval", + field.ErrorList{ + field.Invalid( + field.NewPath("spec").Child("range").Child("interval"), + "5mins", + "Forbidden! The time interval cannot be parsed. Please check for suitable conventions", + ), + }, + ), + oldSpec: &KeptnMetric{ + Spec: KeptnMetricSpec{ + Range: &RangeSpec{Interval: "5m"}, + }, + }, + }, + { + name: "delete", + verb: "delete", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var s *KeptnMetric + if tt.Spec.Range == nil { + s = &KeptnMetric{ + ObjectMeta: metav1.ObjectMeta{Name: tt.name}, + Spec: KeptnMetricSpec{Range: tt.Spec.Range}, + } + } else { + s = &KeptnMetric{ + ObjectMeta: metav1.ObjectMeta{Name: tt.name}, + Spec: KeptnMetricSpec{Range: &RangeSpec{Interval: tt.Spec.Range.Interval}}, + } + } + var err error + switch tt.verb { + case "create": + err = s.ValidateCreate() + case "update": + err = s.ValidateUpdate(tt.oldSpec) + case "delete": + err = s.ValidateDelete() + } + if tt.want == nil { + require.Nil(t, err) + } else { + require.Equal(t, tt.want, err) + } + }) + } +} diff --git a/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go b/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go index 9cca08d179..bf6fb02b5e 100644 --- a/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go +++ b/metrics-operator/api/v1alpha3/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ limitations under the License. package v1alpha3 import ( - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -30,7 +30,7 @@ func (in *KeptnMetric) DeepCopyInto(out *KeptnMetric) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -88,6 +88,11 @@ func (in *KeptnMetricList) DeepCopyObject() runtime.Object { func (in *KeptnMetricSpec) DeepCopyInto(out *KeptnMetricSpec) { *out = *in out.Provider = in.Provider + if in.Range != nil { + in, out := &in.Range, &out.Range + *out = new(RangeSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeptnMetricSpec. @@ -225,3 +230,18 @@ func (in *ProviderRef) DeepCopy() *ProviderRef { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RangeSpec) DeepCopyInto(out *RangeSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RangeSpec. +func (in *RangeSpec) DeepCopy() *RangeSpec { + if in == nil { + return nil + } + out := new(RangeSpec) + in.DeepCopyInto(out) + return out +} diff --git a/metrics-operator/config/crd/bases/metrics.keptn.sh_keptnmetrics.yaml b/metrics-operator/config/crd/bases/metrics.keptn.sh_keptnmetrics.yaml index f3eb4d8471..08e9e2155b 100644 --- a/metrics-operator/config/crd/bases/metrics.keptn.sh_keptnmetrics.yaml +++ b/metrics-operator/config/crd/bases/metrics.keptn.sh_keptnmetrics.yaml @@ -212,6 +212,16 @@ spec: query: description: Query represents the query to be run type: string + range: + description: Range represents the time range for which data is to + be queried + properties: + interval: + default: 5m + description: Interval specifies the duration of the time interval + for the data query + type: string + type: object required: - fetchIntervalSeconds - provider diff --git a/metrics-operator/config/default/manager_webhook_patch.yaml b/metrics-operator/config/default/manager_webhook_patch.yaml new file mode 100644 index 0000000000..8a96063a23 --- /dev/null +++ b/metrics-operator/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/metrics-operator/config/webhook/kustomization.yaml b/metrics-operator/config/webhook/kustomization.yaml index 7ed6560ae7..d619b03d1e 100644 --- a/metrics-operator/config/webhook/kustomization.yaml +++ b/metrics-operator/config/webhook/kustomization.yaml @@ -1,5 +1,6 @@ resources: - service.yaml + - manifests.yaml configurations: - kustomizeconfig.yaml diff --git a/metrics-operator/config/webhook/manifests.yaml b/metrics-operator/config/webhook/manifests.yaml new file mode 100644 index 0000000000..f7444c6444 --- /dev/null +++ b/metrics-operator/config/webhook/manifests.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: metrics-validating-webhook-configuration + labels: + keptn.sh/inject-cert: "true" +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: metrics-webhook-service + namespace: system + path: /validate-metrics-keptn-sh-v1alpha3-keptnmetric + failurePolicy: Fail + name: vkeptnmetric.kb.io + rules: + - apiGroups: + - metrics.keptn.sh + apiVersions: + - v1alpha3 + operations: + - CREATE + - UPDATE + resources: + - keptnmetrics + sideEffects: None diff --git a/metrics-operator/go.mod b/metrics-operator/go.mod index 5f8b55e907..5904d1b775 100644 --- a/metrics-operator/go.mod +++ b/metrics-operator/go.mod @@ -9,6 +9,8 @@ require ( github.com/gorilla/mux v1.8.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/keptn/lifecycle-toolkit/klt-cert-manager v0.0.0-20230620112140-7c4d2abcb84e + github.com/onsi/ginkgo/v2 v2.8.1 + github.com/onsi/gomega v1.27.0 github.com/open-feature/go-sdk v1.4.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.15.1 @@ -67,8 +69,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.8.1 // indirect - github.com/onsi/gomega v1.27.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect diff --git a/test/integration/metrics/01-teststep-install.yaml b/test/integration/metrics/01-teststep-install.yaml index a89d76757e..63bd101959 100644 --- a/test/integration/metrics/01-teststep-install.yaml +++ b/test/integration/metrics/01-teststep-install.yaml @@ -4,3 +4,7 @@ apply: - goodmetric1.yaml - goodmetric2.yaml - goodmetric3.yaml + - goodmetric4.yaml +commands: + - command: kubectl apply -f badmetric.yaml + ignoreFailure: true # we must install ignoring the validating webhook error to proceed with the test diff --git a/test/integration/metrics/02-teststep-assert.yaml b/test/integration/metrics/02-teststep-assert.yaml index d5acbc0386..f81d486c75 100644 --- a/test/integration/metrics/02-teststep-assert.yaml +++ b/test/integration/metrics/02-teststep-assert.yaml @@ -1,6 +1,9 @@ apiVersion: kuttl.dev/v1beta1 kind: TestStep +error: # this checks that kubectl get resource fails, AKA bad CRD not added + - badmetric.yaml assert: # this checks that kubectl get resource succeeds - goodmetric1.yaml - goodmetric2.yaml - goodmetric3.yaml + - goodmetric4.yaml diff --git a/test/integration/metrics/03-teststep-retrieve-metrics-via-api.yaml b/test/integration/metrics/03-teststep-retrieve-metrics-via-api.yaml index 1776aabad4..5041c54e85 100644 --- a/test/integration/metrics/03-teststep-retrieve-metrics-via-api.yaml +++ b/test/integration/metrics/03-teststep-retrieve-metrics-via-api.yaml @@ -4,3 +4,4 @@ commands: - script: ./retrieve-metrics.sh "podtato-head1" - script: ./retrieve-metrics.sh "podtato-head2" - script: ./retrieve-metrics.sh "podtato-head3" + - script: ./retrieve-metrics.sh "podtato-head4" diff --git a/test/integration/metrics/badmetric.yaml b/test/integration/metrics/badmetric.yaml new file mode 100644 index 0000000000..75fd967da1 --- /dev/null +++ b/test/integration/metrics/badmetric.yaml @@ -0,0 +1,11 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: KeptnMetric +metadata: + name: podtato-head1 +spec: + provider: + name: "my-provider" + query: "sum(kube_pod_container_resource_limits{resource='cpu'})" + fetchIntervalSeconds: 5 + range: + interval: "5mins" diff --git a/test/integration/metrics/goodmetric1.yaml b/test/integration/metrics/goodmetric1.yaml index d25cf6594a..ff40729810 100644 --- a/test/integration/metrics/goodmetric1.yaml +++ b/test/integration/metrics/goodmetric1.yaml @@ -7,3 +7,5 @@ spec: name: "my-provider" query: "sum(kube_pod_container_resource_limits{resource='cpu'})" fetchIntervalSeconds: 5 + range: + interval: "5m" diff --git a/test/integration/metrics/goodmetric2.yaml b/test/integration/metrics/goodmetric2.yaml index ba40e4d1c0..e7c5a7eda1 100644 --- a/test/integration/metrics/goodmetric2.yaml +++ b/test/integration/metrics/goodmetric2.yaml @@ -7,3 +7,5 @@ spec: name: "prometheus" query: "sum(kube_pod_container_resource_limits{resource='cpu'})" fetchIntervalSeconds: 5 + range: + interval: "5m" diff --git a/test/integration/metrics/goodmetric3.yaml b/test/integration/metrics/goodmetric3.yaml index 5a16a31209..3cc403ff57 100644 --- a/test/integration/metrics/goodmetric3.yaml +++ b/test/integration/metrics/goodmetric3.yaml @@ -7,3 +7,5 @@ spec: name: "my-provider2" query: "sum(kube_pod_container_resource_limits{resource='cpu'})" fetchIntervalSeconds: 5 + range: + interval: "5m" diff --git a/test/integration/metrics/goodmetric4.yaml b/test/integration/metrics/goodmetric4.yaml new file mode 100644 index 0000000000..1e9649e046 --- /dev/null +++ b/test/integration/metrics/goodmetric4.yaml @@ -0,0 +1,9 @@ +apiVersion: metrics.keptn.sh/v1alpha3 +kind: KeptnMetric +metadata: + name: podtato-head4 +spec: + provider: + name: "prometheus" + query: "sum(kube_pod_container_resource_limits{resource='cpu'})" + fetchIntervalSeconds: 5 diff --git a/test/load/assets/templates/metric.yaml b/test/load/assets/templates/metric.yaml index b4574845de..5df16e6065 100644 --- a/test/load/assets/templates/metric.yaml +++ b/test/load/assets/templates/metric.yaml @@ -1,4 +1,4 @@ -apiVersion: metrics.keptn.sh/v1alpha1 +apiVersion: metrics.keptn.sh/v1alpha3 kind: KeptnMetric metadata: name: keptnmetric-load-test-{{.Iteration}}-{{.Replica}} @@ -8,3 +8,5 @@ spec: name: "prometheus" query: "sum(kube_pod_container_resource_limits{resource='cpu'})" fetchIntervalSeconds: 30 + range: + interval: "5m"