From 2e1a87d149fbe65ee9714ccbd5342bea8cf9a2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Wed, 20 Apr 2022 17:01:02 +0200 Subject: [PATCH] refactor: image utils (#3630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Charles-Edouard Brétéché --- api/kyverno/v1/zz_generated.deepcopy.go | 12 +- pkg/engine/context/context.go | 15 ++- pkg/engine/imageVerify.go | 14 +-- pkg/utils/image/infos.go | 6 +- pkg/utils/image/infos_test.go | 2 +- pkg/utils/kube/image.go | 22 ++-- pkg/utils/kube/image_test.go | 161 +++++++++++++++++------- 7 files changed, 151 insertions(+), 81 deletions(-) diff --git a/api/kyverno/v1/zz_generated.deepcopy.go b/api/kyverno/v1/zz_generated.deepcopy.go index d11e5e2687..c0714eb4e0 100755 --- a/api/kyverno/v1/zz_generated.deepcopy.go +++ b/api/kyverno/v1/zz_generated.deepcopy.go @@ -974,19 +974,13 @@ func (in *Rule) DeepCopyInto(out *Rule) { in, out := &in.ImageExtractors, &out.ImageExtractors *out = make(kube.ImageExtractorConfigs, len(*in)) for key, val := range *in { - var outVal []*kube.ImageExtractorConfig + var outVal []kube.ImageExtractorConfig if val == nil { (*out)[key] = nil } else { in, out := &val, &outVal - *out = make([]*kube.ImageExtractorConfig, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(kube.ImageExtractorConfig) - **out = **in - } - } + *out = make([]kube.ImageExtractorConfig, len(*in)) + copy(*out, *in) } (*out)[key] = outVal } diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go index e9ffb3ada2..2003f79037 100644 --- a/pkg/engine/context/context.go +++ b/pkg/engine/context/context.go @@ -8,7 +8,6 @@ import ( jsonpatch "github.com/evanphx/json-patch/v5" kyverno "github.com/kyverno/kyverno/api/kyverno/v1" pkgcommon "github.com/kyverno/kyverno/pkg/common" - imageutils "github.com/kyverno/kyverno/pkg/utils/image" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" "github.com/pkg/errors" admissionv1 "k8s.io/api/admission/v1" @@ -60,17 +59,17 @@ type Interface interface { AddElement(data map[string]interface{}, index int) error // AddImageInfo adds image info to the context - AddImageInfo(info imageutils.ImageInfo) error + AddImageInfo(info kubeutils.ImageInfo) error // AddImageInfos adds image infos to the context AddImageInfos(resource *unstructured.Unstructured) error // ImageInfo returns image infos present in the context - ImageInfo() map[string]map[string]imageutils.ImageInfo + ImageInfo() map[string]map[string]kubeutils.ImageInfo // GenerateCustomImageInfo returns image infos as defined by a custom image extraction config // and updates the context - GenerateCustomImageInfo(resource *unstructured.Unstructured, imageExtractorConfigs kubeutils.ImageExtractorConfigs) (map[string]map[string]imageutils.ImageInfo, error) + GenerateCustomImageInfo(resource *unstructured.Unstructured, imageExtractorConfigs kubeutils.ImageExtractorConfigs) (map[string]map[string]kubeutils.ImageInfo, error) // Checkpoint creates a copy of the current internal state and pushes it into a stack of stored states. Checkpoint() @@ -92,7 +91,7 @@ type context struct { mutex sync.RWMutex jsonRaw []byte jsonRawCheckpoints [][]byte - images map[string]map[string]imageutils.ImageInfo + images map[string]map[string]kubeutils.ImageInfo } // NewContext returns a new context @@ -215,7 +214,7 @@ func (ctx *context) AddElement(data map[string]interface{}, index int) error { return addToContext(ctx, data) } -func (ctx *context) AddImageInfo(info imageutils.ImageInfo) error { +func (ctx *context) AddImageInfo(info kubeutils.ImageInfo) error { data := map[string]interface{}{ "image": info.String(), "registry": info.Registry, @@ -239,7 +238,7 @@ func (ctx *context) AddImageInfos(resource *unstructured.Unstructured) error { return addToContext(ctx, images, "images") } -func (ctx *context) GenerateCustomImageInfo(resource *unstructured.Unstructured, imageExtractorConfigs kubeutils.ImageExtractorConfigs) (map[string]map[string]imageutils.ImageInfo, error) { +func (ctx *context) GenerateCustomImageInfo(resource *unstructured.Unstructured, imageExtractorConfigs kubeutils.ImageExtractorConfigs) (map[string]map[string]kubeutils.ImageInfo, error) { images, err := kubeutils.ExtractImagesFromResource(*resource, imageExtractorConfigs) if err != nil { return nil, err @@ -250,7 +249,7 @@ func (ctx *context) GenerateCustomImageInfo(resource *unstructured.Unstructured, return images, addToContext(ctx, images, "images") } -func (ctx *context) ImageInfo() map[string]map[string]imageutils.ImageInfo { +func (ctx *context) ImageInfo() map[string]map[string]kubeutils.ImageInfo { return ctx.images } diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index 08a12c3363..7555e1d9c9 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -15,7 +15,7 @@ import ( engineUtils "github.com/kyverno/kyverno/pkg/engine/utils" "github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/registryclient" - imageUtils "github.com/kyverno/kyverno/pkg/utils/image" + kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -133,7 +133,7 @@ type imageVerifier struct { resp *response.EngineResponse } -func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]map[string]imageUtils.ImageInfo) { +func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]map[string]kubeutils.ImageInfo) { // for backward compatibility imageVerify = imageVerify.Convert() @@ -180,7 +180,7 @@ func imageMatches(image string, imagePatterns []string) bool { return false } -func (iv *imageVerifier) verifySignatures(imageVerify *v1.ImageVerification, imageInfo imageUtils.ImageInfo) (*response.RuleResponse, string) { +func (iv *imageVerifier) verifySignatures(imageVerify *v1.ImageVerification, imageInfo kubeutils.ImageInfo) (*response.RuleResponse, string) { image := imageInfo.String() iv.logger.Info("verifying image", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations)) @@ -295,7 +295,7 @@ func (iv *imageVerifier) buildOptionsAndPath(attestor *v1.Attestor, imageVerify return opts, path } -func (iv *imageVerifier) patchDigest(path string, imageInfo imageUtils.ImageInfo, digest string, ruleResp *response.RuleResponse) { +func (iv *imageVerifier) patchDigest(path string, imageInfo kubeutils.ImageInfo, digest string, ruleResp *response.RuleResponse) { if imageInfo.Digest == "" { patch, err := makeAddDigestPatch(path, imageInfo, digest) if err != nil { @@ -307,7 +307,7 @@ func (iv *imageVerifier) patchDigest(path string, imageInfo imageUtils.ImageInfo } } -func makeAddDigestPatch(path string, imageInfo imageUtils.ImageInfo, digest string) ([]byte, error) { +func makeAddDigestPatch(path string, imageInfo kubeutils.ImageInfo, digest string) ([]byte, error) { var patch = make(map[string]interface{}) patch["op"] = "replace" patch["path"] = path @@ -315,7 +315,7 @@ func makeAddDigestPatch(path string, imageInfo imageUtils.ImageInfo, digest stri return json.Marshal(patch) } -func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo imageUtils.ImageInfo) *response.RuleResponse { +func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo kubeutils.ImageInfo) *response.RuleResponse { image := imageInfo.String() start := time.Now() @@ -367,7 +367,7 @@ func buildStatementMap(statements []map[string]interface{}) map[string][]map[str return results } -func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]interface{}, img imageUtils.ImageInfo) (bool, error) { +func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]interface{}, img kubeutils.ImageInfo) (bool, error) { if len(a.Conditions) == 0 { return true, nil } diff --git a/pkg/utils/image/infos.go b/pkg/utils/image/infos.go index ee75393e94..29e8f3aa0f 100644 --- a/pkg/utils/image/infos.go +++ b/pkg/utils/image/infos.go @@ -22,9 +22,6 @@ type ImageInfo struct { // Digest is the image digest portion e.g. `sha256:128c6e3534b842a2eec139999b8ce8aa9a2af9907e2b9269550809d18cd832a3` Digest string `json:"digest,omitempty"` - - // Pointer is the path to the image object in the resource - Pointer string `json:"-"` } func (i *ImageInfo) String() string { @@ -36,7 +33,7 @@ func (i *ImageInfo) String() string { } } -func GetImageInfo(image string, pointer string) (*ImageInfo, error) { +func GetImageInfo(image string) (*ImageInfo, error) { image = addDefaultDomain(image) ref, err := reference.Parse(image) if err != nil { @@ -64,7 +61,6 @@ func GetImageInfo(image string, pointer string) (*ImageInfo, error) { Path: path, Tag: tag, Digest: digest, - Pointer: pointer, }, nil } diff --git a/pkg/utils/image/infos_test.go b/pkg/utils/image/infos_test.go index f608e855c1..307fe761be 100644 --- a/pkg/utils/image/infos_test.go +++ b/pkg/utils/image/infos_test.go @@ -62,7 +62,7 @@ func Test_GetImageInfo(t *testing.T) { } func validateImageInfo(t *testing.T, raw, name, path, registry, tag, digest, str string) { - i1, err := GetImageInfo(raw, "") + i1, err := GetImageInfo(raw) assert.Nil(t, err) assert.Equal(t, name, i1.Name) assert.Equal(t, path, i1.Path) diff --git a/pkg/utils/kube/image.go b/pkg/utils/kube/image.go index 422d5d02a6..4b3bfeb08e 100644 --- a/pkg/utils/kube/image.go +++ b/pkg/utils/kube/image.go @@ -9,7 +9,13 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -type ImageExtractorConfigs map[string][]*ImageExtractorConfig +type ImageInfo struct { + imageutils.ImageInfo + // Pointer is the path to the image object in the resource + Pointer string `json:"-"` +} + +type ImageExtractorConfigs map[string][]ImageExtractorConfig type ImageExtractorConfig struct { // Path is the path to the object containing the image field in a custom resource. @@ -52,15 +58,15 @@ type imageExtractor struct { Name string } -func (i *imageExtractor) ExtractFromResource(resource interface{}) (map[string]imageutils.ImageInfo, error) { - imageInfo := map[string]imageutils.ImageInfo{} +func (i *imageExtractor) ExtractFromResource(resource interface{}) (map[string]ImageInfo, error) { + imageInfo := map[string]ImageInfo{} if err := extract(resource, []string{}, i.Key, i.Value, i.Fields, &imageInfo); err != nil { return nil, err } return imageInfo, nil } -func extract(obj interface{}, path []string, keyPath, valuePath string, fields []string, imageInfos *map[string]imageutils.ImageInfo) error { +func extract(obj interface{}, path []string, keyPath, valuePath string, fields []string, imageInfos *map[string]ImageInfo) error { if obj == nil { return nil } @@ -100,10 +106,10 @@ func extract(obj interface{}, path []string, keyPath, valuePath string, fields [ if !ok { return fmt.Errorf("invalid value") } - if imageInfo, err := imageutils.GetImageInfo(value, pointer); err != nil { + if imageInfo, err := imageutils.GetImageInfo(value); err != nil { return fmt.Errorf("invalid image %s", value) } else { - (*imageInfos)[key] = *imageInfo + (*imageInfos)[key] = ImageInfo{*imageInfo, pointer} } return nil } @@ -160,8 +166,8 @@ func lookupImageExtractor(kind string, configs ImageExtractorConfigs) []imageExt return registeredExtractors[kind] } -func ExtractImagesFromResource(resource unstructured.Unstructured, configs ImageExtractorConfigs) (map[string]map[string]imageutils.ImageInfo, error) { - infos := map[string]map[string]imageutils.ImageInfo{} +func ExtractImagesFromResource(resource unstructured.Unstructured, configs ImageExtractorConfigs) (map[string]map[string]ImageInfo, error) { + infos := map[string]map[string]ImageInfo{} for _, extractor := range lookupImageExtractor(resource.GetKind(), configs) { if infoMap, err := extractor.ExtractFromResource(resource.Object); err != nil { return nil, err diff --git a/pkg/utils/kube/image_test.go b/pkg/utils/kube/image_test.go index 60337dc0e1..9f4f57f905 100644 --- a/pkg/utils/kube/image_test.go +++ b/pkg/utils/kube/image_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/kyverno/kyverno/pkg/engine/utils" - "github.com/kyverno/kyverno/pkg/utils/image" imageutils "github.com/kyverno/kyverno/pkg/utils/image" "gotest.tools/assert" ) @@ -13,106 +12,182 @@ func Test_extractImageInfo(t *testing.T) { tests := []struct { extractionConfig ImageExtractorConfigs raw []byte - images map[string]map[string]imageutils.ImageInfo + images map[string]map[string]ImageInfo }{ { raw: []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"initContainers": [{"name": "init","image": "index.docker.io/busybox:v1.2.3"}],"containers": [{"name": "nginx","image": "nginx:latest"}], "ephemeralContainers": [{"name": "ephemeral", "image":"test/nginx:latest"}]}}`), - images: map[string]map[string]image.ImageInfo{ + images: map[string]map[string]ImageInfo{ "initContainers": { - "init": {Registry: "index.docker.io", Name: "busybox", Path: "busybox", Tag: "v1.2.3", Pointer: "/spec/initContainers/0/image"}, + "init": { + imageutils.ImageInfo{ + Registry: "index.docker.io", + Name: "busybox", + Path: "busybox", + Tag: "v1.2.3", + }, + "/spec/initContainers/0/image", + }, }, "containers": { - "nginx": {Registry: "docker.io", Name: "nginx", Path: "nginx", Tag: "latest", Pointer: "/spec/containers/0/image"}, + "nginx": { + imageutils.ImageInfo{ + Registry: "docker.io", + Name: "nginx", + Path: "nginx", + Tag: "latest", + }, + "/spec/containers/0/image", + }, }, "ephemeralContainers": { - "ephemeral": {Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", Pointer: "/spec/ephemeralContainers/0/image"}, + "ephemeral": { + imageutils.ImageInfo{ + Registry: "docker.io", + Name: "nginx", + Path: "test/nginx", + Tag: "latest", + }, + "/spec/ephemeralContainers/0/image", + }, }, }, }, { raw: []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"containers": [{"name": "nginx","image": "test/nginx:latest"}]}}`), - images: map[string]map[string]imageutils.ImageInfo{ + images: map[string]map[string]ImageInfo{ "containers": { - "nginx": {Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", Pointer: "/spec/containers/0/image"}, + "nginx": { + imageutils.ImageInfo{ + Registry: "docker.io", + Name: "nginx", + Path: "test/nginx", + Tag: "latest", + }, + "/spec/containers/0/image", + }, }, }, }, { raw: []byte(`{"apiVersion": "apps/v1","kind": "Deployment","metadata": {"name": "myapp"},"spec": {"selector": {"matchLabels": {"app": "myapp"}},"template": {"metadata": {"labels": {"app": "myapp"}},"spec": {"initContainers": [{"name": "init","image": "fictional.registry.example:10443/imagename:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}],"containers": [{"name": "myapp","image": "fictional.registry.example:10443/imagename"}],"ephemeralContainers": [{"name": "ephemeral","image": "fictional.registry.example:10443/imagename:tag@sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}] }}}}`), - images: map[string]map[string]imageutils.ImageInfo{ + images: map[string]map[string]ImageInfo{ "initContainers": { - "init": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", Pointer: "/spec/template/spec/initContainers/0/image"}, + "init": { + imageutils.ImageInfo{ + Registry: "fictional.registry.example:10443", + Name: "imagename", + Path: "imagename", + Tag: "tag", + Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + "/spec/template/spec/initContainers/0/image", + }, }, "containers": { - "myapp": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "latest", Pointer: "/spec/template/spec/containers/0/image"}, + "myapp": { + imageutils.ImageInfo{ + Registry: "fictional.registry.example:10443", + Name: "imagename", + Path: "imagename", + Tag: "latest", + }, + "/spec/template/spec/containers/0/image", + }, }, "ephemeralContainers": { - "ephemeral": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Pointer: "/spec/template/spec/ephemeralContainers/0/image"}, + "ephemeral": { + imageutils.ImageInfo{ + Registry: "fictional.registry.example:10443", + Name: "imagename", + Path: "imagename", + Tag: "tag", + Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + }, + "/spec/template/spec/ephemeralContainers/0/image", + }, }, }, }, { raw: []byte(`{"apiVersion": "batch/v1beta1","kind": "CronJob","metadata": {"name": "hello"},"spec": {"schedule": "*/1 * * * *","jobTemplate": {"spec": {"template": {"spec": {"containers": [{"name": "hello","image": "test.example.com/test/my-app:v2"}]}}}}}}`), - images: map[string]map[string]imageutils.ImageInfo{ + images: map[string]map[string]ImageInfo{ "containers": { - "hello": {Registry: "test.example.com", Name: "my-app", Path: "test/my-app", Tag: "v2", Pointer: "/spec/jobTemplate/spec/template/spec/containers/0/image"}, + "hello": { + imageutils.ImageInfo{ + Registry: "test.example.com", + Name: "my-app", + Path: "test/my-app", + Tag: "v2", + }, + "/spec/jobTemplate/spec/template/spec/containers/0/image", + }, }, }, }, { extractionConfig: ImageExtractorConfigs{ - "Task": []*ImageExtractorConfig{ + "Task": []ImageExtractorConfig{ {Path: "/spec/steps/*/image"}, }, }, raw: []byte(`{"apiVersion":"tekton.dev/v1beta1","kind":"Task","metadata":{"name":"example-task-name"},"spec":{"params":[{"name":"pathToDockerFile","type":"string","description":"The path to the dockerfile to build","default":"/workspace/workspace/Dockerfile"}],"resources":{"inputs":[{"name":"workspace","type":"git"}],"outputs":[{"name":"builtImage","type":"image"}]},"steps":[{"name":"ubuntu-example","image":"ubuntu","args":["ubuntu-build-example","SECRETS-example.md"]},{"image":"gcr.io/example-builders/build-example","command":["echo"],"args":["$(params.pathToDockerFile)"]},{"name":"dockerfile-pushexample","image":"gcr.io/example-builders/push-example","args":["push","$(resources.outputs.builtImage.url)"],"volumeMounts":[{"name":"docker-socket-example","mountPath":"/var/run/docker.sock"}]}],"volumes":[{"name":"example-volume","emptyDir":{}}]}}`), - images: map[string]map[string]imageutils.ImageInfo{ + images: map[string]map[string]ImageInfo{ "custom": { "/spec/steps/0/image": { - Registry: "docker.io", - Name: "ubuntu", - Path: "ubuntu", - Tag: "latest", - Pointer: "/spec/steps/0/image", + imageutils.ImageInfo{ + Registry: "docker.io", + Name: "ubuntu", + Path: "ubuntu", + Tag: "latest", + }, + "/spec/steps/0/image", }, "/spec/steps/1/image": { - Registry: "gcr.io", - Name: "build-example", - Path: "example-builders/build-example", - Tag: "latest", - Pointer: "/spec/steps/1/image", + imageutils.ImageInfo{ + Registry: "gcr.io", + Name: "build-example", + Path: "example-builders/build-example", + Tag: "latest", + }, + "/spec/steps/1/image", }, "/spec/steps/2/image": { - Registry: "gcr.io", - Name: "push-example", - Path: "example-builders/push-example", - Tag: "latest", - Pointer: "/spec/steps/2/image", + imageutils.ImageInfo{ + Registry: "gcr.io", + Name: "push-example", + Path: "example-builders/push-example", + Tag: "latest", + }, + "/spec/steps/2/image", }, }, }, }, { extractionConfig: ImageExtractorConfigs{ - "Task": []*ImageExtractorConfig{ + "Task": []ImageExtractorConfig{ {Name: "steps", Path: "/spec/steps/*", Value: "image", Key: "name"}, }, }, raw: []byte(`{"apiVersion":"tekton.dev/v1beta1","kind":"Task","metadata":{"name":"example-task-name"},"spec":{"steps":[{"name":"ubuntu-example","image":"ubuntu","args":["ubuntu-build-example","SECRETS-example.md"]},{"name":"dockerfile-pushexample","image":"gcr.io/example-builders/push-example","args":["push","$(resources.outputs.builtImage.url)"]}]}}`), - images: map[string]map[string]imageutils.ImageInfo{ + images: map[string]map[string]ImageInfo{ "steps": { "dockerfile-pushexample": { - Registry: "gcr.io", - Name: "push-example", - Path: "example-builders/push-example", - Tag: "latest", - Pointer: "/spec/steps/1/image", + imageutils.ImageInfo{ + Registry: "gcr.io", + Name: "push-example", + Path: "example-builders/push-example", + Tag: "latest", + }, + "/spec/steps/1/image", }, "ubuntu-example": { - Registry: "docker.io", - Name: "ubuntu", - Path: "ubuntu", - Tag: "latest", - Pointer: "/spec/steps/0/image", + imageutils.ImageInfo{ + Registry: "docker.io", + Name: "ubuntu", + Path: "ubuntu", + Tag: "latest", + }, + "/spec/steps/0/image", }, }, },