Skip to content

Commit c8dbbbe

Browse files
authored
Add implmentation for tkr-infra-machine-webhook (vmware-tanzu#1638)
Signed-off-by: PremKumar Kalle <pkalle@vmware.com> Review comments addressed Signed-off-by: PremKumar Kalle <pkalle@vmware.com>
1 parent 87e5e4f commit c8dbbbe

File tree

7 files changed

+445
-2
lines changed

7 files changed

+445
-2
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ e2e-tkgpackageclient-docker: $(GINKGO) generate-embedproviders ## Run ginkgo tkg
670670
# These are the components in this repo that need to have a docker image built.
671671
# This variable refers to directory paths that contain a Makefile with `docker-build`, `docker-publish` and
672672
# `kbld-image-replace` targets that can build and push a docker image for that component.
673-
COMPONENTS := pkg/v1/sdk/features addons cliplugins
673+
COMPONENTS := pkg/v1/sdk/features addons cliplugins pkg/v2/tkr/webhook/infra-machine
674674

675675
.PHONY: docker-build
676676
docker-build: TARGET=docker-build

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ require (
9595
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
9696
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
9797
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
98+
gomodules.xyz/jsonpatch/v2 v2.2.0
9899
google.golang.org/api v0.62.0
99100
google.golang.org/grpc v1.42.0
100101
gopkg.in/yaml.v2 v2.4.0
@@ -226,7 +227,6 @@ require (
226227
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
227228
golang.org/x/tools v0.1.5 // indirect
228229
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
229-
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
230230
google.golang.org/appengine v1.6.7 // indirect
231231
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
232232
google.golang.org/protobuf v1.27.1 // indirect
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright 2022 VMware, Inc. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# Build from publicly reachable source by default, but allow people to re-build images on
5+
# top of their own trusted images.
6+
ARG BUILDER_BASE_IMAGE=golang:1.17
7+
8+
# Build the manager binary
9+
FROM $BUILDER_BASE_IMAGE as builder
10+
11+
WORKDIR /workspace
12+
13+
# Copy the Go Modules manifests
14+
COPY go.mod go.mod
15+
COPY go.sum go.sum
16+
RUN go mod download
17+
# cache deps before building and copying source so that we don't need to re-download as much
18+
# and so that source changes don't invalidate our downloaded layer
19+
20+
# Copy the go source
21+
COPY ./ ./
22+
23+
# Build
24+
ARG LD_FLAGS
25+
ENV LD_FLAGS="$LD_FLAGS "'-extldflags "-static"'
26+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -ldflags "$LD_FLAGS" -o manager ./main.go
27+
28+
# Use distroless as minimal base image to package the manager binary
29+
# Refer to https://github.com/GoogleContainerTools/distroless for more details
30+
FROM gcr.io/distroless/static:nonroot
31+
WORKDIR /
32+
COPY --from=builder /workspace/manager .
33+
USER nonroot:nonroot
34+
35+
ENTRYPOINT ["/manager"]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2022 VMware, Inc. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
include ../../../../../common.mk
5+
6+
IMG_DEFAULT_NAME := tkr-infra-machine-webhook
7+
IMG_DEFAULT_TAG := latest
8+
IMG_DEFAULT_NAME_TAG := $(IMG_DEFAULT_NAME):$(IMG_DEFAULT_TAG)
9+
10+
IMG_VERSION_OVERRIDE ?= $(IMG_DEFAULT_TAG)
11+
12+
ifeq ($(strip $(OCI_REGISTRY)),)
13+
IMG ?= $(IMG_DEFAULT_NAME):$(IMG_VERSION_OVERRIDE)
14+
else
15+
IMG ?= $(OCI_REGISTRY)/$(IMG_DEFAULT_NAME):$(IMG_VERSION_OVERRIDE)
16+
endif
17+
18+
all: manager
19+
20+
# Run tests
21+
test: fmt vet manifests
22+
go test ./... -coverprofile cover.out
23+
24+
# Build manager binary
25+
manager: fmt vet
26+
go build -ldflags "$(LD_FLAGS)" -o bin/manager ./main.go
27+
28+
# Run go fmt against code
29+
fmt:
30+
go fmt ./...
31+
32+
# Run go vet against code
33+
vet:
34+
go vet ./...
35+
36+
.PHONY: docker-build
37+
docker-build: ## Build docker image
38+
cd ../../../../../ && docker build -t $(IMG) -f pkg/v2/tkr/webhook/infra-machine/Dockerfile --build-arg LD_FLAGS="$(LD_FLAGS)" .
39+
40+
.PHONY: docker-publish
41+
docker-publish: ## Publish docker image
42+
docker push $(IMG)
43+
44+
.PHONY: kbld-image-replace
45+
kbld-image-replace: ## Add newImage in kbld-config.yaml
46+
cd ../../../../../hack/packages/kbld-image-replace && $(MAKE) run IMAGE=$(IMG_DEFAULT_NAME_TAG) NEW_IMAGE=$(IMG)
47+
48+
.PHONY: docker-image-names
49+
docker-image-names:
50+
@echo $(IMG)
51+
52+
.PHONY: docker-build-and-publish
53+
docker-build-and-publish: docker-build docker-publish kbld-image-replace
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2022 VMware, Inc. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package fieldsetter defines methods to set the fields of <Infra>Machine resource
5+
package fieldsetter
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
"fmt"
11+
"net/http"
12+
"strings"
13+
14+
"github.com/go-logr/logr"
15+
"github.com/pkg/errors"
16+
"gopkg.in/yaml.v2"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
19+
)
20+
21+
const (
22+
osImageRefAnnotationKey = "run.tanzu.vmware.com/os-image-ref"
23+
)
24+
25+
// FieldSetter mutates <infra>Machine
26+
type FieldSetter struct {
27+
decoder *admission.Decoder
28+
Log logr.Logger
29+
FieldPathMap map[string]string
30+
}
31+
32+
// Handle will handle the infra admission request
33+
func (fs *FieldSetter) Handle(ctx context.Context, req admission.Request) admission.Response { // nolint:gocritic // suppress linter error: hugeParam: req is heavy (400 bytes); consider passing by pointer (gocritic)
34+
fs.Log.Info("Received the infra machine admission request")
35+
infraMachine := &unstructured.Unstructured{}
36+
err := fs.decoder.Decode(req, infraMachine)
37+
if err != nil {
38+
fs.Log.Error(err, "Failed to decode infra machine admission request")
39+
return admission.Errored(http.StatusBadRequest, err)
40+
}
41+
42+
annotationValues, err := getOSImageRefAnnotation(infraMachine)
43+
if err != nil {
44+
fs.Log.Error(err, "Failed to get 'run.tanzu.vmware.com/os-image-ref' annotation")
45+
return admission.Errored(http.StatusBadRequest, err)
46+
}
47+
// if 'run.tanzu.vmware.com/os-image-ref' annotation is not set, nothing to update
48+
if annotationValues == nil {
49+
return admission.ValidationResponse(true, "")
50+
}
51+
52+
err = fs.setFields(infraMachine, annotationValues)
53+
if err != nil {
54+
fs.Log.Error(err, "Failed to set the fields using the 'run.tanzu.vmware.com/os-image-ref' annotation")
55+
return admission.Errored(http.StatusBadRequest, err)
56+
}
57+
58+
marshalledInfraMachine, err := json.Marshal(infraMachine)
59+
if err != nil {
60+
fs.Log.Error(err, "Failed to marshal infraMachine object")
61+
return admission.Errored(http.StatusInternalServerError, err)
62+
}
63+
return admission.PatchResponseFromRaw(req.Object.Raw, marshalledInfraMachine)
64+
}
65+
66+
func getOSImageRefAnnotation(infraMachine *unstructured.Unstructured) (map[string]interface{}, error) {
67+
annotations := infraMachine.GetAnnotations()
68+
if annotations == nil {
69+
return nil, nil
70+
}
71+
72+
osImageRefAnnotationValue, exists := annotations[osImageRefAnnotationKey]
73+
if !exists {
74+
return nil, nil
75+
}
76+
annotationValues := make(map[string]interface{}, 1)
77+
err := yaml.Unmarshal([]byte(osImageRefAnnotationValue), &annotationValues)
78+
if err != nil {
79+
return nil, errors.New("failed to unmarshal 'run.tanzu.vmware.com/os-image-ref' annotation")
80+
}
81+
return annotationValues, nil
82+
}
83+
84+
// SetFields sets the <Infra>Machine fields using the annotation values
85+
func (fs *FieldSetter) setFields(o *unstructured.Unstructured, annotationValues map[string]interface{}) error {
86+
for field, value := range annotationValues {
87+
path, exists := fs.FieldPathMap[field]
88+
if !exists {
89+
fs.Log.Info(fmt.Sprintf("os Image reference annotation value's field %q doesn't match with any entry in field path map configured", field))
90+
continue
91+
}
92+
fieldPath := strings.Split(path, ".")
93+
// check if the field path exists
94+
if _, exists, _ := unstructured.NestedFieldNoCopy(o.UnstructuredContent(), fieldPath...); !exists {
95+
fs.Log.Info(fmt.Sprintf("Field path %q doesn't exists in the request object", path))
96+
return fmt.Errorf("field path %q doesn't exists in the request object", path)
97+
}
98+
99+
err := unstructured.SetNestedField(o.UnstructuredContent(), value, fieldPath...)
100+
if err != nil {
101+
return errors.Wrapf(err, "failed to set the %q value to %v", fieldPath, value)
102+
}
103+
}
104+
return nil
105+
}
106+
107+
// InjectDecoder injects the decoder. A decoder will be automatically injected.
108+
func (fs *FieldSetter) InjectDecoder(d *admission.Decoder) error {
109+
fs.decoder = d
110+
return nil
111+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2022 VMware, Inc. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package fieldsetter
5+
6+
import (
7+
"context"
8+
"encoding/json"
9+
"testing"
10+
11+
. "github.com/onsi/ginkgo"
12+
. "github.com/onsi/gomega"
13+
14+
jsonpatch "gomodules.xyz/jsonpatch/v2"
15+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
16+
"k8s.io/client-go/kubernetes/scheme"
17+
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
18+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
19+
)
20+
21+
func TestAPIs(t *testing.T) {
22+
RegisterFailHandler(Fail)
23+
RunSpecs(t, "Infra Machine webhook test")
24+
}
25+
26+
var _ = Describe("Infra Machine webhook", func() {
27+
var (
28+
fieldPathMap map[string]string
29+
infraMachineObj *unstructured.Unstructured
30+
infraMachineAnnotation map[string]string
31+
32+
fs *FieldSetter
33+
err error
34+
)
35+
BeforeEach(func() {
36+
fs = &FieldSetter{
37+
Log: ctrllog.Log,
38+
FieldPathMap: map[string]string{},
39+
}
40+
decoder, err := admission.NewDecoder(scheme.Scheme)
41+
Expect(err).NotTo(HaveOccurred())
42+
Expect(decoder).NotTo(BeNil())
43+
err = fs.InjectDecoder(decoder)
44+
Expect(err).NotTo(HaveOccurred())
45+
})
46+
47+
Describe("fieldSetter Handle tests", func() {
48+
var req admission.Request
49+
var resp admission.Response
50+
BeforeEach(func() {
51+
infraMachineObj = &unstructured.Unstructured{
52+
Object: map[string]interface{}{
53+
"spec": map[string]interface{}{
54+
"image": map[string]interface{}{
55+
"id": "image-id-to-be-replaced",
56+
},
57+
},
58+
},
59+
}
60+
Expect(err).ToNot(HaveOccurred())
61+
})
62+
JustBeforeEach(func() {
63+
infraMachineObj.SetAnnotations(infraMachineAnnotation)
64+
req.Object.Raw, err = json.Marshal(infraMachineObj)
65+
66+
resp = fs.Handle(context.Background(), req)
67+
})
68+
Context("when the Infra machine annotations is not set", func() {
69+
BeforeEach(func() {
70+
infraMachineAnnotation = nil
71+
})
72+
It("should admit the request and should not set any fields(zero patches)", func() {
73+
Expect(resp.Allowed).To(Equal(true))
74+
Expect(len(resp.Patches)).To(Equal(0))
75+
Expect(len(resp.Result.Reason)).To(Equal(0))
76+
})
77+
})
78+
Context("when the Infra machine annotations doesn't have os Image reference annotation", func() {
79+
BeforeEach(func() {
80+
infraMachineAnnotation = map[string]string{
81+
"fake-key": "fake-value",
82+
}
83+
})
84+
It("should admit the request and should not set any fields(zero patches)", func() {
85+
Expect(resp.Allowed).To(Equal(true))
86+
Expect(len(resp.Patches)).To(Equal(0))
87+
Expect(len(string(resp.Result.Reason))).To(Equal(0))
88+
})
89+
})
90+
Context("when os Image reference annotation is not a valid yaml", func() {
91+
BeforeEach(func() {
92+
infraMachineAnnotation = map[string]string{
93+
osImageRefAnnotationKey: "invalid_yaml",
94+
}
95+
})
96+
It("should not admit the request with the message set and should not set any fields(zero patches)", func() {
97+
Expect(resp.Allowed).To(Equal(false))
98+
Expect(len(resp.Patches)).To(Equal(0))
99+
Expect(resp.Result.Message).To(ContainSubstring("failed to unmarshal 'run.tanzu.vmware.com/os-image-ref' annotation"))
100+
})
101+
})
102+
Context("when the os image reference annotation value's field doesn't have corresponding entry in the fieldPathMap", func() {
103+
BeforeEach(func() {
104+
infraMachineAnnotation = map[string]string{
105+
osImageRefAnnotationKey: "id: image-value",
106+
}
107+
fieldPathMap = map[string]string{
108+
"invalidID": "spec.image.id",
109+
}
110+
fs.FieldPathMap = fieldPathMap
111+
})
112+
It("should admit the request and should not set any fields(zero patches)", func() {
113+
Expect(resp.Allowed).To(Equal(true))
114+
Expect(len(resp.Patches)).To(Equal(0))
115+
})
116+
})
117+
Context("when the fieldPathMap value has incorrect path", func() {
118+
BeforeEach(func() {
119+
infraMachineAnnotation = map[string]string{
120+
osImageRefAnnotationKey: "id: image-value",
121+
}
122+
fieldPathMap = map[string]string{
123+
"id": "wrong.path.id",
124+
}
125+
fs.FieldPathMap = fieldPathMap
126+
})
127+
It("should not admit the request with the message set and and should not set any fields(zero patches)", func() {
128+
Expect(resp.Allowed).To(Equal(false))
129+
Expect(len(resp.Patches)).To(Equal(0))
130+
Expect(resp.Result.Message).To(ContainSubstring(`field path "wrong.path.id" doesn't exists in the request object`))
131+
})
132+
})
133+
Context("when os image reference annotation value's field matches the fieldPathMap's field and has correct path", func() {
134+
BeforeEach(func() {
135+
infraMachineAnnotation = map[string]string{
136+
osImageRefAnnotationKey: "id: image-value-updated",
137+
}
138+
fieldPathMap = map[string]string{
139+
"id": "spec.image.id",
140+
}
141+
fs.FieldPathMap = fieldPathMap
142+
})
143+
It("should admit the request with patch to update the value ", func() {
144+
Expect(resp.Allowed).To(Equal(true))
145+
Expect(len(resp.Patches)).To(Equal(1))
146+
Expect(resp.Patches[0]).To(Equal(jsonpatch.Operation{
147+
Operation: "replace",
148+
Path: "/spec/image/id",
149+
Value: "image-value-updated"}))
150+
})
151+
})
152+
})
153+
})

0 commit comments

Comments
 (0)