1
0
Fork 0
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:
Sambhav Kothari 2022-04-11 10:30:38 +01:00 committed by GitHub
parent 585b0f17a6
commit 6f7bd7451b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 401 additions and 356 deletions

View file

@ -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
}

View file

@ -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 {

View file

@ -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())
}

View file

@ -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
View 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
}

View 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
View 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
}

View 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)
}
}