diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5105361..39f3e94 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,11 +19,8 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Generate Mocks - run: go get github.com/golang/mock/mockgen@v1.4.4 && mockgen -destination pkg/mocks/manager/mock.go sigs.k8s.io/controller-runtime/pkg/manager Manager - - name: Test - run: go test ./... -coverprofile cover.out -timeout 30m + run: make test env: CGO_ENABLED: 0 GO111MODULE: on diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9986dd0 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Run go fmt against code +fmt: + go fmt ./... + gofmt -s -w . + +# Run go vet against code +vet: + go vet ./... + +# Run golangci-lint +lint: + golangci-lint run + +# Run go mod tidy +tidy: + go mod tidy + +# Run tests +test: mocks tidy fmt vet + go test ./... -coverprofile cover.out -timeout 30m + +mocks: mockgen + mockgen -destination pkg/mocks/manager/mock.go sigs.k8s.io/controller-runtime/pkg/manager Manager + +mockgen: +ifeq (, $(shell which mockgen)) + $(shell go get github.com/golang/mock/mockgen@v1.6.0) +endif \ No newline at end of file diff --git a/README.md b/README.md index e915678..38c08db 100644 --- a/README.md +++ b/README.md @@ -55,4 +55,44 @@ if err = (&pod.Webhook{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "Pod") os.Exit(1) } +``` + +## Examples +### Mutating admission webhook using `MutateObjectByFunc` +```go +package pod + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/snorwin/k8s-generic-webhook/pkg/webhook" +) + +type Webhook struct { + webhook.MutatingWebhook +} + +func (w *Webhook) SetupWebhookWithManager(mgr manager.Manager) error { + return webhook.NewGenericWebhookManagedBy(mgr). + For(&corev1.Pod{}). + Complete(w) +} + +func (w *Webhook) Mutate(ctx context.Context, req admission.Request) admission.Response { + return webhook.MutateObjectByFunc(ctx, req, func(ctx context.Context, request admission.Request, object runtime.Object) error { + _ = log.FromContext(ctx) + + pod := object.(*corev1.Pod) + // TODO add your programmatic mutation logic here + object = pod + + return nil + }) +} ``` \ No newline at end of file diff --git a/go.mod b/go.mod index 61bc85d..25c2c44 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.16 require ( github.com/go-logr/logr v0.4.0 - github.com/golang/mock v1.4.4 + github.com/golang/mock v1.6.0 github.com/onsi/ginkgo v1.16.4 github.com/onsi/gomega v1.15.0 k8s.io/api v0.21.3 diff --git a/go.sum b/go.sum index 53bd14a..b0ab7c6 100644 --- a/go.sum +++ b/go.sum @@ -141,8 +141,8 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -384,6 +384,7 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -446,6 +447,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -476,6 +478,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -492,6 +495,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -533,8 +537,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -597,8 +603,9 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/mocks/manager/mock.go b/pkg/mocks/manager/mock.go index ca2d4c3..431dd17 100644 --- a/pkg/mocks/manager/mock.go +++ b/pkg/mocks/manager/mock.go @@ -6,14 +6,15 @@ package mock_manager import ( context "context" + http "net/http" + reflect "reflect" + logr "github.com/go-logr/logr" gomock "github.com/golang/mock/gomock" meta "k8s.io/apimachinery/pkg/api/meta" runtime "k8s.io/apimachinery/pkg/runtime" rest "k8s.io/client-go/rest" record "k8s.io/client-go/tools/record" - http "net/http" - reflect "reflect" cache "sigs.k8s.io/controller-runtime/pkg/cache" client "sigs.k8s.io/controller-runtime/pkg/client" v1alpha1 "sigs.k8s.io/controller-runtime/pkg/config/v1alpha1" @@ -22,30 +23,30 @@ import ( webhook "sigs.k8s.io/controller-runtime/pkg/webhook" ) -// MockManager is a mock of Manager interface +// MockManager is a mock of Manager interface. type MockManager struct { ctrl *gomock.Controller recorder *MockManagerMockRecorder } -// MockManagerMockRecorder is the mock recorder for MockManager +// MockManagerMockRecorder is the mock recorder for MockManager. type MockManagerMockRecorder struct { mock *MockManager } -// NewMockManager creates a new mock instance +// NewMockManager creates a new mock instance. func NewMockManager(ctrl *gomock.Controller) *MockManager { mock := &MockManager{ctrl: ctrl} mock.recorder = &MockManagerMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockManager) EXPECT() *MockManagerMockRecorder { return m.recorder } -// Add mocks base method +// Add mocks base method. func (m *MockManager) Add(arg0 manager.Runnable) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Add", arg0) @@ -53,13 +54,13 @@ func (m *MockManager) Add(arg0 manager.Runnable) error { return ret0 } -// Add indicates an expected call of Add +// Add indicates an expected call of Add. func (mr *MockManagerMockRecorder) Add(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockManager)(nil).Add), arg0) } -// AddHealthzCheck mocks base method +// AddHealthzCheck mocks base method. func (m *MockManager) AddHealthzCheck(arg0 string, arg1 healthz.Checker) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddHealthzCheck", arg0, arg1) @@ -67,13 +68,13 @@ func (m *MockManager) AddHealthzCheck(arg0 string, arg1 healthz.Checker) error { return ret0 } -// AddHealthzCheck indicates an expected call of AddHealthzCheck +// AddHealthzCheck indicates an expected call of AddHealthzCheck. func (mr *MockManagerMockRecorder) AddHealthzCheck(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHealthzCheck", reflect.TypeOf((*MockManager)(nil).AddHealthzCheck), arg0, arg1) } -// AddMetricsExtraHandler mocks base method +// AddMetricsExtraHandler mocks base method. func (m *MockManager) AddMetricsExtraHandler(arg0 string, arg1 http.Handler) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddMetricsExtraHandler", arg0, arg1) @@ -81,13 +82,13 @@ func (m *MockManager) AddMetricsExtraHandler(arg0 string, arg1 http.Handler) err return ret0 } -// AddMetricsExtraHandler indicates an expected call of AddMetricsExtraHandler +// AddMetricsExtraHandler indicates an expected call of AddMetricsExtraHandler. func (mr *MockManagerMockRecorder) AddMetricsExtraHandler(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetricsExtraHandler", reflect.TypeOf((*MockManager)(nil).AddMetricsExtraHandler), arg0, arg1) } -// AddReadyzCheck mocks base method +// AddReadyzCheck mocks base method. func (m *MockManager) AddReadyzCheck(arg0 string, arg1 healthz.Checker) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddReadyzCheck", arg0, arg1) @@ -95,13 +96,13 @@ func (m *MockManager) AddReadyzCheck(arg0 string, arg1 healthz.Checker) error { return ret0 } -// AddReadyzCheck indicates an expected call of AddReadyzCheck +// AddReadyzCheck indicates an expected call of AddReadyzCheck. func (mr *MockManagerMockRecorder) AddReadyzCheck(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddReadyzCheck", reflect.TypeOf((*MockManager)(nil).AddReadyzCheck), arg0, arg1) } -// Elected mocks base method +// Elected mocks base method. func (m *MockManager) Elected() <-chan struct{} { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Elected") @@ -109,13 +110,13 @@ func (m *MockManager) Elected() <-chan struct{} { return ret0 } -// Elected indicates an expected call of Elected +// Elected indicates an expected call of Elected. func (mr *MockManagerMockRecorder) Elected() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Elected", reflect.TypeOf((*MockManager)(nil).Elected)) } -// GetAPIReader mocks base method +// GetAPIReader mocks base method. func (m *MockManager) GetAPIReader() client.Reader { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetAPIReader") @@ -123,13 +124,13 @@ func (m *MockManager) GetAPIReader() client.Reader { return ret0 } -// GetAPIReader indicates an expected call of GetAPIReader +// GetAPIReader indicates an expected call of GetAPIReader. func (mr *MockManagerMockRecorder) GetAPIReader() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIReader", reflect.TypeOf((*MockManager)(nil).GetAPIReader)) } -// GetCache mocks base method +// GetCache mocks base method. func (m *MockManager) GetCache() cache.Cache { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCache") @@ -137,13 +138,13 @@ func (m *MockManager) GetCache() cache.Cache { return ret0 } -// GetCache indicates an expected call of GetCache +// GetCache indicates an expected call of GetCache. func (mr *MockManagerMockRecorder) GetCache() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCache", reflect.TypeOf((*MockManager)(nil).GetCache)) } -// GetClient mocks base method +// GetClient mocks base method. func (m *MockManager) GetClient() client.Client { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetClient") @@ -151,13 +152,13 @@ func (m *MockManager) GetClient() client.Client { return ret0 } -// GetClient indicates an expected call of GetClient +// GetClient indicates an expected call of GetClient. func (mr *MockManagerMockRecorder) GetClient() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockManager)(nil).GetClient)) } -// GetConfig mocks base method +// GetConfig mocks base method. func (m *MockManager) GetConfig() *rest.Config { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetConfig") @@ -165,13 +166,13 @@ func (m *MockManager) GetConfig() *rest.Config { return ret0 } -// GetConfig indicates an expected call of GetConfig +// GetConfig indicates an expected call of GetConfig. func (mr *MockManagerMockRecorder) GetConfig() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockManager)(nil).GetConfig)) } -// GetControllerOptions mocks base method +// GetControllerOptions mocks base method. func (m *MockManager) GetControllerOptions() v1alpha1.ControllerConfigurationSpec { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetControllerOptions") @@ -179,13 +180,13 @@ func (m *MockManager) GetControllerOptions() v1alpha1.ControllerConfigurationSpe return ret0 } -// GetControllerOptions indicates an expected call of GetControllerOptions +// GetControllerOptions indicates an expected call of GetControllerOptions. func (mr *MockManagerMockRecorder) GetControllerOptions() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetControllerOptions", reflect.TypeOf((*MockManager)(nil).GetControllerOptions)) } -// GetEventRecorderFor mocks base method +// GetEventRecorderFor mocks base method. func (m *MockManager) GetEventRecorderFor(arg0 string) record.EventRecorder { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetEventRecorderFor", arg0) @@ -193,13 +194,13 @@ func (m *MockManager) GetEventRecorderFor(arg0 string) record.EventRecorder { return ret0 } -// GetEventRecorderFor indicates an expected call of GetEventRecorderFor +// GetEventRecorderFor indicates an expected call of GetEventRecorderFor. func (mr *MockManagerMockRecorder) GetEventRecorderFor(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEventRecorderFor", reflect.TypeOf((*MockManager)(nil).GetEventRecorderFor), arg0) } -// GetFieldIndexer mocks base method +// GetFieldIndexer mocks base method. func (m *MockManager) GetFieldIndexer() client.FieldIndexer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFieldIndexer") @@ -207,13 +208,13 @@ func (m *MockManager) GetFieldIndexer() client.FieldIndexer { return ret0 } -// GetFieldIndexer indicates an expected call of GetFieldIndexer +// GetFieldIndexer indicates an expected call of GetFieldIndexer. func (mr *MockManagerMockRecorder) GetFieldIndexer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFieldIndexer", reflect.TypeOf((*MockManager)(nil).GetFieldIndexer)) } -// GetLogger mocks base method +// GetLogger mocks base method. func (m *MockManager) GetLogger() logr.Logger { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLogger") @@ -221,13 +222,13 @@ func (m *MockManager) GetLogger() logr.Logger { return ret0 } -// GetLogger indicates an expected call of GetLogger +// GetLogger indicates an expected call of GetLogger. func (mr *MockManagerMockRecorder) GetLogger() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogger", reflect.TypeOf((*MockManager)(nil).GetLogger)) } -// GetRESTMapper mocks base method +// GetRESTMapper mocks base method. func (m *MockManager) GetRESTMapper() meta.RESTMapper { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRESTMapper") @@ -235,13 +236,13 @@ func (m *MockManager) GetRESTMapper() meta.RESTMapper { return ret0 } -// GetRESTMapper indicates an expected call of GetRESTMapper +// GetRESTMapper indicates an expected call of GetRESTMapper. func (mr *MockManagerMockRecorder) GetRESTMapper() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRESTMapper", reflect.TypeOf((*MockManager)(nil).GetRESTMapper)) } -// GetScheme mocks base method +// GetScheme mocks base method. func (m *MockManager) GetScheme() *runtime.Scheme { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetScheme") @@ -249,13 +250,13 @@ func (m *MockManager) GetScheme() *runtime.Scheme { return ret0 } -// GetScheme indicates an expected call of GetScheme +// GetScheme indicates an expected call of GetScheme. func (mr *MockManagerMockRecorder) GetScheme() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScheme", reflect.TypeOf((*MockManager)(nil).GetScheme)) } -// GetWebhookServer mocks base method +// GetWebhookServer mocks base method. func (m *MockManager) GetWebhookServer() *webhook.Server { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetWebhookServer") @@ -263,13 +264,13 @@ func (m *MockManager) GetWebhookServer() *webhook.Server { return ret0 } -// GetWebhookServer indicates an expected call of GetWebhookServer +// GetWebhookServer indicates an expected call of GetWebhookServer. func (mr *MockManagerMockRecorder) GetWebhookServer() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWebhookServer", reflect.TypeOf((*MockManager)(nil).GetWebhookServer)) } -// SetFields mocks base method +// SetFields mocks base method. func (m *MockManager) SetFields(arg0 interface{}) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetFields", arg0) @@ -277,13 +278,13 @@ func (m *MockManager) SetFields(arg0 interface{}) error { return ret0 } -// SetFields indicates an expected call of SetFields +// SetFields indicates an expected call of SetFields. func (mr *MockManagerMockRecorder) SetFields(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFields", reflect.TypeOf((*MockManager)(nil).SetFields), arg0) } -// Start mocks base method +// Start mocks base method. func (m *MockManager) Start(arg0 context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Start", arg0) @@ -291,7 +292,7 @@ func (m *MockManager) Start(arg0 context.Context) error { return ret0 } -// Start indicates an expected call of Start +// Start indicates an expected call of Start. func (mr *MockManagerMockRecorder) Start(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockManager)(nil).Start), arg0) diff --git a/pkg/webhook/handler.go b/pkg/webhook/handler.go index e04fb15..f9fbd00 100644 --- a/pkg/webhook/handler.go +++ b/pkg/webhook/handler.go @@ -71,7 +71,11 @@ func (h *handler) Handle(ctx context.Context, req admission.Request) admission.R // invoke mutator if mutator, ok := h.Handler.(Mutator); ok { - return mutator.Mutate(ctx, req) + if req.Object.Object != nil { + return mutator.Mutate(ctx, req) + } else { + return admission.Allowed("") + } } return admission.Denied("") diff --git a/pkg/webhook/handler_test.go b/pkg/webhook/handler_test.go index d37db48..0839aae 100644 --- a/pkg/webhook/handler_test.go +++ b/pkg/webhook/handler_test.go @@ -33,6 +33,38 @@ var _ = Describe("Handler", func() { result := (&handler{}).Handle(context.TODO(), admission.Request{}) Ω(result.Allowed).Should(BeFalse()) }) + It("should mutate", func() { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + } + raw, err := json.Marshal(pod) + Ω(err).ShouldNot(HaveOccurred()) + + h := handler{ + Handler: &MutateFunc{ + Func: func(ctx context.Context, request admission.Request) admission.Response { + return admission.Allowed("") + }, + }, + Object: &corev1.Pod{}, + } + err = h.InjectDecoder(decoder) + Ω(err).ShouldNot(HaveOccurred()) + result := h.Handle(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: raw, + }, + Operation: admissionv1.Create, + }, + }) + Ω(result.Allowed).Should(BeTrue()) + result = h.Handle(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + }) It("should validate", func() { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -130,6 +162,9 @@ var _ = Describe("Handler", func() { result = h.Handle(context.TODO(), admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Raw: raw, + }, OldObject: runtime.RawExtension{ Raw: raw, }, diff --git a/pkg/webhook/injection.go b/pkg/webhook/injection.go new file mode 100644 index 0000000..052f5d9 --- /dev/null +++ b/pkg/webhook/injection.go @@ -0,0 +1,28 @@ +package webhook + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// InjectedClient holds an injected client.Client +type InjectedClient struct { + Client client.Client +} + +// InjectClient implements the inject.Client interface. +func (i *InjectedClient) InjectClient(client client.Client) error { + i.Client = client + return nil +} + +// InjectedDecoder holds an injected admission.Decoder +type InjectedDecoder struct { + Decoder *admission.Decoder +} + +// InjectDecoder implements the admission.DecoderInjector interface. +func (i *InjectedDecoder) InjectDecoder(decoder *admission.Decoder) error { + i.Decoder = decoder + return nil +} diff --git a/pkg/webhook/mutating_webhook.go b/pkg/webhook/mutating_webhook.go index d578afb..5fba6f9 100644 --- a/pkg/webhook/mutating_webhook.go +++ b/pkg/webhook/mutating_webhook.go @@ -2,21 +2,26 @@ package webhook import ( "context" + "encoding/json" + "net/http" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) -// Mutator specifies the interface for a mutating webhook. +// Mutator specifies the interface for a generic mutating webhook. type Mutator interface { // Mutate yields a response to an mutating AdmissionRequest. Mutate(context.Context, admission.Request) admission.Response } +// ensure MutatingWebhook implements Mutator +var _ Mutator = &MutatingWebhook{} + // MutatingWebhook is a generic mutating admission webhook. type MutatingWebhook struct { - Client client.Client - Decoder *admission.Decoder + InjectedClient + InjectedDecoder } // Mutate implements the Mutator interface. @@ -24,18 +29,6 @@ func (m *MutatingWebhook) Mutate(_ context.Context, _ admission.Request) admissi return admission.Allowed("") } -// InjectDecoder implements the admission.DecoderInjector interface. -func (m *MutatingWebhook) InjectDecoder(decoder *admission.Decoder) error { - m.Decoder = decoder - return nil -} - -// InjectClient implements the inject.Client interface. -func (m *MutatingWebhook) InjectClient(client client.Client) error { - m.Client = client - return nil -} - // MutateFunc is a functional interface for a generic mutating admission webhook. type MutateFunc struct { MutatingWebhook @@ -51,3 +44,34 @@ func (m *MutateFunc) Mutate(ctx context.Context, req admission.Request) admissio return m.MutatingWebhook.Mutate(ctx, req) } + +// MutateObjectFunc is a functional interface for an object mutating admission webhook. +type MutateObjectFunc struct { + MutatingWebhook + + Func func(context.Context, admission.Request, runtime.Object) error +} + +// Mutate implements the Mutator interface by calling the Func using the request's runtime.Object. +func (m *MutateObjectFunc) Mutate(ctx context.Context, req admission.Request) admission.Response { + if m.Func != nil { + return MutateObjectByFunc(ctx, req, m.Func) + } + + return m.MutatingWebhook.Mutate(ctx, req) +} + +func MutateObjectByFunc(ctx context.Context, req admission.Request, f func(context.Context, admission.Request, runtime.Object) error) admission.Response { + obj := req.Object.Object + err := f(ctx, req, obj) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + marshalled, err := json.Marshal(obj) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + return admission.PatchResponseFromRaw(req.Object.Raw, marshalled) +} diff --git a/pkg/webhook/mutating_webhook_test.go b/pkg/webhook/mutating_webhook_test.go index 1d32896..894bd0b 100644 --- a/pkg/webhook/mutating_webhook_test.go +++ b/pkg/webhook/mutating_webhook_test.go @@ -2,11 +2,17 @@ package webhook_test import ( "context" + "encoding/json" + "errors" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/snorwin/k8s-generic-webhook/pkg/webhook" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -25,4 +31,55 @@ var _ = Describe("Mutating Webhook", func() { Ω(result.Allowed).Should(BeFalse()) }) }) + Context("MutateObjectFunc", func() { + var ( + n *corev1.Namespace + raw []byte + ) + BeforeEach(func() { + var err error + n = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + } + raw, err = json.Marshal(n) + Ω(err).ShouldNot(HaveOccurred()) + }) + It("should by default allow all", func() { + result := (&webhook.MutateObjectFunc{}).Mutate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + }) + It("should use defined functions", func() { + result := (&webhook.MutateObjectFunc{ + Func: func(ctx context.Context, _ admission.Request, object runtime.Object) error { + Ω(object).Should(Equal(n)) + return nil + }, + }).Mutate(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Object: n, + Raw: raw, + }, + }, + }) + Ω(result.Allowed).Should(BeTrue()) + }) + It("should deny if error", func() { + result := (&webhook.MutateObjectFunc{ + Func: func(ctx context.Context, _ admission.Request, _ runtime.Object) error { + return errors.New("") + }, + }).Mutate(context.TODO(), admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Object: runtime.RawExtension{ + Object: n, + Raw: raw, + }, + }, + }) + Ω(result.Allowed).Should(BeFalse()) + }) + }) }) diff --git a/pkg/webhook/validating_webhook.go b/pkg/webhook/validating_webhook.go index cc5b0ed..a92b322 100644 --- a/pkg/webhook/validating_webhook.go +++ b/pkg/webhook/validating_webhook.go @@ -2,8 +2,9 @@ package webhook import ( "context" + "net/http" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -17,10 +18,13 @@ type Validator interface { ValidateDelete(context.Context, admission.Request) admission.Response } +// ensure ValidatingWebhook implements Validator +var _ Validator = &ValidatingWebhook{} + // ValidatingWebhook is a generic validating admission webhook. type ValidatingWebhook struct { - Client client.Client - Decoder *admission.Decoder + InjectedClient + InjectedDecoder } // ValidateCreate implements the Validator interface. @@ -38,18 +42,6 @@ func (v *ValidatingWebhook) ValidateDelete(_ context.Context, _ admission.Reques return admission.Allowed("") } -// InjectDecoder implements the admission.DecoderInjector interface. -func (v *ValidatingWebhook) InjectDecoder(decoder *admission.Decoder) error { - v.Decoder = decoder - return nil -} - -// InjectClient implements the inject.Client interface. -func (v *ValidatingWebhook) InjectClient(client client.Client) error { - v.Client = client - return nil -} - // ValidateFuncs is a functional interface for a generic validating admission webhook. type ValidateFuncs struct { ValidatingWebhook @@ -85,3 +77,63 @@ func (v *ValidateFuncs) ValidateDelete(ctx context.Context, req admission.Reques return v.ValidatingWebhook.ValidateDelete(ctx, req) } + +// ValidateObjectFuncs is a functional interface for an object validating admission webhook. +type ValidateObjectFuncs struct { + ValidatingWebhook + + CreateFunc func(context.Context, admission.Request, runtime.Object) error + UpdateFunc func(context.Context, admission.Request, runtime.Object, runtime.Object) error + DeleteFunc func(context.Context, admission.Request, runtime.Object) error +} + +// ValidateCreate implements the Validator interface by calling the CreateFunc using the request's runtime.Object. +func (v *ValidateObjectFuncs) ValidateCreate(ctx context.Context, req admission.Request) admission.Response { + if v.CreateFunc != nil { + return ValidateCreateObjectByFunc(ctx, req, v.CreateFunc) + } + + return v.ValidatingWebhook.ValidateCreate(ctx, req) +} + +// ValidateUpdate implements the Validator interface by calling the UpdateFunc using the request's runtime.Object. +func (v *ValidateObjectFuncs) ValidateUpdate(ctx context.Context, req admission.Request) admission.Response { + if v.UpdateFunc != nil { + return ValidateUpdateObjectByFunc(ctx, req, v.UpdateFunc) + } + + return v.ValidatingWebhook.ValidateUpdate(ctx, req) +} + +// ValidateDelete implements the Validator interface by calling the DeleteFunc using the request's runtime.Object. +func (v *ValidateObjectFuncs) ValidateDelete(ctx context.Context, req admission.Request) admission.Response { + if v.DeleteFunc != nil { + return ValidateDeleteObjectByFunc(ctx, req, v.DeleteFunc) + } + + return v.ValidatingWebhook.ValidateDelete(ctx, req) +} + +func ValidateCreateObjectByFunc(ctx context.Context, req admission.Request, f func(context.Context, admission.Request, runtime.Object) error) admission.Response { + err := f(ctx, req, req.Object.Object) + if err != nil { + return admission.Errored(http.StatusForbidden, err) + } + return admission.Allowed("") +} + +func ValidateUpdateObjectByFunc(ctx context.Context, req admission.Request, f func(context.Context, admission.Request, runtime.Object, runtime.Object) error) admission.Response { + err := f(ctx, req, req.Object.Object, req.OldObject.Object) + if err != nil { + return admission.Errored(http.StatusForbidden, err) + } + return admission.Allowed("") +} + +func ValidateDeleteObjectByFunc(ctx context.Context, req admission.Request, f func(context.Context, admission.Request, runtime.Object) error) admission.Response { + err := f(ctx, req, req.Object.Object) + if err != nil { + return admission.Errored(http.StatusForbidden, err) + } + return admission.Allowed("") +} diff --git a/pkg/webhook/validating_webhook_test.go b/pkg/webhook/validating_webhook_test.go index 45bb331..ecc0a17 100644 --- a/pkg/webhook/validating_webhook_test.go +++ b/pkg/webhook/validating_webhook_test.go @@ -2,11 +2,13 @@ package webhook_test import ( "context" + "errors" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/snorwin/k8s-generic-webhook/pkg/webhook" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -28,5 +30,54 @@ var _ = Describe("Validating Webhook", func() { }).ValidateCreate(context.TODO(), admission.Request{}) Ω(result.Allowed).Should(BeFalse()) }) + It("should by default allow all", func() { + result := (&webhook.ValidateObjectFuncs{}).ValidateCreate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + result = (&webhook.ValidateObjectFuncs{}).ValidateUpdate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + result = (&webhook.ValidateObjectFuncs{}).ValidateDelete(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + }) + It("should use defined functions", func() { + result := (&webhook.ValidateObjectFuncs{ + CreateFunc: func(ctx context.Context, _ admission.Request, _ runtime.Object) error { + return errors.New("") + }, + }).ValidateCreate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeFalse()) + result = (&webhook.ValidateObjectFuncs{ + CreateFunc: func(ctx context.Context, _ admission.Request, _ runtime.Object) error { + return nil + }, + }).ValidateCreate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + + result = (&webhook.ValidateObjectFuncs{ + UpdateFunc: func(ctx context.Context, _ admission.Request, _ runtime.Object, _ runtime.Object) error { + return errors.New("") + }, + }).ValidateUpdate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeFalse()) + result = (&webhook.ValidateObjectFuncs{ + UpdateFunc: func(ctx context.Context, _ admission.Request, _ runtime.Object, _ runtime.Object) error { + return nil + }, + }).ValidateUpdate(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + + result = (&webhook.ValidateObjectFuncs{ + DeleteFunc: func(ctx context.Context, _ admission.Request, _ runtime.Object) error { + return errors.New("") + }, + }).ValidateDelete(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeFalse()) + result = (&webhook.ValidateObjectFuncs{ + DeleteFunc: func(ctx context.Context, _ admission.Request, _ runtime.Object) error { + return nil + }, + }).ValidateDelete(context.TODO(), admission.Request{}) + Ω(result.Allowed).Should(BeTrue()) + }) + }) }) diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index d4b7d37..e0f1a55 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -40,21 +40,25 @@ func (blder *Builder) For(apiType runtime.Object) *Builder { return blder } +// WithMutatePath overrides the mutate path of the webhook func (blder *Builder) WithMutatePath(path string) *Builder { blder.pathMutate = path return blder } +// WithValidatePath overrides the validate path of the webhook func (blder *Builder) WithValidatePath(path string) *Builder { blder.pathValidate = path return blder } +// WithMutatePrefix sets a custom prefix for the mutate path of the webhook, default is '/mutate-' func (blder *Builder) WithMutatePrefix(prefix string) *Builder { blder.prefixMutate = prefix return blder } +// WithValidatePrefix sets a custom prefix for the mutate path of the webhook, default is '/validate-' func (blder *Builder) WithValidatePrefix(prefix string) *Builder { blder.prefixMutate = prefix return blder @@ -171,6 +175,7 @@ func generatePath(override string, prefix string, gvk schema.GroupVersionKind) s if override != "" { return override } + return prefix + strings.Replace(gvk.Group, ".", "-", -1) + "-" + gvk.Version + "-" + strings.ToLower(gvk.Kind) }