From 9baec10e99c342b011ff35836db7fd7c146cf1ff Mon Sep 17 00:00:00 2001 From: Stanislav Khalash Date: Wed, 13 Nov 2024 10:29:58 +0100 Subject: [PATCH] chore: Flatten validating webhook packages (#1607) --- .golangci.yaml | 2 - .mockery.yaml | 5 - internal/utils/test/log_pipeline_builder.go | 41 ++- main.go | 26 +- .../{validation => }/spec_validator.go | 4 +- webhook/logparser/webhook.go | 53 +-- .../{validation => }/files_validator.go | 15 +- webhook/logpipeline/files_validator_test.go | 71 ++++ webhook/logpipeline/handler.go | 96 +++++ webhook/logpipeline/handler_test.go | 127 +++++++ .../logpipeline/max_pipelines_validator.go | 31 ++ .../max_pipelines_validator_test.go | 97 +++++ .../{validation => }/spec_validator.go | 4 +- webhook/logpipeline/spec_validator_test.go | 197 ++++++++++ .../validation/files_validator_test.go | 104 ------ .../validation/max_pipelines_validator.go | 39 -- .../max_pipelines_validator_test.go | 111 ------ .../validation/mocks/files_validator.go | 45 --- .../mocks/max_pipelines_validator.go | 45 --- .../validation/mocks/variables_validator.go | 45 --- .../logpipeline/validation/validator_test.go | 347 ------------------ .../validation/variable_validator_test.go | 103 ------ .../{validation => }/variable_validator.go | 20 +- .../logpipeline/variable_validator_test.go | 63 ++++ webhook/logpipeline/webhook.go | 138 ------- webhook/logpipeline/webhook_test.go | 194 ---------- 26 files changed, 730 insertions(+), 1293 deletions(-) rename webhook/logparser/{validation => }/spec_validator.go (86%) rename webhook/logpipeline/{validation => }/files_validator.go (74%) create mode 100644 webhook/logpipeline/files_validator_test.go create mode 100644 webhook/logpipeline/handler.go create mode 100644 webhook/logpipeline/handler_test.go create mode 100644 webhook/logpipeline/max_pipelines_validator.go create mode 100644 webhook/logpipeline/max_pipelines_validator_test.go rename webhook/logpipeline/{validation => }/spec_validator.go (98%) create mode 100644 webhook/logpipeline/spec_validator_test.go delete mode 100644 webhook/logpipeline/validation/files_validator_test.go delete mode 100644 webhook/logpipeline/validation/max_pipelines_validator.go delete mode 100644 webhook/logpipeline/validation/max_pipelines_validator_test.go delete mode 100644 webhook/logpipeline/validation/mocks/files_validator.go delete mode 100644 webhook/logpipeline/validation/mocks/max_pipelines_validator.go delete mode 100644 webhook/logpipeline/validation/mocks/variables_validator.go delete mode 100644 webhook/logpipeline/validation/validator_test.go delete mode 100644 webhook/logpipeline/validation/variable_validator_test.go rename webhook/logpipeline/{validation => }/variable_validator.go (70%) create mode 100644 webhook/logpipeline/variable_validator_test.go delete mode 100644 webhook/logpipeline/webhook.go delete mode 100644 webhook/logpipeline/webhook_test.go diff --git a/.golangci.yaml b/.golangci.yaml index 6d662c855..9bb189c0e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -72,8 +72,6 @@ linters-settings: alias: logpipelinewebhook - pkg: github.com/kyma-project/telemetry-manager/internal/reconciler/logpipeline/fluentbit alias: logpipelinefluentbit - - pkg: github.com/kyma-project/telemetry-manager/webhook/logpipeline/validation/mocks - alias: logpipelinevalidationmocks - pkg: github.com/prometheus/client_golang/api/prometheus/v1 alias: promv1 - pkg: github.com/prometheus/client_model/go diff --git a/.mockery.yaml b/.mockery.yaml index 970cdbab5..4df24b378 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -38,8 +38,3 @@ packages: github.com/kyma-project/telemetry-manager/internal/selfmonitor/prober: interfaces: alertGetter: - github.com/kyma-project/telemetry-manager/webhook/logpipeline/validation: - interfaces: - FilesValidator: - MaxPipelinesValidator: - VariablesValidator: diff --git a/internal/utils/test/log_pipeline_builder.go b/internal/utils/test/log_pipeline_builder.go index 3b6f6ce7b..ea777aee0 100644 --- a/internal/utils/test/log_pipeline_builder.go +++ b/internal/utils/test/log_pipeline_builder.go @@ -20,14 +20,13 @@ type LogPipelineBuilder struct { finalizers []string deletionTimeStamp metav1.Time - input telemetryv1alpha1.LogPipelineInput - - filters []telemetryv1alpha1.LogPipelineFilter - - httpOutput *telemetryv1alpha1.LogPipelineHTTPOutput - otlpOutput *telemetryv1alpha1.OTLPOutput - customOutput string - + input telemetryv1alpha1.LogPipelineInput + filters []telemetryv1alpha1.LogPipelineFilter + httpOutput *telemetryv1alpha1.LogPipelineHTTPOutput + otlpOutput *telemetryv1alpha1.OTLPOutput + customOutput string + files []telemetryv1alpha1.LogPipelineFileMount + variables []telemetryv1alpha1.LogPipelineVariableRef statusConditions []metav1.Condition } @@ -155,6 +154,30 @@ func (b *LogPipelineBuilder) WithCustomFilter(filter string) *LogPipelineBuilder return b } +func (b *LogPipelineBuilder) WithFile(name, content string) *LogPipelineBuilder { + b.files = append(b.files, telemetryv1alpha1.LogPipelineFileMount{ + Name: name, + Content: content, + }) + + return b +} + +func (b *LogPipelineBuilder) WithVariable(name, secretName, secretNamespace, secretKey string) *LogPipelineBuilder { + b.variables = append(b.variables, telemetryv1alpha1.LogPipelineVariableRef{ + Name: name, + ValueFrom: telemetryv1alpha1.ValueFromSource{ + SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ + Name: secretName, + Namespace: secretNamespace, + Key: secretKey, + }, + }, + }) + + return b +} + func (b *LogPipelineBuilder) WithHTTPOutput(opts ...HTTPOutputOption) *LogPipelineBuilder { b.httpOutput = defaultHTTPOutput() for _, opt := range opts { @@ -216,6 +239,8 @@ func (b *LogPipelineBuilder) Build() telemetryv1alpha1.LogPipeline { Custom: b.customOutput, OTLP: b.otlpOutput, }, + Files: b.files, + Variables: b.variables, }, Status: telemetryv1alpha1.LogPipelineStatus{ Conditions: b.statusConditions, diff --git a/main.go b/main.go index 09b7c8601..952e514b2 100644 --- a/main.go +++ b/main.go @@ -43,7 +43,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" operatorv1alpha1 "github.com/kyma-project/telemetry-manager/apis/operator/v1alpha1" telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" @@ -59,7 +58,6 @@ import ( "github.com/kyma-project/telemetry-manager/internal/webhookcert" logparserwebhook "github.com/kyma-project/telemetry-manager/webhook/logparser" logpipelinewebhook "github.com/kyma-project/telemetry-manager/webhook/logpipeline" - "github.com/kyma-project/telemetry-manager/webhook/logpipeline/validation" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -318,10 +316,10 @@ func run() error { } mgr.GetWebhookServer().Register("/validate-logpipeline", &webhook.Admission{ - Handler: createLogPipelineValidator(mgr.GetClient()), + Handler: logpipelinewebhook.NewValidatingWebhookHandler(mgr.GetClient(), scheme), }) mgr.GetWebhookServer().Register("/validate-logparser", &webhook.Admission{ - Handler: createLogParserValidator(mgr.GetClient()), + Handler: logparserwebhook.NewValidatingWebhookHandler(scheme), }) mgr.GetWebhookServer().Register("/api/v2/alerts", selfmonitorwebhook.NewHandler( mgr.GetClient(), @@ -502,26 +500,6 @@ func setNamespaceFieldSelector() fields.Selector { return fields.SelectorFromSet(fields.Set{"metadata.namespace": telemetryNamespace}) } -func createLogPipelineValidator(client client.Client) *logpipelinewebhook.ValidatingWebhookHandler { - // TODO: Align max log pipeline enforcement with the method used in the TracePipeline/MetricPipeline controllers, - // replacing the current validating webhook approach. - const maxLogPipelines = 5 - - return logpipelinewebhook.NewValidatingWebhookHandler( - client, - validation.NewVariablesValidator(client), - validation.NewMaxPipelinesValidator(maxLogPipelines), - validation.NewFilesValidator(), - admission.NewDecoder(scheme), - ) -} - -func createLogParserValidator(client client.Client) *logparserwebhook.ValidatingWebhookHandler { - return logparserwebhook.NewValidatingWebhookHandler( - client, - admission.NewDecoder(scheme)) -} - func createSelfMonitoringConfig() telemetry.SelfMonitorConfig { return telemetry.SelfMonitorConfig{ Config: selfmonitor.Config{ diff --git a/webhook/logparser/validation/spec_validator.go b/webhook/logparser/spec_validator.go similarity index 86% rename from webhook/logparser/validation/spec_validator.go rename to webhook/logparser/spec_validator.go index 12e043145..3ba6d346c 100644 --- a/webhook/logparser/validation/spec_validator.go +++ b/webhook/logparser/spec_validator.go @@ -1,4 +1,4 @@ -package validation +package logparser import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/kyma-project/telemetry-manager/internal/fluentbit/config" ) -func ValidateSpec(lp *telemetryv1alpha1.LogParser) error { +func validateSpec(lp *telemetryv1alpha1.LogParser) error { if len(lp.Spec.Parser) == 0 { return fmt.Errorf("log parser '%s' has no parser defined", lp.Name) } diff --git a/webhook/logparser/webhook.go b/webhook/logparser/webhook.go index b7b9f529e..445feee0f 100644 --- a/webhook/logparser/webhook.go +++ b/webhook/logparser/webhook.go @@ -1,47 +1,23 @@ -/* -Copyright 2021. - -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 logparser import ( "context" "net/http" - admissionv1 "k8s.io/api/admission/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - "github.com/kyma-project/telemetry-manager/webhook/logparser/validation" - logpipelinewebhook "github.com/kyma-project/telemetry-manager/webhook/logpipeline" ) -// +kubebuilder:webhook:path=/validate-logparser,mutating=false,failurePolicy=fail,sideEffects=None,groups=telemetry.kyma-project.io,resources=logparsers,verbs=create;update,versions=v1alpha1,name=vlogparser.kb.io,admissionReviewVersions=v1 type ValidatingWebhookHandler struct { - client.Client decoder admission.Decoder } -// TODO: Merge validation package with the webhook package, avoid useless dependency injection -func NewValidatingWebhookHandler(client client.Client, decoder admission.Decoder) *ValidatingWebhookHandler { +func NewValidatingWebhookHandler(scheme *runtime.Scheme) *ValidatingWebhookHandler { return &ValidatingWebhookHandler{ - Client: client, - decoder: decoder, + decoder: admission.NewDecoder(scheme), } } @@ -54,29 +30,10 @@ func (v *ValidatingWebhookHandler) Handle(ctx context.Context, req admission.Req return admission.Errored(http.StatusBadRequest, err) } - if err := v.validateLogParser(logParser); err != nil { + if err := validateSpec(logParser); err != nil { log.Error(err, "LogParser rejected") - - return admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Reason: logpipelinewebhook.StatusReasonConfigurationError, - Message: err.Error(), - }, - }, - } + return admission.Errored(http.StatusBadRequest, err) } return admission.Allowed("LogParser validation successful") } - -func (v *ValidatingWebhookHandler) validateLogParser(logParser *telemetryv1alpha1.LogParser) error { - err := validation.ValidateSpec(logParser) - if err != nil { - return err - } - - return nil -} diff --git a/webhook/logpipeline/validation/files_validator.go b/webhook/logpipeline/files_validator.go similarity index 74% rename from webhook/logpipeline/validation/files_validator.go rename to webhook/logpipeline/files_validator.go index d83e06c25..42c2484a5 100644 --- a/webhook/logpipeline/validation/files_validator.go +++ b/webhook/logpipeline/files_validator.go @@ -1,4 +1,4 @@ -package validation +package logpipeline import ( "fmt" @@ -6,18 +6,7 @@ import ( telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" ) -type FilesValidator interface { - Validate(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error -} - -type filesValidator struct { -} - -func NewFilesValidator() FilesValidator { - return &filesValidator{} -} - -func (f *filesValidator) Validate(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error { +func validateFiles(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error { err := validateUniqueFileName(logPipeline, logPipelines) if err != nil { return err diff --git a/webhook/logpipeline/files_validator_test.go b/webhook/logpipeline/files_validator_test.go new file mode 100644 index 000000000..3325376b8 --- /dev/null +++ b/webhook/logpipeline/files_validator_test.go @@ -0,0 +1,71 @@ +package logpipeline + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" + testutils "github.com/kyma-project/telemetry-manager/internal/utils/test" +) + +func TestDuplicateFileName(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + existingPipeline := testutils.NewLogPipelineBuilder().WithName("foo").WithFile("f1.json", "").Build() + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&existingPipeline).Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder().WithName("bar").WithFile("f1.json", "").Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.False(t, response.Allowed) + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Equal(t, response.Result.Message, "filename 'f1.json' is already being used in the logPipeline 'foo'") +} + +func TestDuplicateFileNameInSamePipeline(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder().WithName("foo").WithFile("f1.json", "").WithFile("f1.json", "").Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.False(t, response.Allowed) + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Equal(t, response.Result.Message, "duplicate file names detected please review your pipeline") +} + +func TestValidateUpdatePipeline(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + existingPipeline := testutils.NewLogPipelineBuilder().WithName("foo").WithFile("f1.json", "").Build() + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&existingPipeline).Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder().WithName("foo").WithFile("f1.json", "").Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.True(t, response.Allowed) +} diff --git a/webhook/logpipeline/handler.go b/webhook/logpipeline/handler.go new file mode 100644 index 000000000..dfb96de61 --- /dev/null +++ b/webhook/logpipeline/handler.go @@ -0,0 +1,96 @@ +package logpipeline + +import ( + "context" + "fmt" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" + logpipelineutils "github.com/kyma-project/telemetry-manager/internal/utils/logpipeline" +) + +type ValidatingWebhookHandler struct { + client client.Client + decoder admission.Decoder +} + +func NewValidatingWebhookHandler( + client client.Client, + scheme *runtime.Scheme, +) *ValidatingWebhookHandler { + return &ValidatingWebhookHandler{ + client: client, + decoder: admission.NewDecoder(scheme), + } +} + +func (h *ValidatingWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + log := logf.FromContext(ctx) + + logPipeline := &telemetryv1alpha1.LogPipeline{} + if err := h.decoder.Decode(req, logPipeline); err != nil { + log.Error(err, "Failed to decode LogPipeline") + return admission.Errored(http.StatusBadRequest, err) + } + + if err := h.validateLogPipeline(ctx, logPipeline); err != nil { + log.Error(err, "LogPipeline rejected") + return admission.Errored(http.StatusBadRequest, err) + } + + var warnMsg []string + + if logpipelineutils.ContainsCustomPlugin(logPipeline) { + helpText := "https://kyma-project.io/#/telemetry-manager/user/02-logs" + msg := fmt.Sprintf("Logpipeline '%s' uses unsupported custom filters or outputs. We recommend changing the pipeline to use supported filters or output. See the documentation: %s", logPipeline.Name, helpText) + warnMsg = append(warnMsg, msg) + } + + if len(warnMsg) != 0 { + return admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: true, + Warnings: warnMsg, + }, + } + } + + return admission.Allowed("LogPipeline validation successful") +} + +func (h *ValidatingWebhookHandler) validateLogPipeline(ctx context.Context, logPipeline *telemetryv1alpha1.LogPipeline) error { + log := logf.FromContext(ctx) + + var logPipelines telemetryv1alpha1.LogPipelineList + if err := h.client.List(ctx, &logPipelines); err != nil { + return err + } + + if err := validatePipelineLimit(logPipeline, &logPipelines); err != nil { + log.Error(err, "Maximum number of log pipelines reached") + return err + } + + if err := validateSpec(logPipeline); err != nil { + log.Error(err, "Log pipeline spec validation failed") + return err + } + + if err := validateVariables(logPipeline, &logPipelines); err != nil { + log.Error(err, "Log pipeline variable validation failed") + return err + } + + if err := validateFiles(logPipeline, &logPipelines); err != nil { + log.Error(err, "Log pipeline file validation failed") + return err + } + + return nil +} diff --git a/webhook/logpipeline/handler_test.go b/webhook/logpipeline/handler_test.go new file mode 100644 index 000000000..1df1fca88 --- /dev/null +++ b/webhook/logpipeline/handler_test.go @@ -0,0 +1,127 @@ +package logpipeline + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" + "github.com/kyma-project/telemetry-manager/internal/featureflags" + testutils "github.com/kyma-project/telemetry-manager/internal/utils/test" +) + +func TestHandle(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + t.Run("should return a warning when a custom plugin is used", func(t *testing.T) { + logPipeline := testutils.NewLogPipelineBuilder().WithName("custom-output").WithCustomOutput("Name stdout").Build() + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + response := sut.Handle(context.Background(), admissionRequestFrom(t, logPipeline)) + + require.True(t, response.Allowed) + require.Contains(t, response.Warnings, "Logpipeline 'custom-output' uses unsupported custom filters or outputs. We recommend changing the pipeline to use supported filters or output. See the documentation: https://kyma-project.io/#/telemetry-manager/user/02-logs") + }) + + t.Run("should validate OTLP input based on output", func(t *testing.T) { + type args struct { + name string + output *telemetryv1alpha1.LogPipelineOutput + input *telemetryv1alpha1.LogPipelineInput + allowed bool + message string + } + + tests := []args{ + { + name: "otlp-input-and-output", + output: &telemetryv1alpha1.LogPipelineOutput{ + Custom: "", + HTTP: nil, + OTLP: &telemetryv1alpha1.OTLPOutput{ + Protocol: "grpc", + Endpoint: telemetryv1alpha1.ValueType{Value: ""}, + TLS: &telemetryv1alpha1.OTLPTLS{ + Insecure: true, + }, + }, + }, + input: &telemetryv1alpha1.LogPipelineInput{ + OTLP: &telemetryv1alpha1.OTLPInput{}, + }, + allowed: true, + }, + { + name: "otlp-input-and-fluentbit-output", + output: &telemetryv1alpha1.LogPipelineOutput{ + Custom: "", + HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ + Host: telemetryv1alpha1.ValueType{Value: "127.0.0.1"}, + Port: "8080", + URI: "/", + Format: "json", + TLS: telemetryv1alpha1.LogPipelineOutputTLS{ + Disabled: true, + SkipCertificateValidation: true, + }, + }, + }, + input: &telemetryv1alpha1.LogPipelineInput{ + OTLP: &telemetryv1alpha1.OTLPInput{}, + }, + allowed: false, + message: "invalid log pipeline definition: cannot use OTLP input for pipeline in FluentBit mode", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + featureflags.Enable(featureflags.LogPipelineOTLP) + defer featureflags.Disable(featureflags.LogPipelineOTLP) + + logPipeline := testutils.NewLogPipelineBuilder().Build() + logPipeline.Spec.Output = *tt.output + logPipeline.Spec.Input = *tt.input + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + response := sut.Handle(context.Background(), admissionRequestFrom(t, logPipeline)) + require.Equal(t, tt.allowed, response.Allowed) + + if !tt.allowed { + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Contains(t, response.Result.Message, tt.message) + } + }) + } + }) +} + +func admissionRequestFrom(t *testing.T, logPipeline telemetryv1alpha1.LogPipeline) admission.Request { + t.Helper() + + pipelineJSON, err := json.Marshal(logPipeline) + if err != nil { + t.Fatalf("failed to marshal log pipeline: %v", err) + } + + return admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{Raw: pipelineJSON}, + }, + } +} diff --git a/webhook/logpipeline/max_pipelines_validator.go b/webhook/logpipeline/max_pipelines_validator.go new file mode 100644 index 000000000..4fb34e1ed --- /dev/null +++ b/webhook/logpipeline/max_pipelines_validator.go @@ -0,0 +1,31 @@ +package logpipeline + +import ( + "fmt" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" +) + +const ( + maxPipelines = 5 +) + +// TODO: Align max log pipeline enforcement with the method used in the TracePipeline/MetricPipeline controllers, +// replacing the current validating webhook approach. +func validatePipelineLimit(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error { + if isNewPipeline(logPipeline, logPipelines) && len(logPipelines.Items) >= maxPipelines { + return fmt.Errorf("the maximum number of log pipelines is %d", maxPipelines) + } + + return nil +} + +func isNewPipeline(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) bool { + for _, pipeline := range logPipelines.Items { + if pipeline.Name == logPipeline.Name { + return false + } + } + + return true +} diff --git a/webhook/logpipeline/max_pipelines_validator_test.go b/webhook/logpipeline/max_pipelines_validator_test.go new file mode 100644 index 000000000..530648d71 --- /dev/null +++ b/webhook/logpipeline/max_pipelines_validator_test.go @@ -0,0 +1,97 @@ +package logpipeline + +import ( + "context" + "net/http" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" + testutils "github.com/kyma-project/telemetry-manager/internal/utils/test" +) + +func TestValidateFirstPipeline(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder().Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.True(t, response.Allowed) +} + +func TestValidateLimitNotExceeded(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + var existingPipelines []client.Object + + for i := range 4 { + p := testutils.NewLogPipelineBuilder().WithName(strconv.Itoa(i)).Build() + existingPipelines = append(existingPipelines, &p) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingPipelines...).Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder().Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.True(t, response.Allowed) +} + +func TestValidateLimitExceeded(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + var existingPipelines []client.Object + + for i := range 5 { + p := testutils.NewLogPipelineBuilder().WithName(strconv.Itoa(i)).Build() + existingPipelines = append(existingPipelines, &p) + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingPipelines...).Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder().Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.False(t, response.Allowed) + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Equal(t, response.Result.Message, "the maximum number of log pipelines is 5") +} + +func TestValidateUpdate(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + existingPipeline := testutils.NewLogPipelineBuilder().WithName("foo").Build() + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&existingPipeline).Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + response := sut.Handle(context.Background(), admissionRequestFrom(t, existingPipeline)) + + require.True(t, response.Allowed) +} diff --git a/webhook/logpipeline/validation/spec_validator.go b/webhook/logpipeline/spec_validator.go similarity index 98% rename from webhook/logpipeline/validation/spec_validator.go rename to webhook/logpipeline/spec_validator.go index f53f6b394..b9d8fb9a9 100644 --- a/webhook/logpipeline/validation/spec_validator.go +++ b/webhook/logpipeline/spec_validator.go @@ -1,4 +1,4 @@ -package validation +package logpipeline import ( "errors" @@ -17,7 +17,7 @@ var ( ErrInvalidPipelineDefinition = errors.New("invalid log pipeline definition") ) -func ValidateSpec(lp *telemetryv1alpha1.LogPipeline) error { +func validateSpec(lp *telemetryv1alpha1.LogPipeline) error { if err := validateOutput(lp); err != nil { return err } diff --git a/webhook/logpipeline/spec_validator_test.go b/webhook/logpipeline/spec_validator_test.go new file mode 100644 index 000000000..f86d18141 --- /dev/null +++ b/webhook/logpipeline/spec_validator_test.go @@ -0,0 +1,197 @@ +package logpipeline + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" +) + +func TestValidateLogPipelineSpec(t *testing.T) { + tests := []struct { + name string + logPipeline *telemetryv1alpha1.LogPipeline + expectError bool + errorMessage string + }{ + // Output validation cases + { + name: "no output defined", + logPipeline: &telemetryv1alpha1.LogPipeline{}, + expectError: true, + errorMessage: "no output plugin is defined, you must define one output plugin", + }, + { + name: "multiple outputs defined", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.LogPipelineOutput{ + Custom: `Name http`, + HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ + Host: telemetryv1alpha1.ValueType{Value: "localhost"}, + }, + }, + }, + }, + expectError: true, + errorMessage: "multiple output plugins are defined, you must define only one output plugin", + }, + { + name: "valid custom output", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.LogPipelineOutput{Custom: "name http"}, + }, + }, + expectError: false, + }, + { + name: "custom output with forbidden parameter", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.LogPipelineOutput{ + Custom: ` + name http + storage.total_limit_size 10G`, + }, + }, + }, + expectError: true, + errorMessage: "output plugin 'http' contains forbidden configuration key 'storage.total_limit_size'", + }, + { + name: "custom output missing name", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.LogPipelineOutput{Custom: "Regex .*"}, + }, + }, + expectError: true, + errorMessage: "configuration section must have name attribute", + }, + { + name: "both value and valueFrom in HTTP output host", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.LogPipelineOutput{ + HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ + Host: telemetryv1alpha1.ValueType{ + Value: "localhost", + ValueFrom: &telemetryv1alpha1.ValueFromSource{ + SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ + Name: "foo", + Namespace: "foo-ns", + Key: "foo-key", + }, + }, + }, + }, + }, + }, + }, + expectError: true, + errorMessage: "http output host must have either a value or secret key reference", + }, + { + name: "valid HTTP output with ValueFrom", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Output: telemetryv1alpha1.LogPipelineOutput{ + HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ + Host: telemetryv1alpha1.ValueType{ + ValueFrom: &telemetryv1alpha1.ValueFromSource{ + SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ + Name: "foo", + Namespace: "foo-ns", + Key: "foo-key", + }, + }, + }, + }, + }, + }, + }, + expectError: false, + }, + // Filter validation cases + { + name: "valid custom filter", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Filters: []telemetryv1alpha1.LogPipelineFilter{ + {Custom: "Name grep"}, + }, + Output: telemetryv1alpha1.LogPipelineOutput{Custom: "Name http"}, + }, + }, + expectError: false, + }, + { + name: "custom filter without name", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Filters: []telemetryv1alpha1.LogPipelineFilter{ + {Custom: "foo bar"}, + }, + Output: telemetryv1alpha1.LogPipelineOutput{Custom: "Name http"}, + }, + }, + expectError: true, + errorMessage: "configuration section must have name attribute", + }, + { + name: "custom filter with forbidden match condition", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Filters: []telemetryv1alpha1.LogPipelineFilter{ + {Custom: "Name grep\nMatch *"}, + }, + Output: telemetryv1alpha1.LogPipelineOutput{Custom: "Name http"}, + }, + }, + expectError: true, + errorMessage: "filter plugin 'grep' contains match condition. Match conditions are forbidden", + }, + { + name: "denied filter plugin", + logPipeline: &telemetryv1alpha1.LogPipeline{ + Spec: telemetryv1alpha1.LogPipelineSpec{ + Filters: []telemetryv1alpha1.LogPipelineFilter{ + {Custom: "Name kubernetes"}, + }, + Output: telemetryv1alpha1.LogPipelineOutput{Custom: "Name http"}, + }, + }, + expectError: true, + errorMessage: "filter plugin 'kubernetes' is forbidden. ", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + response := sut.Handle(context.Background(), admissionRequestFrom(t, *tt.logPipeline)) + + if tt.expectError { + require.False(t, response.Allowed) + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Equal(t, response.Result.Message, tt.errorMessage) + } else { + require.True(t, response.Allowed) + } + }) + } +} diff --git a/webhook/logpipeline/validation/files_validator_test.go b/webhook/logpipeline/validation/files_validator_test.go deleted file mode 100644 index 4596e6d3e..000000000 --- a/webhook/logpipeline/validation/files_validator_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package validation - -import ( - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" -) - -func TestDuplicateFileName(t *testing.T) { - l1 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Files: []telemetryv1alpha1.LogPipelineFileMount{{ - Name: "f1.json", - Content: "", - }, - }, - }, - } - - pipeLineList := telemetryv1alpha1.LogPipelineList{} - pipeLineList.Items = []telemetryv1alpha1.LogPipeline{l1} - - l2 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "bar", - }, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Files: []telemetryv1alpha1.LogPipelineFileMount{{ - Name: "f1.json", - Content: "", - }, - }, - }, - } - f := NewFilesValidator() - err := f.Validate(&l2, &pipeLineList) - require.Error(t, err) - require.Equal(t, "filename 'f1.json' is already being used in the logPipeline 'foo'", err.Error()) -} - -func TestDuplicateFileNameInSamePipeline(t *testing.T) { - l1 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Files: []telemetryv1alpha1.LogPipelineFileMount{{ - Name: "f1.json", - Content: "", - }, { - Name: "f1.json", - Content: "", - }, - }, - }, - } - - pipeLineList := telemetryv1alpha1.LogPipelineList{} - - f := NewFilesValidator() - err := f.Validate(&l1, &pipeLineList) - require.Error(t, err) - require.Equal(t, "duplicate file names detected please review your pipeline", err.Error()) -} - -func TestValidateUpdatePipeline(t *testing.T) { - l1 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Files: []telemetryv1alpha1.LogPipelineFileMount{{ - Name: "f1.json", - Content: "", - }, - }, - }, - } - - pipeLineList := telemetryv1alpha1.LogPipelineList{} - pipeLineList.Items = []telemetryv1alpha1.LogPipeline{l1} - - l2 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Files: []telemetryv1alpha1.LogPipelineFileMount{{ - Name: "f1.json", - Content: "", - }, - }, - }, - } - f := NewFilesValidator() - err := f.Validate(&l2, &pipeLineList) - require.NoError(t, err) -} diff --git a/webhook/logpipeline/validation/max_pipelines_validator.go b/webhook/logpipeline/validation/max_pipelines_validator.go deleted file mode 100644 index 0d6d2c7b1..000000000 --- a/webhook/logpipeline/validation/max_pipelines_validator.go +++ /dev/null @@ -1,39 +0,0 @@ -package validation - -import ( - "fmt" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" -) - -type MaxPipelinesValidator interface { - Validate(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error -} - -type maxPipelinesValidator struct { - maxPipelines int -} - -func NewMaxPipelinesValidator(maxPipelines int) MaxPipelinesValidator { - return &maxPipelinesValidator{ - maxPipelines: maxPipelines, - } -} - -func (m maxPipelinesValidator) Validate(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error { - if m.maxPipelines > 0 && m.isNewPipeline(logPipeline, logPipelines) && len(logPipelines.Items) >= m.maxPipelines { - return fmt.Errorf("the maximum number of log pipelines is %d", m.maxPipelines) - } - - return nil -} - -func (maxPipelinesValidator) isNewPipeline(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) bool { - for _, pipeline := range logPipelines.Items { - if pipeline.Name == logPipeline.Name { - return false - } - } - - return true -} diff --git a/webhook/logpipeline/validation/max_pipelines_validator_test.go b/webhook/logpipeline/validation/max_pipelines_validator_test.go deleted file mode 100644 index 28cd13aae..000000000 --- a/webhook/logpipeline/validation/max_pipelines_validator_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package validation - -import ( - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" -) - -func TestValidateWithoutLimit(t *testing.T) { - validator := NewMaxPipelinesValidator(0) - pipeline := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-1", - }, - } - pipelines := telemetryv1alpha1.LogPipelineList{} - - err := validator.Validate(&pipeline, &pipelines) - require.NoError(t, err) -} - -func TestValidateFirstPipeline(t *testing.T) { - validator := NewMaxPipelinesValidator(1) - pipeline := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-1", - }, - } - pipelines := telemetryv1alpha1.LogPipelineList{} - - err := validator.Validate(&pipeline, &pipelines) - require.NoError(t, err) -} - -func TestValidateUpdate(t *testing.T) { - validator := NewMaxPipelinesValidator(1) - pipeline := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-1", - }, - } - pipelines := telemetryv1alpha1.LogPipelineList{} - pipelines.Items = append(pipelines.Items, pipeline) - - err := validator.Validate(&pipeline, &pipelines) - require.NoError(t, err) -} - -func TestValidateSecondPipeline(t *testing.T) { - validator := NewMaxPipelinesValidator(2) - pipeline1 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-1", - }, - } - pipeline2 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-2", - }, - } - - pipelines := telemetryv1alpha1.LogPipelineList{} - pipelines.Items = append(pipelines.Items, pipeline1) - - err := validator.Validate(&pipeline2, &pipelines) - require.NoError(t, err) -} - -func TestValidateLimitExceeded(t *testing.T) { - validator := NewMaxPipelinesValidator(1) - pipeline1 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-1", - }, - } - pipeline2 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-2", - }, - } - - pipelines := telemetryv1alpha1.LogPipelineList{} - pipelines.Items = append(pipelines.Items, pipeline1) - - err := validator.Validate(&pipeline2, &pipelines) - require.Error(t, err) -} - -func TestIsNewPipeline(t *testing.T) { - var validator maxPipelinesValidator - - pipeline1 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-1", - }, - } - pipeline2 := telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipeline-2", - }, - } - - pipelines := telemetryv1alpha1.LogPipelineList{} - pipelines.Items = append(pipelines.Items, pipeline1) - - require.True(t, validator.isNewPipeline(&pipeline2, &pipelines)) - require.False(t, validator.isNewPipeline(&pipeline1, &pipelines)) -} diff --git a/webhook/logpipeline/validation/mocks/files_validator.go b/webhook/logpipeline/validation/mocks/files_validator.go deleted file mode 100644 index e91214281..000000000 --- a/webhook/logpipeline/validation/mocks/files_validator.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package mocks - -import ( - v1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - mock "github.com/stretchr/testify/mock" -) - -// FilesValidator is an autogenerated mock type for the FilesValidator type -type FilesValidator struct { - mock.Mock -} - -// Validate provides a mock function with given fields: logPipeline, logPipelines -func (_m *FilesValidator) Validate(logPipeline *v1alpha1.LogPipeline, logPipelines *v1alpha1.LogPipelineList) error { - ret := _m.Called(logPipeline, logPipelines) - - if len(ret) == 0 { - panic("no return value specified for Validate") - } - - var r0 error - if rf, ok := ret.Get(0).(func(*v1alpha1.LogPipeline, *v1alpha1.LogPipelineList) error); ok { - r0 = rf(logPipeline, logPipelines) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewFilesValidator creates a new instance of FilesValidator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewFilesValidator(t interface { - mock.TestingT - Cleanup(func()) -}) *FilesValidator { - mock := &FilesValidator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/webhook/logpipeline/validation/mocks/max_pipelines_validator.go b/webhook/logpipeline/validation/mocks/max_pipelines_validator.go deleted file mode 100644 index 2ab516cd4..000000000 --- a/webhook/logpipeline/validation/mocks/max_pipelines_validator.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package mocks - -import ( - v1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - mock "github.com/stretchr/testify/mock" -) - -// MaxPipelinesValidator is an autogenerated mock type for the MaxPipelinesValidator type -type MaxPipelinesValidator struct { - mock.Mock -} - -// Validate provides a mock function with given fields: logPipeline, logPipelines -func (_m *MaxPipelinesValidator) Validate(logPipeline *v1alpha1.LogPipeline, logPipelines *v1alpha1.LogPipelineList) error { - ret := _m.Called(logPipeline, logPipelines) - - if len(ret) == 0 { - panic("no return value specified for Validate") - } - - var r0 error - if rf, ok := ret.Get(0).(func(*v1alpha1.LogPipeline, *v1alpha1.LogPipelineList) error); ok { - r0 = rf(logPipeline, logPipelines) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewMaxPipelinesValidator creates a new instance of MaxPipelinesValidator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMaxPipelinesValidator(t interface { - mock.TestingT - Cleanup(func()) -}) *MaxPipelinesValidator { - mock := &MaxPipelinesValidator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/webhook/logpipeline/validation/mocks/variables_validator.go b/webhook/logpipeline/validation/mocks/variables_validator.go deleted file mode 100644 index fbf483de2..000000000 --- a/webhook/logpipeline/validation/mocks/variables_validator.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package mocks - -import ( - v1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - mock "github.com/stretchr/testify/mock" -) - -// VariablesValidator is an autogenerated mock type for the VariablesValidator type -type VariablesValidator struct { - mock.Mock -} - -// Validate provides a mock function with given fields: logPipeline, logPipelines -func (_m *VariablesValidator) Validate(logPipeline *v1alpha1.LogPipeline, logPipelines *v1alpha1.LogPipelineList) error { - ret := _m.Called(logPipeline, logPipelines) - - if len(ret) == 0 { - panic("no return value specified for Validate") - } - - var r0 error - if rf, ok := ret.Get(0).(func(*v1alpha1.LogPipeline, *v1alpha1.LogPipelineList) error); ok { - r0 = rf(logPipeline, logPipelines) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewVariablesValidator creates a new instance of VariablesValidator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewVariablesValidator(t interface { - mock.TestingT - Cleanup(func()) -}) *VariablesValidator { - mock := &VariablesValidator{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/webhook/logpipeline/validation/validator_test.go b/webhook/logpipeline/validation/validator_test.go deleted file mode 100644 index b83fabee1..000000000 --- a/webhook/logpipeline/validation/validator_test.go +++ /dev/null @@ -1,347 +0,0 @@ -package validation - -import ( - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" -) - -func TestContainsNoOutputPlugins(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{}, - }} - - result := validateOutput(logPipeline) - - require.Error(t, result) - require.Contains(t, result.Error(), "no output plugin is defined, you must define one output plugin") -} - -func TestContainsMultipleOutputPlugins(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - Custom: `Name http`, - HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ - Host: telemetryv1alpha1.ValueType{ - Value: "localhost", - }, - }, - }, - }} - result := validateOutput(logPipeline) - - require.Error(t, result) - require.Contains(t, result.Error(), "multiple output plugins are defined, you must define only one output") -} - -func TestValidateCustomOutput(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - Custom: ` - name http`, - }, - }, - } - - err := validateOutput(logPipeline) - require.NoError(t, err) -} - -func TestValidateCustomHasForbiddenParameter(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - Custom: ` - name http - storage.total_limit_size 10G`, - }, - }, - } - - err := validateOutput(logPipeline) - require.Error(t, err) -} - -func TestValidateCustomOutputsContainsNoName(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - Custom: ` - Regex .*`, - }, - }, - } - - err := validateOutput(logPipeline) - - require.Error(t, err) - require.Contains(t, err.Error(), "configuration section must have name attribute") -} - -func TestBothValueAndValueFromPresent(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ - Host: telemetryv1alpha1.ValueType{ - Value: "localhost", - ValueFrom: &telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "foo", - Namespace: "foo-ns", - Key: "foo-key", - }, - }, - }, - }, - }, - }} - err := validateOutput(logPipeline) - require.Error(t, err) - require.Contains(t, err.Error(), "http output host must have either a value or secret key reference") -} - -func TestValueFromSecretKeyRef(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ - Host: telemetryv1alpha1.ValueType{ - ValueFrom: &telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "foo", - Namespace: "foo-ns", - Key: "foo-key", - }, - }, - }, - }, - }, - }} - err := validateOutput(logPipeline) - require.NoError(t, err) -} - -func TestValidateCustomFilter(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Output: telemetryv1alpha1.LogPipelineOutput{ - Custom: ` - Name http`, - }, - }, - } - - err := validateFilters(logPipeline) - require.NoError(t, err) -} - -func TestValidateCustomFiltersContainsNoName(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Filters: []telemetryv1alpha1.LogPipelineFilter{ - {Custom: ` - Match *`, - }, - }, - }, - } - - err := validateFilters(logPipeline) - require.Error(t, err) - require.Contains(t, err.Error(), "configuration section must have name attribute") -} - -func TestValidateCustomFiltersContainsMatch(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Filters: []telemetryv1alpha1.LogPipelineFilter{ - {Custom: ` - Name grep - Match *`, - }, - }, - }, - } - - err := validateFilters(logPipeline) - - require.Error(t, err) - require.Contains(t, err.Error(), "plugin 'grep' contains match condition. Match conditions are forbidden") -} - -func TestDeniedFilterPlugins(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "foo"}, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Filters: []telemetryv1alpha1.LogPipelineFilter{ - {Custom: ` - Name kubernetes`, - }, - }, - }, - } - - err := validateFilters(logPipeline) - - require.Error(t, err) - require.Contains(t, err.Error(), "plugin 'kubernetes' is forbidden. ") -} - -func TestValidateWithValidInputIncludes(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - Include: []string{"namespace-1", "namespace-2"}, - }, - Containers: telemetryv1alpha1.LogPipelineContainerSelector{ - Include: []string{"container-1"}, - }, - }, - }, - }} - - err := validateInput(logPipeline) - require.NoError(t, err) -} - -func TestValidateWithValidInputExcludes(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - Exclude: []string{"namespace-1", "namespace-2"}, - }, - Containers: telemetryv1alpha1.LogPipelineContainerSelector{ - Exclude: []string{"container-1"}, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.NoError(t, err) -} - -func TestValidateWithValidInputIncludeContainersSystemFlag(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - System: true, - }, - Containers: telemetryv1alpha1.LogPipelineContainerSelector{ - Include: []string{"container-1"}, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.NoError(t, err) -} - -func TestValidateWithValidInputExcludeContainersSystemFlag(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - System: true, - }, - Containers: telemetryv1alpha1.LogPipelineContainerSelector{ - Exclude: []string{"container-1"}, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.NoError(t, err) -} - -func TestValidateWithInvalidNamespaceSelectors(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - Include: []string{"namespace-1", "namespace-2"}, - Exclude: []string{"namespace-3"}, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.Error(t, err) -} - -func TestValidateWithInvalidIncludeSystemFlag(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - Include: []string{"namespace-1", "namespace-2"}, - System: true, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.Error(t, err) -} - -func TestValidateWithInvalidExcludeSystemFlag(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Namespaces: telemetryv1alpha1.LogPipelineNamespaceSelector{ - Exclude: []string{"namespace-3"}, - System: true, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.Error(t, err) -} - -func TestValidateWithInvalidContainerSelectors(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Input: telemetryv1alpha1.LogPipelineInput{ - Application: &telemetryv1alpha1.LogPipelineApplicationInput{ - Containers: telemetryv1alpha1.LogPipelineContainerSelector{ - Include: []string{"container-1", "container-2"}, - Exclude: []string{"container-3"}, - }, - }, - }, - }, - } - - err := validateInput(logPipeline) - require.Error(t, err) -} diff --git a/webhook/logpipeline/validation/variable_validator_test.go b/webhook/logpipeline/validation/variable_validator_test.go deleted file mode 100644 index bc8da8339..000000000 --- a/webhook/logpipeline/validation/variable_validator_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package validation - -import ( - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - "github.com/kyma-project/telemetry-manager/internal/utils/k8s/mocks" -) - -func TestValidateSecretKeyRefs(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Variables: []telemetryv1alpha1.LogPipelineVariableRef{ - { - Name: "foo1", - ValueFrom: telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "fooN", - Namespace: "fooNs", - Key: "foo", - }, - }, - }, - { - Name: "foo2", - ValueFrom: telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "fooN", - Namespace: "fooNs", - Key: "foo", - }}, - }, - }, - }, - } - logPipeline.Name = "pipe1" - logPipelines := &telemetryv1alpha1.LogPipelineList{ - Items: []telemetryv1alpha1.LogPipeline{*logPipeline}, - } - - newLogPipeline := &telemetryv1alpha1.LogPipeline{ - ObjectMeta: metav1.ObjectMeta{ - Name: "pipe2", - }, - Spec: telemetryv1alpha1.LogPipelineSpec{ - Variables: []telemetryv1alpha1.LogPipelineVariableRef{{ - Name: "foo2", - ValueFrom: telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "fooN", - Namespace: "fooNs", - Key: "foo", - }}, - }}, - }, - } - mockClient := &mocks.Client{} - varValidator := NewVariablesValidator(mockClient) - - err := varValidator.Validate(newLogPipeline, logPipelines) - require.Error(t, err) -} - -func TestVariableValidator(t *testing.T) { - logPipeline := &telemetryv1alpha1.LogPipeline{ - Spec: telemetryv1alpha1.LogPipelineSpec{ - Variables: []telemetryv1alpha1.LogPipelineVariableRef{ - { - Name: "foo1", - ValueFrom: telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "fooN", - Namespace: "fooNs", - Key: "foo", - }, - }, - }, - { - Name: "foo2", - ValueFrom: telemetryv1alpha1.ValueFromSource{ - SecretKeyRef: &telemetryv1alpha1.SecretKeyRef{ - Name: "", - Namespace: "", - Key: "", - }}, - }, - }, - }, - } - logPipeline.Name = "pipe1" - mockClient := &mocks.Client{} - varValidator := NewVariablesValidator(mockClient) - logPipelines := &telemetryv1alpha1.LogPipelineList{ - Items: []telemetryv1alpha1.LogPipeline{*logPipeline}, - } - - err := varValidator.Validate(logPipeline, logPipelines) - require.Error(t, err) - require.Equal(t, "mandatory field variable name or secretKeyRef name or secretKeyRef namespace or secretKeyRef key cannot be empty", err.Error()) -} diff --git a/webhook/logpipeline/validation/variable_validator.go b/webhook/logpipeline/variable_validator.go similarity index 70% rename from webhook/logpipeline/validation/variable_validator.go rename to webhook/logpipeline/variable_validator.go index 7c545bc28..12795d8de 100644 --- a/webhook/logpipeline/validation/variable_validator.go +++ b/webhook/logpipeline/variable_validator.go @@ -1,28 +1,12 @@ -package validation +package logpipeline import ( "fmt" - "sigs.k8s.io/controller-runtime/pkg/client" - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" ) -type VariablesValidator interface { - Validate(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error -} - -type variablesValidator struct { - client client.Client -} - -func NewVariablesValidator(client client.Client) VariablesValidator { - return &variablesValidator{ - client: client, - } -} - -func (v *variablesValidator) Validate(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error { +func validateVariables(logPipeline *telemetryv1alpha1.LogPipeline, logPipelines *telemetryv1alpha1.LogPipelineList) error { if len(logPipeline.Spec.Variables) == 0 { return nil } diff --git a/webhook/logpipeline/variable_validator_test.go b/webhook/logpipeline/variable_validator_test.go new file mode 100644 index 000000000..01c48b15d --- /dev/null +++ b/webhook/logpipeline/variable_validator_test.go @@ -0,0 +1,63 @@ +package logpipeline + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" + testutils "github.com/kyma-project/telemetry-manager/internal/utils/test" +) + +func TestVariableNotGloballyUnique(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + existingPipeline := testutils.NewLogPipelineBuilder(). + WithName("log-pipeline-1"). + WithVariable("foo1", "fooN", "fooNs", "foo"). + WithVariable("foo2", "fooN", "fooNs", "foo"). + Build() + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&existingPipeline).Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder(). + WithName("log-pipeline-2"). + WithVariable("foo2", "fooN", "fooNs", "foo"). + Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.False(t, response.Allowed) + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Equal(t, response.Result.Message, "variable name must be globally unique: variable 'foo2' is used in pipeline 'log-pipeline-1'") +} + +func TestVariableValidator(t *testing.T) { + scheme := runtime.NewScheme() + _ = clientgoscheme.AddToScheme(scheme) + _ = telemetryv1alpha1.AddToScheme(scheme) + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() + + sut := NewValidatingWebhookHandler(fakeClient, scheme) + + newPipeline := testutils.NewLogPipelineBuilder(). + WithName("log-pipeline-2"). + WithVariable("foo2", "", "", ""). + Build() + + response := sut.Handle(context.Background(), admissionRequestFrom(t, newPipeline)) + + require.False(t, response.Allowed) + require.EqualValues(t, response.Result.Code, http.StatusBadRequest) + require.Equal(t, response.Result.Message, "mandatory field variable name or secretKeyRef name or secretKeyRef namespace or secretKeyRef key cannot be empty") +} diff --git a/webhook/logpipeline/webhook.go b/webhook/logpipeline/webhook.go deleted file mode 100644 index 57a32ef36..000000000 --- a/webhook/logpipeline/webhook.go +++ /dev/null @@ -1,138 +0,0 @@ -/* -Copyright 2021. - -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 logpipeline - -import ( - "context" - "fmt" - "net/http" - - admissionv1 "k8s.io/api/admission/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - logpipelineutils "github.com/kyma-project/telemetry-manager/internal/utils/logpipeline" - "github.com/kyma-project/telemetry-manager/webhook/logpipeline/validation" -) - -const ( - StatusReasonConfigurationError = "InvalidConfiguration" -) - -// +kubebuilder:webhook:path=/validate-logpipeline,mutating=false,failurePolicy=fail,sideEffects=None,groups=telemetry.kyma-project.io,resources=logpipelines,verbs=create;update,versions=v1alpha1,name=vlogpipeline.kb.io,admissionReviewVersions=v1 -type ValidatingWebhookHandler struct { - client.Client - variablesValidator validation.VariablesValidator - maxPipelinesValidator validation.MaxPipelinesValidator - fileValidator validation.FilesValidator - decoder admission.Decoder -} - -// TODO: Merge validation package with the webhook package, avoid useless dependency injection -func NewValidatingWebhookHandler( - client client.Client, - variablesValidator validation.VariablesValidator, - maxPipelinesValidator validation.MaxPipelinesValidator, - fileValidator validation.FilesValidator, - decoder admission.Decoder, -) *ValidatingWebhookHandler { - return &ValidatingWebhookHandler{ - Client: client, - variablesValidator: variablesValidator, - maxPipelinesValidator: maxPipelinesValidator, - decoder: decoder, - fileValidator: fileValidator, - } -} - -func (v *ValidatingWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { - log := logf.FromContext(ctx) - - logPipeline := &telemetryv1alpha1.LogPipeline{} - if err := v.decoder.Decode(req, logPipeline); err != nil { - log.Error(err, "Failed to decode LogPipeline") - return admission.Errored(http.StatusBadRequest, err) - } - - if err := v.validateLogPipeline(ctx, logPipeline); err != nil { - log.Error(err, "LogPipeline rejected") - - return admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: false, - Result: &metav1.Status{ - Code: int32(http.StatusForbidden), - Reason: StatusReasonConfigurationError, - Message: err.Error(), - }, - }, - } - } - - var warnMsg []string - - if logpipelineutils.ContainsCustomPlugin(logPipeline) { - helpText := "https://kyma-project.io/#/telemetry-manager/user/02-logs" - msg := fmt.Sprintf("Logpipeline '%s' uses unsupported custom filters or outputs. We recommend changing the pipeline to use supported filters or output. See the documentation: %s", logPipeline.Name, helpText) - warnMsg = append(warnMsg, msg) - } - - if len(warnMsg) != 0 { - return admission.Response{ - AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: true, - Warnings: warnMsg, - }, - } - } - - return admission.Allowed("LogPipeline validation successful") -} - -func (v *ValidatingWebhookHandler) validateLogPipeline(ctx context.Context, logPipeline *telemetryv1alpha1.LogPipeline) error { - log := logf.FromContext(ctx) - - var logPipelines telemetryv1alpha1.LogPipelineList - if err := v.List(ctx, &logPipelines); err != nil { - return err - } - - if err := v.maxPipelinesValidator.Validate(logPipeline, &logPipelines); err != nil { - log.Error(err, "Maximum number of log pipelines reached") - return err - } - - if err := validation.ValidateSpec(logPipeline); err != nil { - log.Error(err, "Failed to validate Fluent Bit input") - return err - } - - if err := v.variablesValidator.Validate(logPipeline, &logPipelines); err != nil { - log.Error(err, "Failed to validate variables") - return err - } - - if err := v.fileValidator.Validate(logPipeline, &logPipelines); err != nil { - log.Error(err, "Failed to validate Fluent Bit config") - return err - } - - return nil -} diff --git a/webhook/logpipeline/webhook_test.go b/webhook/logpipeline/webhook_test.go deleted file mode 100644 index 581dd0677..000000000 --- a/webhook/logpipeline/webhook_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package logpipeline - -import ( - "context" - "encoding/json" - "net/http" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - admissionv1 "k8s.io/api/admission/v1" - "k8s.io/apimachinery/pkg/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - telemetryv1alpha1 "github.com/kyma-project/telemetry-manager/apis/telemetry/v1alpha1" - "github.com/kyma-project/telemetry-manager/internal/featureflags" - testutils "github.com/kyma-project/telemetry-manager/internal/utils/test" - logpipelinevalidationmocks "github.com/kyma-project/telemetry-manager/webhook/logpipeline/validation/mocks" -) - -func TestHandle(t *testing.T) { - scheme := runtime.NewScheme() - _ = clientgoscheme.AddToScheme(scheme) - _ = telemetryv1alpha1.AddToScheme(scheme) - - t.Run("should execute validations for max pipelines, variables, files", func(t *testing.T) { - maxPipelinesValidatorMock := &logpipelinevalidationmocks.MaxPipelinesValidator{} - variableValidatorMock := &logpipelinevalidationmocks.VariablesValidator{} - fileValidatorMock := &logpipelinevalidationmocks.FilesValidator{} - - maxPipelinesValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - variableValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - fileValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - - logPipeline := testutils.NewLogPipelineBuilder().Build() - pipelineJSON, _ := json.Marshal(logPipeline) - admissionRequest := admissionv1.AdmissionRequest{ - Object: runtime.RawExtension{Raw: pipelineJSON}, - } - request := admission.Request{ - AdmissionRequest: admissionRequest, - } - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - logPipelineValidatingWebhookHandler := NewValidatingWebhookHandler(fakeClient, variableValidatorMock, maxPipelinesValidatorMock, fileValidatorMock, admission.NewDecoder(clientgoscheme.Scheme)) - - response := logPipelineValidatingWebhookHandler.Handle(context.Background(), request) - require.True(t, response.Allowed) - - variableValidatorMock.AssertExpectations(t) - maxPipelinesValidatorMock.AssertExpectations(t) - fileValidatorMock.AssertExpectations(t) - }) - - t.Run("should execute validations for API semantic", func(t *testing.T) { - maxPipelinesValidatorMock := &logpipelinevalidationmocks.MaxPipelinesValidator{} - variableValidatorMock := &logpipelinevalidationmocks.VariablesValidator{} - fileValidatorMock := &logpipelinevalidationmocks.FilesValidator{} - - maxPipelinesValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - variableValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - fileValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - - logPipeline := testutils.NewLogPipelineBuilder().WithName("denied-filter").WithCustomFilter("Name kubernetes").Build() - pipelineJSON, _ := json.Marshal(logPipeline) - admissionRequest := admissionv1.AdmissionRequest{ - Object: runtime.RawExtension{Raw: pipelineJSON}, - } - request := admission.Request{ - AdmissionRequest: admissionRequest, - } - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - logPipelineValidatingWebhookHandler := NewValidatingWebhookHandler(fakeClient, variableValidatorMock, maxPipelinesValidatorMock, fileValidatorMock, admission.NewDecoder(clientgoscheme.Scheme)) - - response := logPipelineValidatingWebhookHandler.Handle(context.Background(), request) - require.False(t, response.Allowed) - require.Equal(t, int32(http.StatusForbidden), response.Result.Code) - require.Equal(t, "InvalidConfiguration", string(response.Result.Reason)) - require.Contains(t, response.Result.Message, "filter plugin 'kubernetes' is forbidden") - }) - - t.Run("should return a warning when a custom plugin is used", func(t *testing.T) { - maxPipelinesValidatorMock := &logpipelinevalidationmocks.MaxPipelinesValidator{} - variableValidatorMock := &logpipelinevalidationmocks.VariablesValidator{} - fileValidatorMock := &logpipelinevalidationmocks.FilesValidator{} - - maxPipelinesValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - variableValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - fileValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - - logPipeline := testutils.NewLogPipelineBuilder().WithName("custom-output").WithCustomOutput("Name stdout").Build() - pipelineJSON, _ := json.Marshal(logPipeline) - admissionRequest := admissionv1.AdmissionRequest{ - Object: runtime.RawExtension{Raw: pipelineJSON}, - } - request := admission.Request{ - AdmissionRequest: admissionRequest, - } - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - logPipelineValidatingWebhookHandler := NewValidatingWebhookHandler(fakeClient, variableValidatorMock, maxPipelinesValidatorMock, fileValidatorMock, admission.NewDecoder(clientgoscheme.Scheme)) - - response := logPipelineValidatingWebhookHandler.Handle(context.Background(), request) - require.True(t, response.Allowed) - require.Contains(t, response.Warnings, "Logpipeline 'custom-output' uses unsupported custom filters or outputs. We recommend changing the pipeline to use supported filters or output. See the documentation: https://kyma-project.io/#/telemetry-manager/user/02-logs") - }) - - t.Run("should validate OTLP input based on output", func(t *testing.T) { - type args struct { - name string - output *telemetryv1alpha1.LogPipelineOutput - input *telemetryv1alpha1.LogPipelineInput - allowed bool - message string - } - - tests := []args{ - { - name: "otlp-input-and-output", - output: &telemetryv1alpha1.LogPipelineOutput{ - Custom: "", - HTTP: nil, - OTLP: &telemetryv1alpha1.OTLPOutput{ - Protocol: "grpc", - Endpoint: telemetryv1alpha1.ValueType{Value: ""}, - TLS: &telemetryv1alpha1.OTLPTLS{ - Insecure: true, - }, - }, - }, - input: &telemetryv1alpha1.LogPipelineInput{ - OTLP: &telemetryv1alpha1.OTLPInput{}, - }, - allowed: true, - }, - { - name: "otlp-input-and-fluentbit-output", - output: &telemetryv1alpha1.LogPipelineOutput{ - Custom: "", - HTTP: &telemetryv1alpha1.LogPipelineHTTPOutput{ - Host: telemetryv1alpha1.ValueType{Value: "127.0.0.1"}, - Port: "8080", - URI: "/", - Format: "json", - TLS: telemetryv1alpha1.LogPipelineOutputTLS{ - Disabled: true, - SkipCertificateValidation: true, - }, - }, - }, - input: &telemetryv1alpha1.LogPipelineInput{ - OTLP: &telemetryv1alpha1.OTLPInput{}, - }, - allowed: false, - message: "invalid log pipeline definition: cannot use OTLP input for pipeline in FluentBit mode", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - featureflags.Enable(featureflags.LogPipelineOTLP) - - maxPipelinesValidatorMock := &logpipelinevalidationmocks.MaxPipelinesValidator{} - variableValidatorMock := &logpipelinevalidationmocks.VariablesValidator{} - fileValidatorMock := &logpipelinevalidationmocks.FilesValidator{} - - maxPipelinesValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - variableValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - fileValidatorMock.On("Validate", mock.Anything, mock.Anything).Return(nil).Times(1) - - logPipeline := testutils.NewLogPipelineBuilder().Build() - logPipeline.Spec.Output = *tt.output - logPipeline.Spec.Input = *tt.input - - pipelineJSON, _ := json.Marshal(logPipeline) - admissionRequest := admissionv1.AdmissionRequest{ - Object: runtime.RawExtension{Raw: pipelineJSON}, - } - request := admission.Request{ - AdmissionRequest: admissionRequest, - } - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build() - logPipelineValidatingWebhookHandler := NewValidatingWebhookHandler(fakeClient, variableValidatorMock, maxPipelinesValidatorMock, fileValidatorMock, admission.NewDecoder(clientgoscheme.Scheme)) - - response := logPipelineValidatingWebhookHandler.Handle(context.Background(), request) - require.Equal(t, tt.allowed, response.Allowed) - - if !tt.allowed { - require.Contains(t, response.Result.Message, tt.message) - } - }) - } - }) -}