Skip to content

Commit

Permalink
Added Mutation webhook (#755)
Browse files Browse the repository at this point in the history
* added mutate webhook

* fix mutation operation type

* if no mutation just use valid response
  • Loading branch information
makoscafee authored May 3, 2022
1 parent 6c33168 commit 6b7d6ab
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 55 deletions.
3 changes: 2 additions & 1 deletion cmd/polaris/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"`
}

Expand Down
78 changes: 39 additions & 39 deletions pkg/mutation/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,24 @@ 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)
if err != nil {
return resource, err
}

patch, err := jsonpatch.DecodePatch(mutationByte)
patch, err := jsonpatchV5.DecodePatch(mutationByte)
if err != nil {
return resource, err
}
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/validator/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/fatih/color"
"github.com/thoas/go-funk"
"gomodules.xyz/jsonpatch/v2"

"github.com/fairwindsops/polaris/pkg/config"
)
Expand Down Expand Up @@ -78,7 +79,7 @@ type ResultMessage struct {
Success bool
Severity config.Severity
Category string
Mutations []map[string]interface{}
Mutations []jsonpatch.Operation
Comments []config.MutationComment
}

Expand Down
16 changes: 9 additions & 7 deletions pkg/validator/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/webhook/mutate.go
Original file line number Diff line number Diff line change
@@ -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...)
}
14 changes: 9 additions & 5 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down

0 comments on commit 6b7d6ab

Please sign in to comment.