mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-31 03:45:17 +00:00
Refactor image extraction to allow extracting custom resources (#3572)
* refactor: image extraction Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com> * Refactor image extraction to allow extracting custom resources Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net> Co-authored-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
This commit is contained in:
parent
585b0f17a6
commit
6f7bd7451b
8 changed files with 401 additions and 356 deletions
|
@ -8,6 +8,8 @@ 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"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
@ -58,13 +60,13 @@ type Interface interface {
|
|||
AddElement(data map[string]interface{}, index int) error
|
||||
|
||||
// AddImageInfo adds image info to the context
|
||||
AddImageInfo(info *ImageInfo) error
|
||||
AddImageInfo(info imageutils.ImageInfo) error
|
||||
|
||||
// AddImageInfos adds image infos to the context
|
||||
AddImageInfos(resource *unstructured.Unstructured) error
|
||||
|
||||
// ImageInfo returns image infos present in the context
|
||||
ImageInfo() *Images
|
||||
ImageInfo() map[string]map[string]imageutils.ImageInfo
|
||||
|
||||
// Checkpoint creates a copy of the current internal state and pushes it into a stack of stored states.
|
||||
Checkpoint()
|
||||
|
@ -86,7 +88,7 @@ type context struct {
|
|||
mutex sync.RWMutex
|
||||
jsonRaw []byte
|
||||
jsonRawCheckpoints [][]byte
|
||||
images *Images
|
||||
images map[string]map[string]imageutils.ImageInfo
|
||||
}
|
||||
|
||||
// NewContext returns a new context
|
||||
|
@ -209,7 +211,7 @@ func (ctx *context) AddElement(data map[string]interface{}, index int) error {
|
|||
return addToContext(ctx, data)
|
||||
}
|
||||
|
||||
func (ctx *context) AddImageInfo(info *ImageInfo) error {
|
||||
func (ctx *context) AddImageInfo(info imageutils.ImageInfo) error {
|
||||
data := map[string]interface{}{
|
||||
"image": info.String(),
|
||||
"registry": info.Registry,
|
||||
|
@ -222,16 +224,18 @@ func (ctx *context) AddImageInfo(info *ImageInfo) error {
|
|||
}
|
||||
|
||||
func (ctx *context) AddImageInfos(resource *unstructured.Unstructured) error {
|
||||
initContainersImgs, containersImgs, ephemeralContainersImgs := extractImageInfo(resource, logger)
|
||||
if len(initContainersImgs) == 0 && len(containersImgs) == 0 && len(ephemeralContainersImgs) == 0 {
|
||||
images, err := kubeutils.ExtractImagesFromResource(*resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(images) == 0 {
|
||||
return nil
|
||||
}
|
||||
images := newImages(initContainersImgs, containersImgs, ephemeralContainersImgs)
|
||||
ctx.images = &images
|
||||
ctx.images = images
|
||||
return addToContext(ctx, images, "images")
|
||||
}
|
||||
|
||||
func (ctx *context) ImageInfo() *Images {
|
||||
func (ctx *context) ImageInfo() map[string]map[string]imageutils.ImageInfo {
|
||||
return ctx.images
|
||||
}
|
||||
|
||||
|
|
|
@ -2,179 +2,10 @@ package context
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/distribution/reference"
|
||||
"github.com/go-logr/logr"
|
||||
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
type ImageInfo struct {
|
||||
// Registry is the URL address of the image registry e.g. `docker.io`
|
||||
Registry string `json:"registry,omitempty"`
|
||||
|
||||
// Name is the image name portion e.g. `busybox`
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the repository path and image name e.g. `some-repository/busybox`
|
||||
Path string `json:"path"`
|
||||
|
||||
// Tag is the image tag e.g. `v2`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
|
||||
// Digest is the image digest portion e.g. `sha256:128c6e3534b842a2eec139999b8ce8aa9a2af9907e2b9269550809d18cd832a3`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
|
||||
// JSONPointer is full JSON path to this image e.g. `/spec/containers/0/image`
|
||||
JSONPointer string `json:"jsonPath,omitempty"`
|
||||
}
|
||||
|
||||
func (i *ImageInfo) String() string {
|
||||
image := i.Registry + "/" + i.Path + ":" + i.Tag
|
||||
// image that needs only digest and not the tag
|
||||
if i.Digest != "" {
|
||||
image = i.Registry + "/" + i.Path + "@" + i.Digest
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
type ContainerImage struct {
|
||||
ImageInfo
|
||||
Name string
|
||||
}
|
||||
|
||||
type Images struct {
|
||||
InitContainers map[string]ImageInfo `json:"initContainers,omitempty"`
|
||||
Containers map[string]ImageInfo `json:"containers"`
|
||||
EphemeralContainers map[string]ImageInfo `json:"ephemeralContainers"`
|
||||
}
|
||||
|
||||
func newImages(initContainersImgs, containersImgs, ephemeralContainersImgs []ContainerImage) Images {
|
||||
initContainers := make(map[string]ImageInfo)
|
||||
for _, resource := range initContainersImgs {
|
||||
initContainers[resource.Name] = resource.ImageInfo
|
||||
}
|
||||
containers := make(map[string]ImageInfo)
|
||||
for _, resource := range containersImgs {
|
||||
containers[resource.Name] = resource.ImageInfo
|
||||
}
|
||||
ephemeralContainers := make(map[string]ImageInfo)
|
||||
for _, resource := range ephemeralContainersImgs {
|
||||
ephemeralContainers[resource.Name] = resource.ImageInfo
|
||||
}
|
||||
return Images{
|
||||
InitContainers: initContainers,
|
||||
Containers: containers,
|
||||
EphemeralContainers: ephemeralContainers,
|
||||
}
|
||||
}
|
||||
|
||||
type imageExtractor struct {
|
||||
fields []string
|
||||
}
|
||||
|
||||
func (i imageExtractor) extract(tag string, resource *unstructured.Unstructured) []ContainerImage {
|
||||
f := append(i.fields[:len(i.fields):len(i.fields)], tag)
|
||||
if containers, ok, _ := unstructured.NestedSlice(resource.UnstructuredContent(), f...); ok {
|
||||
return extractImageInfos(containers, "/"+strings.Join(f, "/"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var extractors = map[string]imageExtractor{
|
||||
"Pod": {[]string{"spec"}},
|
||||
"CronJob": {[]string{"spec", "jobTemplate", "spec", "template", "spec"}},
|
||||
"Deployment": {[]string{"spec", "template", "spec"}},
|
||||
"DaemonSet": {[]string{"spec", "template", "spec"}},
|
||||
"Job": {[]string{"spec", "template", "spec"}},
|
||||
"StatefulSet": {[]string{"spec", "template", "spec"}},
|
||||
}
|
||||
|
||||
func extractImageInfo(resource *unstructured.Unstructured, log logr.Logger) (initContainersImgs, containersImgs, ephemeralContainersImgs []ContainerImage) {
|
||||
extractor := extractors[resource.GetKind()]
|
||||
initContainersImgs = extractor.extract("initContainers", resource)
|
||||
containersImgs = extractor.extract("containers", resource)
|
||||
ephemeralContainersImgs = extractor.extract("ephemeralContainers", resource)
|
||||
return
|
||||
}
|
||||
|
||||
func extractImageInfos(containers []interface{}, jsonPath string) []ContainerImage {
|
||||
img, err := convertToImageInfo(containers, jsonPath)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to extract image info", "element", containers)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func convertToImageInfo(containers []interface{}, jsonPath string) (images []ContainerImage, err error) {
|
||||
var errs []string
|
||||
var index = 0
|
||||
for _, ctr := range containers {
|
||||
if container, ok := ctr.(map[string]interface{}); ok {
|
||||
var name, image string
|
||||
name = container["name"].(string)
|
||||
if _, ok := container["image"]; ok {
|
||||
image = container["image"].(string)
|
||||
}
|
||||
jp := strings.Join([]string{jsonPath, strconv.Itoa(index), "image"}, "/")
|
||||
imageInfo, err := newImageInfo(image, jp)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
continue
|
||||
}
|
||||
images = append(images, ContainerImage{*imageInfo, name})
|
||||
}
|
||||
index++
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
return images, nil
|
||||
}
|
||||
return images, errors.Errorf("%s", strings.Join(errs, ";"))
|
||||
}
|
||||
|
||||
func newImageInfo(image, jsonPointer string) (*ImageInfo, error) {
|
||||
image = addDefaultDomain(image)
|
||||
ref, err := reference.Parse(image)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "bad image: %s", image)
|
||||
}
|
||||
var registry, path, name, tag, digest string
|
||||
if named, ok := ref.(reference.Named); ok {
|
||||
registry = reference.Domain(named)
|
||||
path = reference.Path(named)
|
||||
name = path[strings.LastIndex(path, "/")+1:]
|
||||
}
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
if digested, ok := ref.(reference.Digested); ok {
|
||||
digest = digested.Digest().String()
|
||||
}
|
||||
// set default tag - the domain is set via addDefaultDomain before parsing
|
||||
if digest == "" && tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
return &ImageInfo{
|
||||
Registry: registry,
|
||||
Name: name,
|
||||
Path: path,
|
||||
Tag: tag,
|
||||
Digest: digest,
|
||||
JSONPointer: jsonPointer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addDefaultDomain(name string) string {
|
||||
i := strings.IndexRune(name, '/')
|
||||
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) {
|
||||
return "docker.io/" + name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// MutateResourceWithImageInfo will set images to their canonical form so that they can be compared
|
||||
// in a predictable manner. This sets the default registry as `docker.io` and the tag as `latest` if
|
||||
// these are missing.
|
||||
|
@ -183,19 +14,15 @@ func MutateResourceWithImageInfo(raw []byte, ctx Interface) error {
|
|||
if images == nil {
|
||||
return nil
|
||||
}
|
||||
var patches [][]byte
|
||||
buildJSONPatch := func(op, path, value string) []byte {
|
||||
p := fmt.Sprintf(`{ "op": "%s", "path": "%s", "value":"%s" }`, op, path, value)
|
||||
return []byte(p)
|
||||
}
|
||||
var patches [][]byte
|
||||
for _, info := range images.Containers {
|
||||
patches = append(patches, buildJSONPatch("replace", info.JSONPointer, info.String()))
|
||||
}
|
||||
for _, info := range images.InitContainers {
|
||||
patches = append(patches, buildJSONPatch("replace", info.JSONPointer, info.String()))
|
||||
}
|
||||
for _, info := range images.EphemeralContainers {
|
||||
patches = append(patches, buildJSONPatch("replace", info.JSONPointer, info.String()))
|
||||
for _, infoMaps := range images {
|
||||
for _, info := range infoMaps {
|
||||
patches = append(patches, buildJSONPatch("replace", info.Pointer, info.String()))
|
||||
}
|
||||
}
|
||||
patchedResource, err := engineutils.ApplyPatches(raw, patches)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kyverno/kyverno/pkg/engine/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
func Test_extractImageInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
raw []byte
|
||||
containers []ContainerImage
|
||||
initContainers []ContainerImage
|
||||
ephemeralContainers []ContainerImage
|
||||
}{
|
||||
{
|
||||
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"}]}}`),
|
||||
initContainers: []ContainerImage{{Name: "init", ImageInfo: ImageInfo{Registry: "index.docker.io", Name: "busybox", Path: "busybox", Tag: "v1.2.3", JSONPointer: "/spec/initContainers/0/image"}}},
|
||||
containers: []ContainerImage{{Name: "nginx", ImageInfo: ImageInfo{Registry: "docker.io", Name: "nginx", Path: "nginx", Tag: "latest", JSONPointer: "/spec/containers/0/image"}}},
|
||||
ephemeralContainers: []ContainerImage{{Name: "ephemeral", ImageInfo: ImageInfo{Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", JSONPointer: "/spec/ephemeralContainers/0/image"}}},
|
||||
},
|
||||
{
|
||||
raw: []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"containers": [{"name": "nginx","image": "test/nginx:latest"}]}}`),
|
||||
initContainers: []ContainerImage{},
|
||||
containers: []ContainerImage{{Name: "nginx", ImageInfo: ImageInfo{Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", JSONPointer: "/spec/containers/0/image"}}},
|
||||
ephemeralContainers: []ContainerImage{},
|
||||
},
|
||||
{
|
||||
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"}] }}}}`),
|
||||
initContainers: []ContainerImage{{Name: "init", ImageInfo: ImageInfo{Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", JSONPointer: "/spec/template/spec/initContainers/0/image", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}},
|
||||
containers: []ContainerImage{{Name: "myapp", ImageInfo: ImageInfo{Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "latest", JSONPointer: "/spec/template/spec/containers/0/image"}}},
|
||||
ephemeralContainers: []ContainerImage{{Name: "ephemeral", ImageInfo: ImageInfo{Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", JSONPointer: "/spec/template/spec/ephemeralContainers/0/image", Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}}},
|
||||
},
|
||||
{
|
||||
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"}]}}}}}}`),
|
||||
containers: []ContainerImage{{Name: "hello", ImageInfo: ImageInfo{Registry: "test.example.com", Name: "my-app", Path: "test/my-app", Tag: "v2", JSONPointer: "/spec/jobTemplate/spec/template/spec/containers/0/image"}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
resource, err := utils.ConvertToUnstructured(test.raw)
|
||||
assert.Nil(t, err)
|
||||
|
||||
init, container, ephemeral := extractImageInfo(resource, log.Log.WithName("TestExtractImageInfo"))
|
||||
if len(test.initContainers) > 0 {
|
||||
assert.Equal(t, test.initContainers, init, "unexpected initContainers %s", resource.GetName())
|
||||
}
|
||||
|
||||
if len(test.containers) > 0 {
|
||||
assert.Equal(t, test.containers, container, "unexpected containers %s", resource.GetName())
|
||||
}
|
||||
|
||||
if len(test.ephemeralContainers) > 0 {
|
||||
assert.Equal(t, test.ephemeralContainers, ephemeral, "unexpected ephemeralContainers %s", resource.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ImageInfo_String(t *testing.T) {
|
||||
validateImageInfo(t,
|
||||
"nginx",
|
||||
"nginx",
|
||||
"nginx",
|
||||
"docker.io",
|
||||
"latest",
|
||||
"",
|
||||
"docker.io/nginx:latest")
|
||||
|
||||
validateImageInfo(t,
|
||||
"nginx:v10.3",
|
||||
"nginx",
|
||||
"nginx",
|
||||
"docker.io",
|
||||
"v10.3",
|
||||
"",
|
||||
"docker.io/nginx:v10.3")
|
||||
|
||||
validateImageInfo(t,
|
||||
"docker.io/test/nginx:v10.3",
|
||||
"nginx",
|
||||
"test/nginx",
|
||||
"docker.io",
|
||||
"v10.3",
|
||||
"",
|
||||
"docker.io/test/nginx:v10.3")
|
||||
|
||||
validateImageInfo(t,
|
||||
"test/nginx",
|
||||
"nginx",
|
||||
"test/nginx",
|
||||
"docker.io",
|
||||
"latest",
|
||||
"",
|
||||
"docker.io/test/nginx:latest")
|
||||
|
||||
validateImageInfo(t,
|
||||
"localhost:4443/test/nginx",
|
||||
"nginx",
|
||||
"test/nginx",
|
||||
"localhost:4443",
|
||||
"latest",
|
||||
"",
|
||||
"localhost:4443/test/nginx:latest")
|
||||
validateImageInfo(t,
|
||||
"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
|
||||
"centos",
|
||||
"test/centos",
|
||||
"docker.io",
|
||||
"",
|
||||
"sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
|
||||
"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f")
|
||||
}
|
||||
|
||||
func validateImageInfo(t *testing.T, raw, name, path, registry, tag, digest, str string) {
|
||||
i1, err := newImageInfo(raw, "/spec/containers/0/image")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, name, i1.Name)
|
||||
assert.Equal(t, path, i1.Path)
|
||||
assert.Equal(t, registry, i1.Registry)
|
||||
assert.Equal(t, tag, i1.Tag)
|
||||
assert.Equal(t, digest, i1.Digest)
|
||||
assert.Equal(t, str, i1.String())
|
||||
}
|
|
@ -5,18 +5,18 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/autogen"
|
||||
"github.com/kyverno/kyverno/pkg/engine/variables"
|
||||
"github.com/kyverno/kyverno/pkg/registryclient"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
wildcard "github.com/kyverno/go-wildcard"
|
||||
v1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
"github.com/kyverno/kyverno/pkg/autogen"
|
||||
"github.com/kyverno/kyverno/pkg/cosign"
|
||||
"github.com/kyverno/kyverno/pkg/engine/context"
|
||||
"github.com/kyverno/kyverno/pkg/engine/response"
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
|
@ -81,9 +81,7 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
|
|||
}
|
||||
|
||||
for _, imageVerify := range ruleCopy.VerifyImages {
|
||||
iv.verify(imageVerify, images.Containers)
|
||||
iv.verify(imageVerify, images.InitContainers)
|
||||
iv.verify(imageVerify, images.EphemeralContainers)
|
||||
iv.verify(imageVerify, images)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,36 +123,39 @@ type imageVerifier struct {
|
|||
resp *response.EngineResponse
|
||||
}
|
||||
|
||||
func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]context.ImageInfo) {
|
||||
func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]map[string]imageutils.ImageInfo) {
|
||||
imagePattern := imageVerify.Image
|
||||
|
||||
for _, imageInfo := range images {
|
||||
image := imageInfo.String()
|
||||
jmespath := utils.JsonPointerToJMESPath(imageInfo.JSONPointer)
|
||||
changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
|
||||
if err == nil && !changed {
|
||||
iv.logger.V(4).Info("no change in image, skipping check", "image", image)
|
||||
continue
|
||||
}
|
||||
|
||||
if !wildcard.Match(imagePattern, image) {
|
||||
iv.logger.V(4).Info("image does not match pattern", "image", image, "pattern", imagePattern)
|
||||
continue
|
||||
}
|
||||
|
||||
var ruleResp *response.RuleResponse
|
||||
if len(imageVerify.Attestations) == 0 {
|
||||
var digest string
|
||||
ruleResp, digest = iv.verifySignature(imageVerify, imageInfo)
|
||||
if ruleResp.Status == response.RuleStatusPass {
|
||||
iv.patchDigest(imageInfo, digest, ruleResp)
|
||||
for _, infoMap := range images {
|
||||
for _, imageInfo := range infoMap {
|
||||
path := imageInfo.Path
|
||||
image := imageInfo.String()
|
||||
jmespath := utils.JsonPointerToJMESPath(path)
|
||||
changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
|
||||
if err == nil && !changed {
|
||||
iv.logger.V(4).Info("no change in image, skipping check", "image", image)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
ruleResp = iv.attestImage(imageVerify, imageInfo)
|
||||
}
|
||||
|
||||
iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
|
||||
incrementAppliedCount(iv.resp)
|
||||
if !wildcard.Match(imagePattern, image) {
|
||||
iv.logger.V(4).Info("image does not match pattern", "image", image, "pattern", imagePattern)
|
||||
continue
|
||||
}
|
||||
|
||||
var ruleResp *response.RuleResponse
|
||||
if len(imageVerify.Attestations) == 0 {
|
||||
var digest string
|
||||
ruleResp, digest = iv.verifySignature(imageVerify, imageInfo)
|
||||
if ruleResp.Status == response.RuleStatusPass {
|
||||
iv.patchDigest(path, imageInfo, digest, ruleResp)
|
||||
}
|
||||
} else {
|
||||
ruleResp = iv.attestImage(imageVerify, imageInfo)
|
||||
}
|
||||
|
||||
iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
|
||||
incrementAppliedCount(iv.resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,7 +168,7 @@ func getSignatureRepository(imageVerify *v1.ImageVerification) string {
|
|||
return repository
|
||||
}
|
||||
|
||||
func (iv *imageVerifier) verifySignature(imageVerify *v1.ImageVerification, imageInfo context.ImageInfo) (*response.RuleResponse, string) {
|
||||
func (iv *imageVerifier) verifySignature(imageVerify *v1.ImageVerification, imageInfo imageutils.ImageInfo) (*response.RuleResponse, string) {
|
||||
image := imageInfo.String()
|
||||
iv.logger.Info("verifying image", "image", image)
|
||||
|
||||
|
@ -219,11 +220,11 @@ func (iv *imageVerifier) verifySignature(imageVerify *v1.ImageVerification, imag
|
|||
return ruleResp, digest
|
||||
}
|
||||
|
||||
func (iv *imageVerifier) patchDigest(imageInfo context.ImageInfo, digest string, ruleResp *response.RuleResponse) {
|
||||
func (iv *imageVerifier) patchDigest(path string, imageInfo imageutils.ImageInfo, digest string, ruleResp *response.RuleResponse) {
|
||||
if imageInfo.Digest == "" {
|
||||
patch, err := makeAddDigestPatch(imageInfo, digest)
|
||||
patch, err := makeAddDigestPatch(path, imageInfo, digest)
|
||||
if err != nil {
|
||||
iv.logger.Error(err, "failed to patch image with digest", "image", imageInfo.String(), "jsonPath", imageInfo.JSONPointer)
|
||||
iv.logger.Error(err, "failed to patch image with digest", "image", imageInfo.String(), "jsonPath", path)
|
||||
} else {
|
||||
iv.logger.V(4).Info("patching verified image with digest", "patch", string(patch))
|
||||
ruleResp.Patches = [][]byte{patch}
|
||||
|
@ -231,15 +232,15 @@ func (iv *imageVerifier) patchDigest(imageInfo context.ImageInfo, digest string,
|
|||
}
|
||||
}
|
||||
|
||||
func makeAddDigestPatch(imageInfo context.ImageInfo, digest string) ([]byte, error) {
|
||||
func makeAddDigestPatch(path string, imageInfo imageutils.ImageInfo, digest string) ([]byte, error) {
|
||||
var patch = make(map[string]interface{})
|
||||
patch["op"] = "replace"
|
||||
patch["path"] = imageInfo.JSONPointer
|
||||
patch["path"] = path
|
||||
patch["value"] = imageInfo.String() + "@" + digest
|
||||
return json.Marshal(patch)
|
||||
}
|
||||
|
||||
func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo context.ImageInfo) *response.RuleResponse {
|
||||
func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo imageutils.ImageInfo) *response.RuleResponse {
|
||||
image := imageInfo.String()
|
||||
start := time.Now()
|
||||
|
||||
|
@ -291,7 +292,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 context.ImageInfo) (bool, error) {
|
||||
func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]interface{}, img imageutils.ImageInfo) (bool, error) {
|
||||
if len(a.Conditions) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -308,7 +309,7 @@ func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]inter
|
|||
return false, errors.Wrapf(err, fmt.Sprintf("failed to add Statement to the context %v", s))
|
||||
}
|
||||
|
||||
if err := iv.policyContext.JSONContext.AddImageInfo(&img); err != nil {
|
||||
if err := iv.policyContext.JSONContext.AddImageInfo(img); err != nil {
|
||||
return false, errors.Wrapf(err, fmt.Sprintf("failed to add image to the context %v", s))
|
||||
}
|
||||
|
||||
|
|
77
pkg/utils/image/infos.go
Normal file
77
pkg/utils/image/infos.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/distribution/reference"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ImageInfo struct {
|
||||
// Registry is the URL address of the image registry e.g. `docker.io`
|
||||
Registry string `json:"registry,omitempty"`
|
||||
|
||||
// Name is the image name portion e.g. `busybox`
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the repository path and image name e.g. `some-repository/busybox`
|
||||
Path string `json:"path"`
|
||||
|
||||
// Tag is the image tag e.g. `v2`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
|
||||
// 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 {
|
||||
image := i.Registry + "/" + i.Path
|
||||
if i.Digest != "" {
|
||||
return image + "@" + i.Digest
|
||||
} else {
|
||||
return image + ":" + i.Tag
|
||||
}
|
||||
}
|
||||
|
||||
func GetImageInfo(image string, pointer string) (*ImageInfo, error) {
|
||||
image = addDefaultDomain(image)
|
||||
ref, err := reference.Parse(image)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "bad image: %s", image)
|
||||
}
|
||||
var registry, path, name, tag, digest string
|
||||
if named, ok := ref.(reference.Named); ok {
|
||||
registry = reference.Domain(named)
|
||||
path = reference.Path(named)
|
||||
name = path[strings.LastIndex(path, "/")+1:]
|
||||
}
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
if digested, ok := ref.(reference.Digested); ok {
|
||||
digest = digested.Digest().String()
|
||||
}
|
||||
// set default tag - the domain is set via addDefaultDomain before parsing
|
||||
if digest == "" && tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
return &ImageInfo{
|
||||
Registry: registry,
|
||||
Name: name,
|
||||
Path: path,
|
||||
Tag: tag,
|
||||
Digest: digest,
|
||||
Pointer: pointer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func addDefaultDomain(name string) string {
|
||||
i := strings.IndexRune(name, '/')
|
||||
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) {
|
||||
return "docker.io/" + name
|
||||
}
|
||||
return name
|
||||
}
|
73
pkg/utils/image/infos_test.go
Normal file
73
pkg/utils/image/infos_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_GetImageInfo(t *testing.T) {
|
||||
validateImageInfo(t,
|
||||
"nginx",
|
||||
"nginx",
|
||||
"nginx",
|
||||
"docker.io",
|
||||
"latest",
|
||||
"",
|
||||
"docker.io/nginx:latest")
|
||||
|
||||
validateImageInfo(t,
|
||||
"nginx:v10.3",
|
||||
"nginx",
|
||||
"nginx",
|
||||
"docker.io",
|
||||
"v10.3",
|
||||
"",
|
||||
"docker.io/nginx:v10.3")
|
||||
|
||||
validateImageInfo(t,
|
||||
"docker.io/test/nginx:v10.3",
|
||||
"nginx",
|
||||
"test/nginx",
|
||||
"docker.io",
|
||||
"v10.3",
|
||||
"",
|
||||
"docker.io/test/nginx:v10.3")
|
||||
|
||||
validateImageInfo(t,
|
||||
"test/nginx",
|
||||
"nginx",
|
||||
"test/nginx",
|
||||
"docker.io",
|
||||
"latest",
|
||||
"",
|
||||
"docker.io/test/nginx:latest")
|
||||
|
||||
validateImageInfo(t,
|
||||
"localhost:4443/test/nginx",
|
||||
"nginx",
|
||||
"test/nginx",
|
||||
"localhost:4443",
|
||||
"latest",
|
||||
"",
|
||||
"localhost:4443/test/nginx:latest")
|
||||
validateImageInfo(t,
|
||||
"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
|
||||
"centos",
|
||||
"test/centos",
|
||||
"docker.io",
|
||||
"",
|
||||
"sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
|
||||
"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f")
|
||||
}
|
||||
|
||||
func validateImageInfo(t *testing.T, raw, name, path, registry, tag, digest, str string) {
|
||||
i1, err := GetImageInfo(raw, "")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, name, i1.Name)
|
||||
assert.Equal(t, path, i1.Path)
|
||||
assert.Equal(t, registry, i1.Registry)
|
||||
assert.Equal(t, tag, i1.Tag)
|
||||
assert.Equal(t, digest, i1.Digest)
|
||||
assert.Equal(t, str, i1.String())
|
||||
}
|
119
pkg/utils/kube/image.go
Normal file
119
pkg/utils/kube/image.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package kube
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
imageutils "github.com/kyverno/kyverno/pkg/utils/image"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
var (
|
||||
podExtractors = BuildStandardExtractors("spec")
|
||||
podControllerExtractors = BuildStandardExtractors("spec", "template", "spec")
|
||||
cronjobControllerExtractors = BuildStandardExtractors("spec", "jobTemplate", "spec", "template", "spec")
|
||||
registeredExtractors = map[string][]ImageExtractor{
|
||||
"Pod": podExtractors,
|
||||
"DaemonSet": podControllerExtractors,
|
||||
"Deployment": podControllerExtractors,
|
||||
"ReplicaSet": podControllerExtractors,
|
||||
"StatefulSet": podControllerExtractors,
|
||||
"CronJob": cronjobControllerExtractors,
|
||||
"Job": podControllerExtractors,
|
||||
}
|
||||
)
|
||||
|
||||
type ImageExtractor struct {
|
||||
fields []string
|
||||
key string
|
||||
value string
|
||||
name string
|
||||
}
|
||||
|
||||
func (i *ImageExtractor) ExtractFromResource(resource interface{}) (map[string]imageutils.ImageInfo, error) {
|
||||
imageInfo := map[string]imageutils.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 {
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
if len(fields) > 0 && fields[0] == "*" {
|
||||
switch typedObj := obj.(type) {
|
||||
case []interface{}:
|
||||
for i, v := range typedObj {
|
||||
if err := extract(v, append(path, strconv.Itoa(i)), keyPath, valuePath, fields[1:], imageInfos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case map[string]interface{}:
|
||||
for i, v := range typedObj {
|
||||
if err := extract(v, append(path, i), keyPath, valuePath, fields[1:], imageInfos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case interface{}:
|
||||
return fmt.Errorf("invalid type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
output, ok := obj.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid image config")
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
pointer := fmt.Sprintf("/%s/%s", strings.Join(path, "/"), valuePath)
|
||||
key := pointer
|
||||
if keyPath != "" {
|
||||
key, ok = output[keyPath].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid key")
|
||||
}
|
||||
}
|
||||
value, ok := output[valuePath].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid value")
|
||||
}
|
||||
if imageInfo, err := imageutils.GetImageInfo(value, pointer); err != nil {
|
||||
return fmt.Errorf("invalid image %s", value)
|
||||
} else {
|
||||
(*imageInfos)[key] = *imageInfo
|
||||
}
|
||||
return nil
|
||||
}
|
||||
currentPath := fields[0]
|
||||
return extract(output[currentPath], append(path, currentPath), keyPath, valuePath, fields[1:], imageInfos)
|
||||
}
|
||||
|
||||
func BuildStandardExtractors(tags ...string) []ImageExtractor {
|
||||
var extractors []ImageExtractor
|
||||
for _, tag := range []string{"initContainers", "containers", "ephemeralContainers"} {
|
||||
var t []string
|
||||
t = append(t, tags...)
|
||||
t = append(t, tag)
|
||||
t = append(t, "*")
|
||||
extractors = append(extractors, ImageExtractor{fields: t, key: "name", value: "image", name: tag})
|
||||
}
|
||||
return extractors
|
||||
}
|
||||
|
||||
func LookupImageExtractor(kind string) []ImageExtractor {
|
||||
return registeredExtractors[kind]
|
||||
}
|
||||
|
||||
func ExtractImagesFromResource(resource unstructured.Unstructured) (map[string]map[string]imageutils.ImageInfo, error) {
|
||||
infos := map[string]map[string]imageutils.ImageInfo{}
|
||||
for _, extractor := range LookupImageExtractor(resource.GetKind()) {
|
||||
if infoMap, err := extractor.ExtractFromResource(resource.Object); err != nil {
|
||||
return nil, err
|
||||
} else if infoMap != nil && len(infoMap) > 0 {
|
||||
infos[extractor.name] = infoMap
|
||||
}
|
||||
}
|
||||
return infos, nil
|
||||
}
|
69
pkg/utils/kube/image_test.go
Normal file
69
pkg/utils/kube/image_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package kube
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func Test_extractImageInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
raw []byte
|
||||
images map[string]map[string]imageutils.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{
|
||||
"initContainers": {
|
||||
"init": {Registry: "index.docker.io", Name: "busybox", Path: "busybox", Tag: "v1.2.3", Pointer: "/spec/initContainers/0/image"},
|
||||
},
|
||||
"containers": {
|
||||
"nginx": {Registry: "docker.io", Name: "nginx", Path: "nginx", Tag: "latest", Pointer: "/spec/containers/0/image"},
|
||||
},
|
||||
"ephemeralContainers": {
|
||||
"ephemeral": {Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", Pointer: "/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{
|
||||
"containers": {
|
||||
"nginx": {Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", Pointer: "/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{
|
||||
"initContainers": {
|
||||
"init": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", Pointer: "/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"},
|
||||
},
|
||||
"ephemeralContainers": {
|
||||
"ephemeral": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Pointer: "/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{
|
||||
"containers": {
|
||||
"hello": {Registry: "test.example.com", Name: "my-app", Path: "test/my-app", Tag: "v2", Pointer: "/spec/jobTemplate/spec/template/spec/containers/0/image"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
resource, err := utils.ConvertToUnstructured(test.raw)
|
||||
assert.NilError(t, err)
|
||||
images, err := ExtractImagesFromResource(*resource)
|
||||
assert.DeepEqual(t, test.images, images)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue