mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-22 07:41:10 +00:00
fix: image parse func and add chainsaw tests (#12396)
* fix: image parse func and add chainsaw tests Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> * fix: linter Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com> --------- Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
This commit is contained in:
parent
6fdbdbce28
commit
d56e6037a4
19 changed files with 598 additions and 116 deletions
|
@ -204,7 +204,7 @@ func (e *ivengine) handleMutation(ctx context.Context, policies []CompiledImageV
|
|||
}
|
||||
|
||||
if p, errList := c.Compile(e.logger, ivpol.Policy); errList != nil {
|
||||
response.Result = *engineapi.RuleError("evaluation", engineapi.ImageVerify, "failed to compile policy", err, nil)
|
||||
response.Result = *engineapi.RuleError("evaluation", engineapi.ImageVerify, "failed to compile policy", errList.ToAggregate(), nil)
|
||||
} else {
|
||||
result, err := p.Evaluate(ctx, ictx, attr, request, namespace, true)
|
||||
if err != nil {
|
||||
|
|
|
@ -91,6 +91,10 @@ uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
|
|||
},
|
||||
},
|
||||
Verifications: []admissionregistrationv1.Validation{
|
||||
{
|
||||
Expression: "images.containers.map(i, image(i).registry() == \"ghcr.io\" ).all(e, e)",
|
||||
Message: "images are not from ghcr registry",
|
||||
},
|
||||
{
|
||||
Expression: "images.containers.map(image, verifyImageSignatures(image, [attestors.notary])).all(e, e > 0)",
|
||||
Message: "failed to verify image with notary cert",
|
||||
|
|
|
@ -3,6 +3,7 @@ package cel
|
|||
import (
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/ext"
|
||||
"github.com/kyverno/kyverno/pkg/cel/libs/image"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
|
@ -30,5 +31,6 @@ func NewEnv() (*cel.Env, error) {
|
|||
library.Lists(),
|
||||
library.Regex(),
|
||||
library.URLs(),
|
||||
image.ImageLib(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -62,21 +62,6 @@ func (c *impl) get_imagedata_string(ctx ref.Val, image ref.Val) ref.Val {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *impl) parse_imagereference_string(ctx ref.Val, image ref.Val) ref.Val {
|
||||
if self, err := utils.ConvertToNative[Context](ctx); err != nil {
|
||||
return types.WrapErr(err)
|
||||
} else if image, err := utils.ConvertToNative[string](image); err != nil {
|
||||
return types.WrapErr(err)
|
||||
} else {
|
||||
parsedRef, err := self.ParseImageReference(image)
|
||||
if err != nil {
|
||||
// Errors are not expected here since Parse is a more lenient parser than ParseRequestURI.
|
||||
return types.NewErr("failed to parse image data: %v", err)
|
||||
}
|
||||
return c.NativeToValue(parsedRef)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *impl) list_resources_string_string_string(args ...ref.Val) ref.Val {
|
||||
if self, err := utils.ConvertToNative[Context](args[0]); err != nil {
|
||||
return types.WrapErr(err)
|
||||
|
|
|
@ -2,6 +2,7 @@ package context
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -146,7 +147,7 @@ func Test_impl_get_imagedata_string(t *testing.T) {
|
|||
env, err := base.Extend(options...)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, env)
|
||||
ast, issues := env.Compile(`context.GetImageData("ghcr.io/kyverno/kyverno:latest")`)
|
||||
ast, issues := env.Compile(`context.GetImageData("ghcr.io/kyverno/kyverno:latest").resolvedImage`)
|
||||
assert.Nil(t, issues)
|
||||
assert.NotNil(t, ast)
|
||||
prog, err := env.Program(ast)
|
||||
|
@ -154,54 +155,30 @@ func Test_impl_get_imagedata_string(t *testing.T) {
|
|||
assert.NotNil(t, prog)
|
||||
data := map[string]any{
|
||||
"context": Context{&MockCtx{
|
||||
GetImageDataFunc: func(image string) (*imagedataloader.ImageData, error) {
|
||||
GetImageDataFunc: func(image string) (map[string]interface{}, error) {
|
||||
idl, err := imagedataloader.New(nil)
|
||||
assert.NoError(t, err)
|
||||
return idl.FetchImageData(context.TODO(), image)
|
||||
data, err := idl.FetchImageData(context.TODO(), image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := json.Marshal(data.Data())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiData := map[string]interface{}{}
|
||||
err = json.Unmarshal(raw, &apiData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return apiData, nil
|
||||
},
|
||||
},
|
||||
}}
|
||||
out, _, err := prog.Eval(data)
|
||||
assert.NoError(t, err)
|
||||
img := out.Value().(*imagedataloader.ImageData)
|
||||
assert.Equal(t, img.Tag, "latest")
|
||||
assert.True(t, strings.HasPrefix(img.ResolvedImage, "ghcr.io/kyverno/kyverno:latest@sha256:"))
|
||||
assert.True(t, img.ConfigData != nil)
|
||||
assert.True(t, img.Manifest != nil)
|
||||
assert.True(t, img.ImageIndex != nil)
|
||||
}
|
||||
|
||||
func Test_impl_parse_image_ref_string(t *testing.T) {
|
||||
opts := Lib()
|
||||
base, err := cel.NewEnv(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, base)
|
||||
options := []cel.EnvOption{
|
||||
cel.Variable("context", ContextType),
|
||||
}
|
||||
env, err := base.Extend(options...)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, env)
|
||||
ast, issues := env.Compile(`context.ParseImageReference("ghcr.io/kyverno/kyverno:latest")`)
|
||||
assert.Nil(t, issues)
|
||||
assert.NotNil(t, ast)
|
||||
prog, err := env.Program(ast)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, prog)
|
||||
data := map[string]any{
|
||||
"context": Context{&MockCtx{
|
||||
ParseImageReferenceFunc: func(image string) (imagedataloader.ImageReference, error) {
|
||||
return imagedataloader.ParseImageReference(image)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
out, _, err := prog.Eval(data)
|
||||
assert.NoError(t, err)
|
||||
img := out.Value().(imagedataloader.ImageReference)
|
||||
assert.Equal(t, img.Tag, "latest")
|
||||
assert.Equal(t, img.Identifier, "latest")
|
||||
assert.Equal(t, img.Image, "ghcr.io/kyverno/kyverno:latest")
|
||||
resolvedImg := out.Value().(string)
|
||||
assert.True(t, strings.HasPrefix(resolvedImg, "ghcr.io/kyverno/kyverno:latest@sha256:"))
|
||||
}
|
||||
|
||||
func Test_impl_get_resource_string_string_string_string(t *testing.T) {
|
||||
|
|
|
@ -67,18 +67,10 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) {
|
|||
cel.MemberOverload(
|
||||
"get_imagedata_string",
|
||||
[]*cel.Type{ContextType, types.StringType},
|
||||
imageDataType.CelType(),
|
||||
types.DynType,
|
||||
cel.BinaryBinding(impl.get_imagedata_string),
|
||||
),
|
||||
},
|
||||
"ParseImageReference": {
|
||||
cel.MemberOverload(
|
||||
"parse_image_reference_string",
|
||||
[]*cel.Type{ContextType, types.StringType},
|
||||
imageReferenceType.CelType(),
|
||||
cel.BinaryBinding(impl.parse_imagereference_string),
|
||||
),
|
||||
},
|
||||
"ListResources": {
|
||||
// TODO: should not use DynType in return
|
||||
cel.MemberOverload(
|
||||
|
|
|
@ -2,7 +2,6 @@ package context
|
|||
|
||||
import (
|
||||
"github.com/kyverno/kyverno/pkg/globalcontext/store"
|
||||
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
|
@ -10,8 +9,7 @@ import (
|
|||
type MockCtx struct {
|
||||
GetConfigMapFunc func(string, string) (*unstructured.Unstructured, error)
|
||||
GetGlobalReferenceFunc func(string, string) (any, error)
|
||||
GetImageDataFunc func(string) (*imagedataloader.ImageData, error)
|
||||
ParseImageReferenceFunc func(string) (imagedataloader.ImageReference, error)
|
||||
GetImageDataFunc func(string) (map[string]interface{}, error)
|
||||
ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error)
|
||||
GetResourceFunc func(string, string, string, string) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
@ -24,14 +22,10 @@ func (mock *MockCtx) GetGlobalReference(n, p string) (any, error) {
|
|||
return mock.GetGlobalReferenceFunc(n, p)
|
||||
}
|
||||
|
||||
func (mock *MockCtx) GetImageData(n string) (*imagedataloader.ImageData, error) {
|
||||
func (mock *MockCtx) GetImageData(n string) (map[string]interface{}, error) {
|
||||
return mock.GetImageDataFunc(n)
|
||||
}
|
||||
|
||||
func (mock *MockCtx) ParseImageReference(n string) (imagedataloader.ImageReference, error) {
|
||||
return mock.ParseImageReferenceFunc(n)
|
||||
}
|
||||
|
||||
func (mock *MockCtx) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
|
||||
return mock.ListResourcesFunc(apiVersion, resource, namespace)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package context
|
|||
|
||||
import (
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
)
|
||||
|
@ -11,14 +10,12 @@ var (
|
|||
ContextType = types.NewOpaqueType("context.Context")
|
||||
configMapType = BuildConfigMapType()
|
||||
imageDataType = BuildImageDataType()
|
||||
imageReferenceType = BuildImageReferenceType()
|
||||
)
|
||||
|
||||
type ContextInterface interface {
|
||||
GetConfigMap(string, string) (*unstructured.Unstructured, error)
|
||||
GetGlobalReference(string, string) (any, error)
|
||||
GetImageData(string) (*imagedataloader.ImageData, error)
|
||||
ParseImageReference(string) (imagedataloader.ImageReference, error)
|
||||
GetImageData(string) (map[string]interface{}, error)
|
||||
ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error)
|
||||
GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
@ -81,20 +78,6 @@ func BuildImageDataType() *apiservercel.DeclType {
|
|||
return apiservercel.NewObjectType("imageData", fields(f...))
|
||||
}
|
||||
|
||||
func BuildImageReferenceType() *apiservercel.DeclType {
|
||||
f := make([]*apiservercel.DeclField, 0)
|
||||
f = append(f,
|
||||
field("image", apiservercel.StringType, true),
|
||||
field("resolvedImage", apiservercel.StringType, true),
|
||||
field("registry", apiservercel.StringType, true),
|
||||
field("repository", apiservercel.StringType, true),
|
||||
field("tag", apiservercel.StringType, false),
|
||||
field("digest", apiservercel.StringType, false),
|
||||
field("identifier", apiservercel.StringType, true),
|
||||
)
|
||||
return apiservercel.NewObjectType("imageReference", fields(f...))
|
||||
}
|
||||
|
||||
func field(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
|
||||
return apiservercel.NewDeclField(name, declType, required, nil, nil)
|
||||
}
|
||||
|
|
158
pkg/cel/libs/image/lib.go
Normal file
158
pkg/cel/libs/image/lib.go
Normal file
|
@ -0,0 +1,158 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
func ImageLib() cel.EnvOption {
|
||||
return cel.Lib(imageLib)
|
||||
}
|
||||
|
||||
var imageLib = &imageLibType{}
|
||||
|
||||
type imageLibType struct{}
|
||||
|
||||
func (*imageLibType) LibraryName() string {
|
||||
return "kyverno.Image"
|
||||
}
|
||||
|
||||
func (*imageLibType) Types() []*cel.Type {
|
||||
return []*cel.Type{ImageType}
|
||||
}
|
||||
|
||||
func (*imageLibType) declarations() map[string][]cel.FunctionOpt {
|
||||
return map[string][]cel.FunctionOpt{
|
||||
"image": {
|
||||
cel.Overload("string_to_image", []*cel.Type{cel.StringType}, ImageType, cel.UnaryBinding((stringToImage))),
|
||||
},
|
||||
"isImage": {
|
||||
cel.Overload("is_image_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isImage)),
|
||||
},
|
||||
"containsDigest": {
|
||||
cel.MemberOverload("image_contains_digest", []*cel.Type{ImageType}, cel.BoolType, cel.UnaryBinding(imageContainsDigest)),
|
||||
},
|
||||
"registry": {
|
||||
cel.MemberOverload("image_registry", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageRegistry)),
|
||||
},
|
||||
"repository": {
|
||||
cel.MemberOverload("image_repository", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageRepository)),
|
||||
},
|
||||
"identifier": {
|
||||
cel.MemberOverload("image_identifier", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageIdentifier)),
|
||||
},
|
||||
"tag": {
|
||||
cel.MemberOverload("image_tag", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageTag)),
|
||||
},
|
||||
"digest": {
|
||||
cel.MemberOverload("image_digest", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageDigest)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (i *imageLibType) CompileOptions() []cel.EnvOption {
|
||||
imageLibraryDecls := i.declarations()
|
||||
options := make([]cel.EnvOption, 0, len(imageLibraryDecls))
|
||||
for name, overloads := range imageLibraryDecls {
|
||||
options = append(options, cel.Function(name, overloads...))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (*imageLibType) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
|
||||
func isImage(arg ref.Val) ref.Val {
|
||||
str, ok := arg.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
_, err := name.ParseReference(str)
|
||||
if err != nil {
|
||||
return types.Bool(false)
|
||||
}
|
||||
|
||||
return types.Bool(true)
|
||||
}
|
||||
|
||||
func stringToImage(arg ref.Val) ref.Val {
|
||||
str, ok := arg.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
v, err := name.ParseReference(str)
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
|
||||
return Image{ImageReference: ConvertToImageRef(v)}
|
||||
}
|
||||
|
||||
func imageContainsDigest(arg ref.Val) ref.Val {
|
||||
v, ok := arg.Value().(ImageReference)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.Bool(len(v.Digest) != 0)
|
||||
}
|
||||
|
||||
func imageRegistry(arg ref.Val) ref.Val {
|
||||
v, ok := arg.Value().(ImageReference)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.String(v.Registry)
|
||||
}
|
||||
|
||||
func imageRepository(arg ref.Val) ref.Val {
|
||||
v, ok := arg.Value().(ImageReference)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.String(v.Repository)
|
||||
}
|
||||
|
||||
func imageIdentifier(arg ref.Val) ref.Val {
|
||||
v, ok := arg.Value().(ImageReference)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.String(v.Identifier)
|
||||
}
|
||||
|
||||
func imageTag(arg ref.Val) ref.Val {
|
||||
v, ok := arg.Value().(ImageReference)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.String(v.Tag)
|
||||
}
|
||||
|
||||
func imageDigest(arg ref.Val) ref.Val {
|
||||
v, ok := arg.Value().(ImageReference)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.String(v.Digest)
|
||||
}
|
||||
|
||||
func ConvertToImageRef(ref name.Reference) ImageReference {
|
||||
var img ImageReference
|
||||
img.Image = ref.String()
|
||||
img.Registry = ref.Context().RegistryStr()
|
||||
img.Repository = ref.Context().RepositoryStr()
|
||||
img.Identifier = ref.Identifier()
|
||||
|
||||
if _, ok := ref.(name.Tag); ok {
|
||||
img.Tag = ref.Identifier()
|
||||
} else {
|
||||
img.Digest = ref.Identifier()
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
226
pkg/cel/libs/image/lib_test.go
Normal file
226
pkg/cel/libs/image/lib_test.go
Normal file
|
@ -0,0 +1,226 @@
|
|||
package image_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/kyverno/kyverno/pkg/cel/libs/image"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
func testImageLib(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
|
||||
env, err := cel.NewEnv(
|
||||
image.ImageLib(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
compiled, issues := env.Compile(expr)
|
||||
|
||||
if len(expectCompileErrs) > 0 {
|
||||
missingCompileErrs := []string{}
|
||||
matchedCompileErrs := sets.New[int]()
|
||||
for _, expectedCompileErr := range expectCompileErrs {
|
||||
compiledPattern, err := regexp.Compile(expectedCompileErr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expected err regex: %v", err)
|
||||
}
|
||||
|
||||
didMatch := false
|
||||
|
||||
for i, compileError := range issues.Errors() {
|
||||
if compiledPattern.Match([]byte(compileError.Message)) {
|
||||
didMatch = true
|
||||
matchedCompileErrs.Insert(i)
|
||||
}
|
||||
}
|
||||
|
||||
if !didMatch {
|
||||
missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
|
||||
} else if len(matchedCompileErrs) != len(issues.Errors()) {
|
||||
unmatchedErrs := []cel.Error{}
|
||||
for i, issue := range issues.Errors() {
|
||||
if !matchedCompileErrs.Has(i) {
|
||||
unmatchedErrs = append(unmatchedErrs, *issue)
|
||||
}
|
||||
}
|
||||
require.Empty(t, unmatchedErrs, "unexpected compilation errors")
|
||||
}
|
||||
}
|
||||
|
||||
require.Empty(t, missingCompileErrs, "expected compilation errors")
|
||||
return
|
||||
} else if len(issues.Errors()) > 0 {
|
||||
for _, err := range issues.Errors() {
|
||||
t.Errorf("unexpected compile error: %v", err)
|
||||
}
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
prog, err := env.Program(compiled)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
res, _, err := prog.Eval(map[string]interface{}{})
|
||||
if len(expectRuntimeErrPattern) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern)
|
||||
} else if matched, regexErr := regexp.MatchString(expectRuntimeErrPattern, err.Error()); regexErr != nil {
|
||||
t.Fatalf("failed to compile expected err regex: %v", regexErr)
|
||||
} else if !matched {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
} else if expectResult != nil {
|
||||
converted := res.Equal(expectResult).Value().(bool)
|
||||
require.True(t, converted, "expectation not equal to output")
|
||||
} else {
|
||||
t.Fatal("expected result must not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestImage(t *testing.T) {
|
||||
trueVal := types.Bool(true)
|
||||
falseVal := types.Bool(false)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
expectValue ref.Val
|
||||
expectedCompileErr []string
|
||||
expectedRuntimeErr string
|
||||
}{
|
||||
{
|
||||
name: "parse",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:latest")`,
|
||||
expectValue: image.Image{ImageReference: image.ConvertToImageRef(name.MustParseReference("registry.k8s.io/kube-apiserver-arm64:latest"))},
|
||||
},
|
||||
{
|
||||
name: "parse_invalid_image",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:@")`,
|
||||
expectedRuntimeErr: "could not parse reference: registry.k8s.io/kube-apiserver-arm64:@",
|
||||
},
|
||||
{
|
||||
name: "isImage",
|
||||
expr: `isImage("registry.k8s.io/kube-apiserver-arm64:latest")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "isImage_false",
|
||||
expr: `isImage("registry.k8s.io/kube-apiserver-arm64:@")`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "isImage_noOverload",
|
||||
expr: `isImage(0)`,
|
||||
expectedCompileErr: []string{"found no matching overload for 'isImage' applied to.*"},
|
||||
},
|
||||
{
|
||||
name: "contains_digest_no_identifier",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").containsDigest()`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "contains_digest_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:latest").containsDigest()`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "contains_digest_true",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").containsDigest()`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "contains_digest_with_tag_true",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:latest@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").containsDigest()`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "registry",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").registry() == "registry.k8s.io"`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "registry_matches",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").registry().matches("(registry.k8s.io|ghcr.io)")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "repository",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").repository() == "kube-apiserver-arm64"`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "identifier_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").identifier()`,
|
||||
expectValue: types.String("testtag"),
|
||||
},
|
||||
{
|
||||
name: "default_identifier",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").identifier()`,
|
||||
expectValue: types.String("latest"),
|
||||
},
|
||||
{
|
||||
name: "identifer_digest",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").identifier()`,
|
||||
expectValue: types.String("sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"),
|
||||
},
|
||||
{
|
||||
name: "identifer_digest_and_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:latest@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").identifier()`,
|
||||
expectValue: types.String("sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"),
|
||||
},
|
||||
{
|
||||
name: "tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").tag()`,
|
||||
expectValue: types.String("testtag"),
|
||||
},
|
||||
{
|
||||
name: "default_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").tag()`,
|
||||
expectValue: types.String("latest"),
|
||||
},
|
||||
{
|
||||
name: "no_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").tag()`,
|
||||
expectValue: types.String(""),
|
||||
},
|
||||
{
|
||||
name: "identifier_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").identifier()`,
|
||||
expectValue: types.String("testtag"),
|
||||
},
|
||||
{
|
||||
name: "no_digest",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64").digest()`,
|
||||
expectValue: types.String(""),
|
||||
},
|
||||
{
|
||||
name: "digest_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").digest()`,
|
||||
expectValue: types.String(""),
|
||||
},
|
||||
{
|
||||
name: "digest",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").digest()`,
|
||||
expectValue: types.String("sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"),
|
||||
},
|
||||
{
|
||||
name: "digest_digest_and_tag",
|
||||
expr: `image("registry.k8s.io/kube-apiserver-arm64:latest@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").digest() == "sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testImageLib(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr)
|
||||
})
|
||||
}
|
||||
}
|
60
pkg/cel/libs/image/types.go
Normal file
60
pkg/cel/libs/image/types.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
var ImageType = cel.ObjectType("kyverno.image")
|
||||
|
||||
type ImageReference struct {
|
||||
Image string `json:"image,omitempty"`
|
||||
Registry string `json:"registry,omitempty"`
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Identifier string `json:"identifier,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
ImageReference
|
||||
}
|
||||
|
||||
func (v Image) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
|
||||
if reflect.TypeOf(v.ImageReference).AssignableTo(typeDesc) {
|
||||
return v.ImageReference, nil
|
||||
}
|
||||
if reflect.TypeOf("").AssignableTo(typeDesc) {
|
||||
return v.ImageReference.Image, nil
|
||||
}
|
||||
return nil, fmt.Errorf("type conversion error from 'Image' to '%v'", typeDesc)
|
||||
}
|
||||
|
||||
func (v Image) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
switch typeVal {
|
||||
case ImageType:
|
||||
return v
|
||||
default:
|
||||
return types.NewErr("type conversion error from '%s' to '%s'", ImageType, typeVal)
|
||||
}
|
||||
}
|
||||
|
||||
func (v Image) Equal(other ref.Val) ref.Val {
|
||||
img, ok := other.(Image)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(other)
|
||||
}
|
||||
return types.Bool(reflect.DeepEqual(v.ImageReference, img.ImageReference))
|
||||
}
|
||||
|
||||
func (v Image) Type() ref.Type {
|
||||
return ImageType
|
||||
}
|
||||
|
||||
func (v Image) Value() interface{} {
|
||||
return v.ImageReference
|
||||
}
|
|
@ -91,9 +91,13 @@ func (cp *contextProvider) GetGlobalReference(name, projection string) (any, err
|
|||
}
|
||||
}
|
||||
|
||||
func (cp *contextProvider) GetImageData(image string) (*imagedataloader.ImageData, error) {
|
||||
func (cp *contextProvider) GetImageData(image string) (map[string]interface{}, error) {
|
||||
// TODO: get image credentials from image verification policies?
|
||||
return cp.imagedata.FetchImageData(context.TODO(), image)
|
||||
data, err := cp.imagedata.FetchImageData(context.TODO(), image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getValue(data.Data())
|
||||
}
|
||||
|
||||
func isLikelyKubernetesObject(data any) bool {
|
||||
|
@ -132,10 +136,6 @@ func (cp *contextProvider) GetResource(apiVersion, resource, namespace, name str
|
|||
return resourceInteface.Get(context.TODO(), name, metav1.GetOptions{})
|
||||
}
|
||||
|
||||
func (cp *contextProvider) ParseImageReference(image string) (imagedataloader.ImageReference, error) {
|
||||
return imagedataloader.ParseImageReference(image)
|
||||
}
|
||||
|
||||
func (cp *contextProvider) getResourceClient(groupVersion schema.GroupVersion, resource string, namespace string) dynamic.ResourceInterface {
|
||||
client := cp.dclient.Resource(groupVersion.WithResource(resource))
|
||||
if namespace != "" {
|
||||
|
|
|
@ -3,7 +3,6 @@ package policy
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -48,14 +47,10 @@ func (cp *FakeContextProvider) GetGlobalReference(string, string) (any, error) {
|
|||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (cp *FakeContextProvider) GetImageData(string) (*imagedataloader.ImageData, error) {
|
||||
func (cp *FakeContextProvider) GetImageData(string) (map[string]interface{}, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (cp *FakeContextProvider) ParseImageReference(image string) (imagedataloader.ImageReference, error) {
|
||||
return imagedataloader.ParseImageReference(image)
|
||||
}
|
||||
|
||||
func (cp *FakeContextProvider) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
|
||||
gv, err := schema.ParseGroupVersion(apiVersion)
|
||||
if err != nil {
|
||||
|
|
16
pkg/cel/policy/utils.go
Normal file
16
pkg/cel/policy/utils.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package policy
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func getValue(data any) (map[string]interface{}, error) {
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiData := map[string]interface{}{}
|
||||
err = json.Unmarshal(raw, &apiData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return apiData, nil
|
||||
}
|
|
@ -160,10 +160,7 @@ type ImageData struct {
|
|||
RemoteOpts []remote.Option
|
||||
NameOpts []name.Option
|
||||
|
||||
ImageReference `json:",inline"`
|
||||
ImageIndex interface{} `json:"imageIndex,omitempty"`
|
||||
Manifest *gcrv1.Manifest `json:"manifest,omitempty"`
|
||||
ConfigData *gcrv1.ConfigFile `json:"config,omitempty"`
|
||||
ImageDescriptor `json:",inline"`
|
||||
|
||||
NameRef name.Reference
|
||||
desc *remote.Descriptor
|
||||
|
@ -173,6 +170,13 @@ type ImageData struct {
|
|||
verifiedIntotoPayloads map[string][]byte
|
||||
}
|
||||
|
||||
type ImageDescriptor struct {
|
||||
ImageReference `json:",inline"`
|
||||
ImageIndex interface{} `json:"imageIndex,omitempty"`
|
||||
Manifest *gcrv1.Manifest `json:"manifest,omitempty"`
|
||||
ConfigData *gcrv1.ConfigFile `json:"config,omitempty"`
|
||||
}
|
||||
|
||||
type referrerData struct {
|
||||
layerDescriptor *gcrv1.Descriptor
|
||||
data []byte
|
||||
|
@ -201,6 +205,10 @@ func (i *ImageData) WithDigest(digest string) string {
|
|||
return i.NameRef.Context().Digest(digest).String()
|
||||
}
|
||||
|
||||
func (i *ImageData) Data() ImageDescriptor {
|
||||
return i.ImageDescriptor
|
||||
}
|
||||
|
||||
func (i *ImageData) loadReferrers() error {
|
||||
if i.referrersManifest != nil {
|
||||
return nil
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
|
@ -0,0 +1,24 @@
|
|||
# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json
|
||||
apiVersion: chainsaw.kyverno.io/v1alpha1
|
||||
kind: Test
|
||||
metadata:
|
||||
name: image-data
|
||||
spec:
|
||||
steps:
|
||||
- name: create policy
|
||||
try:
|
||||
- create:
|
||||
file: policy.yaml
|
||||
- sleep:
|
||||
duration: 10s
|
||||
- name: create deployment
|
||||
try:
|
||||
- create:
|
||||
file: deployment.yaml
|
||||
- name: create bad deployment
|
||||
try:
|
||||
- apply:
|
||||
expect:
|
||||
- check:
|
||||
($error != null): true
|
||||
file: bad-deployment.yaml
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: ghcr.io/kyverno/kyverno
|
|
@ -0,0 +1,20 @@
|
|||
apiVersion: policies.kyverno.io/v1alpha1
|
||||
kind: ValidatingPolicy
|
||||
metadata:
|
||||
name: check-images
|
||||
spec:
|
||||
matchConstraints:
|
||||
resourceRules:
|
||||
- apiGroups: [apps]
|
||||
apiVersions: [v1]
|
||||
operations: [CREATE, UPDATE]
|
||||
resources: [deployments]
|
||||
variables:
|
||||
- name: images
|
||||
expression: >-
|
||||
object.spec.template.spec.containers.map(e, image(e.image))
|
||||
validations:
|
||||
- expression: >-
|
||||
variables.images.map(i, i.registry() == "ghcr.io" && !i.containsDigest()).all(e, e)
|
||||
message: >-
|
||||
Deployment must be have images from ghcr and images should be tagged
|
Loading…
Add table
Reference in a new issue