diff --git a/cmd/polaris/webhook.go b/cmd/polaris/webhook.go index f42ae5772..183b32be1 100644 --- a/cmd/polaris/webhook.go +++ b/cmd/polaris/webhook.go @@ -63,7 +63,8 @@ var webhookCmd = &cobra.Command{ // Iterate all the configurations supported controllers to scan and register them for webhooks // Should only register controllers that are configured to be scanned - fwebhook.NewWebhook(mgr, fwebhook.Validator{Config: config, Client: mgr.GetClient()}) + fwebhook.NewValidateWebhook(mgr, fwebhook.Validator{Config: config, Client: mgr.GetClient()}) + fwebhook.NewMutateWebhook(mgr, fwebhook.Mutator{Config: config, Client: mgr.GetClient()}) logrus.Infof("Polaris webhook server listening on port %d", webhookPort) if err := mgr.Start(signals.SetupSignalHandler()); err != nil { diff --git a/go.mod b/go.mod index 31b27279b..b4f201c06 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require gomodules.xyz/jsonpatch/v2 v2.2.0 + require ( cloud.google.com/go v0.81.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect @@ -72,7 +74,6 @@ require ( golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect - gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/pkg/config/schema.go b/pkg/config/schema.go index 692ced0aa..b80887d55 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -25,6 +25,7 @@ import ( "github.com/qri-io/jsonschema" "github.com/thoas/go-funk" + "gomodules.xyz/jsonpatch/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" k8sYaml "k8s.io/apimachinery/pkg/util/yaml" @@ -71,7 +72,7 @@ type SchemaCheck struct { AdditionalSchemas map[string]map[string]interface{} `yaml:"additionalSchemas" json:"additionalSchemas"` AdditionalSchemaStrings map[string]string `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"` AdditionalValidators map[string]jsonschema.RootSchema `yaml:"-" json:"-"` - Mutations []map[string]interface{} `yaml:"mutations" json:"mutations"` + Mutations []jsonpatch.Operation `yaml:"mutations" json:"mutations"` Comments []MutationComment `yaml:"comments" json:"comments"` } diff --git a/pkg/mutation/mutate.go b/pkg/mutation/mutate.go index df07b98eb..7732c6964 100644 --- a/pkg/mutation/mutate.go +++ b/pkg/mutation/mutate.go @@ -6,15 +6,16 @@ import ( "fmt" "strings" - jsonpatch "github.com/evanphx/json-patch/v5" + jsonpatchV5 "github.com/evanphx/json-patch/v5" "github.com/fairwindsops/polaris/pkg/config" "github.com/fairwindsops/polaris/pkg/kube" "github.com/fairwindsops/polaris/pkg/validator" "github.com/thoas/go-funk" + "gomodules.xyz/jsonpatch/v2" ) // ApplyAllSchemaMutations applies available mutation to a single resource -func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource, mutations []map[string]interface{}) (kube.GenericResource, error) { +func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource, mutations []jsonpatch.Operation) (kube.GenericResource, error) { resByte := resource.OriginalObjectJSON var jsonByte []byte mutationByte, err := json.Marshal(mutations) @@ -22,7 +23,7 @@ func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube. return resource, err } - patch, err := jsonpatch.DecodePatch(mutationByte) + patch, err := jsonpatchV5.DecodePatch(mutationByte) if err != nil { return resource, err } @@ -39,55 +40,54 @@ func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube. } // GetMutationsAndCommentsFromResults returns all mutations from results -func GetMutationsAndCommentsFromResults(results []validator.Result) ([]config.MutationComment, map[string][]map[string]interface{}) { - allMutationsFromResults := make(map[string][]map[string]interface{}) +func GetMutationsAndCommentsFromResults(results []validator.Result) ([]config.MutationComment, map[string][]jsonpatch.Operation) { + allMutationsFromResults := make(map[string][]jsonpatch.Operation) comments := []config.MutationComment{} for _, result := range results { key := fmt.Sprintf("%s/%s/%s", result.Kind, result.Name, result.Namespace) - for _, resultMessage := range result.Results { - if len(resultMessage.Mutations) > 0 { - mutations, ok := allMutationsFromResults[key] - if !ok { - mutations = make([]map[string]interface{}, 0) - } - allMutationsFromResults[key] = append(mutations, resultMessage.Mutations...) - } - if len(resultMessage.Comments) > 0 { - comments = append(comments, resultMessage.Comments...) - } + mutations, resultsComments := GetMutationsAndCommentsFromResult(&result) + allMutationsFromResults[key] = mutations + comments = append(comments, resultsComments...) + + } + return comments, allMutationsFromResults +} + +// GetMutationsAndCommentsFromResult returns all mutations from single result +func GetMutationsAndCommentsFromResult(result *validator.Result) ([]jsonpatch.Operation, []config.MutationComment) { + mutations := []jsonpatch.Operation{} + comments := []config.MutationComment{} + for _, resultMessage := range result.Results { + if len(resultMessage.Mutations) > 0 { + mutations = append(mutations, resultMessage.Mutations...) + } + if len(resultMessage.Comments) > 0 { + comments = append(comments, resultMessage.Comments...) } + } + + for _, resultMessage := range result.PodResult.Results { + if len(resultMessage.Mutations) > 0 { + mutations = append(mutations, resultMessage.Mutations...) + } + if len(resultMessage.Comments) > 0 { + comments = append(comments, resultMessage.Comments...) + } + } - for _, resultMessage := range result.PodResult.Results { + for _, containerResult := range result.PodResult.ContainerResults { + for _, resultMessage := range containerResult.Results { if len(resultMessage.Mutations) > 0 { - mutations, ok := allMutationsFromResults[key] - if !ok { - mutations = make([]map[string]interface{}, 0) - } - allMutationsFromResults[key] = append(mutations, resultMessage.Mutations...) + mutations = append(mutations, resultMessage.Mutations...) } if len(resultMessage.Comments) > 0 { comments = append(comments, resultMessage.Comments...) } } - - for _, containerResult := range result.PodResult.ContainerResults { - for _, resultMessage := range containerResult.Results { - if len(resultMessage.Mutations) > 0 { - mutations, ok := allMutationsFromResults[key] - if !ok { - mutations = make([]map[string]interface{}, 0) - } - allMutationsFromResults[key] = append(mutations, resultMessage.Mutations...) - } - if len(resultMessage.Comments) > 0 { - comments = append(comments, resultMessage.Comments...) - } - } - } - } - return comments, allMutationsFromResults + + return mutations, comments } // UpdateMutatedContentWithComments Updates mutated object with comments diff --git a/pkg/validator/output.go b/pkg/validator/output.go index 08257fd92..74636a5ae 100644 --- a/pkg/validator/output.go +++ b/pkg/validator/output.go @@ -20,6 +20,7 @@ import ( "github.com/fatih/color" "github.com/thoas/go-funk" + "gomodules.xyz/jsonpatch/v2" "github.com/fairwindsops/polaris/pkg/config" ) @@ -78,7 +79,7 @@ type ResultMessage struct { Success bool Severity config.Severity Category string - Mutations []map[string]interface{} + Mutations []jsonpatch.Operation Comments []config.MutationComment } diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index c37d1060f..1056101b5 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -23,6 +23,7 @@ import ( "github.com/qri-io/jsonschema" "github.com/sirupsen/logrus" "github.com/thoas/go-funk" + "gomodules.xyz/jsonpatch/v2" corev1 "k8s.io/api/core/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -313,11 +314,11 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes result := makeResult(conf, check, passes, issues) if !passes { if funk.Contains(conf.Mutations, checkID) { - mutations := funk.Map(check.Mutations, func(mutation map[string]interface{}) map[string]interface{} { + mutations := funk.Map(check.Mutations, func(mutation jsonpatch.Operation) jsonpatch.Operation { mutationCopy := deepCopyMutation(mutation) - mutationCopy["path"] = prefix + mutationCopy["path"].(string) + mutationCopy.Path = prefix + mutationCopy.Path return mutationCopy - }).([]map[string]interface{}) + }).([]jsonpatch.Operation) result.Mutations = mutations result.Comments = check.Comments } @@ -334,10 +335,11 @@ func getSortedKeys(m map[string]config.Severity) []string { return keys } -func deepCopyMutation(source map[string]interface{}) map[string]interface{} { - destination := map[string]interface{}{} - for key, value := range source { - destination[key] = value +func deepCopyMutation(source jsonpatch.Operation) jsonpatch.Operation { + destination := jsonpatch.Operation{ + Operation: source.Operation, + Path: source.Path, + Value: source.Value, } return destination } diff --git a/pkg/webhook/mutate.go b/pkg/webhook/mutate.go new file mode 100644 index 000000000..05d931cc6 --- /dev/null +++ b/pkg/webhook/mutate.go @@ -0,0 +1,66 @@ +// Copyright 2022 FairwindsOps Inc +// +// 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 webhook + +import ( + "context" + + "github.com/fairwindsops/polaris/pkg/config" + "github.com/fairwindsops/polaris/pkg/mutation" + "github.com/sirupsen/logrus" + "gomodules.xyz/jsonpatch/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// Mutator mutate k8s resources. +type Mutator struct { + Client client.Client + Config config.Configuration + decoder *admission.Decoder +} + +var _ admission.Handler = &Mutator{} + +// NewMutateWebhook creates a mutating admission webhook for the apiType. +func NewMutateWebhook(mgr manager.Manager, mutator Mutator) { + path := "/mutate" + + mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &mutator}) +} + +func (m *Mutator) mutate(req admission.Request) ([]jsonpatch.Operation, error) { + results, err := getValidateResults(req.AdmissionRequest.Kind.Kind, m.decoder, req, m.Config) + if err != nil { + return nil, err + } + patches, _ := mutation.GetMutationsAndCommentsFromResult(results) + return patches, nil +} + +// Handle for Validator to run validation checks. +func (m *Mutator) Handle(ctx context.Context, req admission.Request) admission.Response { + logrus.Info("Starting request") + patches, err := m.mutate(req) + if err != nil { + return admission.Errored(403, err) + } + if patches == nil { + return admission.Allowed("Allowed") + } + return admission.Patched("", patches...) +} diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 57cd19563..4127fbfa0 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -47,19 +47,23 @@ func (v *Validator) InjectDecoder(d *admission.Decoder) error { var _ admission.Handler = &Validator{} -// NewWebhook creates a validating admission webhook for the apiType. -func NewWebhook(mgr manager.Manager, validator Validator) { +// NewValidateWebhook creates a validating admission webhook for the apiType. +func NewValidateWebhook(mgr manager.Manager, validator Validator) { path := "/validate" mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &validator}) } func (v *Validator) handleInternal(req admission.Request) (*validator.Result, error) { + return getValidateResults(req.AdmissionRequest.Kind.Kind, v.decoder, req, v.Config) +} + +func getValidateResults(kind string, decoder *admission.Decoder, req admission.Request, config config.Configuration) (*validator.Result, error) { var controller kube.GenericResource var err error - if req.AdmissionRequest.Kind.Kind == "Pod" { + if kind == "Pod" { pod := corev1.Pod{} - err := v.decoder.Decode(req, &pod) + err := decoder.Decode(req, &pod) if err != nil { return nil, err } @@ -75,7 +79,7 @@ func (v *Validator) handleInternal(req admission.Request) (*validator.Result, er return nil, err } // TODO: consider enabling multi-resource checks - controllerResult, err := validator.ApplyAllSchemaChecks(&v.Config, nil, controller) + controllerResult, err := validator.ApplyAllSchemaChecks(&config, nil, controller) if err != nil { return nil, err }