From 453ae3c469c1bb3250373e15a52df4d11b7fe376 Mon Sep 17 00:00:00 2001 From: Piotr Halama Date: Wed, 20 Nov 2024 14:16:47 +0100 Subject: [PATCH] Fix image name parsing (#342) (#343) * Fix image name parsing * simplify validation * test * Adjust test to provide a valid image name --- internal/admission/defaulting_test.go | 2 +- internal/validate/image.go | 48 ++++++++------------------- internal/validate/image_test.go | 35 ++++++++++--------- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/internal/admission/defaulting_test.go b/internal/admission/defaulting_test.go index 7aac919..0865a6d 100644 --- a/internal/admission/defaulting_test.go +++ b/internal/admission/defaulting_test.go @@ -48,7 +48,7 @@ func TestTimeout(t *testing.T) { ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNs, Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, - Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "eu-gcr.io/test:test"}}}, } raw, err := json.Marshal(pod) diff --git a/internal/validate/image.go b/internal/validate/image.go index a3209dc..dfec289 100644 --- a/internal/validate/image.go +++ b/internal/validate/image.go @@ -32,10 +32,6 @@ import ( "github.com/pkg/errors" ) -const ( - tagDelim = ":" -) - //go:generate mockery --name=ImageValidatorService type ImageValidatorService interface { Validate(ctx context.Context, image string, imagePullCredentials map[string]cliType.AuthConfig) error @@ -70,21 +66,18 @@ func (s *notaryService) Validate(ctx context.Context, image string, imagePullCre return nil } - split := strings.Split(image, tagDelim) - - if len(split) != 2 { - return pkg.NewValidationFailedErr(errors.New("image name is not formatted correctly")) + // strict validation requires image name to contain domain and a tag, and/or sha256 + ref, err := name.ParseReference(image, name.StrictValidation) + if err != nil { + return pkg.NewValidationFailedErr(errors.Wrap(err, "image name could not be parsed")) } - imgRepo := split[0] - imgTag := split[1] - - expectedShaBytes, err := s.loggedGetNotaryImageDigestHash(ctx, imgRepo, imgTag) + expectedShaBytes, err := s.loggedGetNotaryImageDigestHash(ctx, ref) if err != nil { return err } - shaImageBytes, shaManifestBytes, err := s.loggedGetRepositoryDigestHash(ctx, image, imagePullCredentials) + shaImageBytes, shaManifestBytes, err := s.loggedGetRepositoryDigestHash(ctx, ref, imagePullCredentials) if err != nil { return err } @@ -111,23 +104,14 @@ func (s *notaryService) isImageAllowed(imgRepo string) bool { return false } -func (s *notaryService) loggedGetRepositoryDigestHash(ctx context.Context, image string, imagePullCredentials map[string]cliType.AuthConfig) ([]byte, []byte, error) { +func (s *notaryService) loggedGetRepositoryDigestHash(ctx context.Context, ref name.Reference, imagePullCredentials map[string]cliType.AuthConfig) ([]byte, []byte, error) { const message = "request to image registry" closeLog := helpers.LogStartTime(ctx, message) defer closeLog() - return s.getRepositoryDigestHash(image, imagePullCredentials) + return s.getRepositoryDigestHash(ref, imagePullCredentials) } -func (s *notaryService) getRepositoryDigestHash(image string, imagePullCredentials map[string]cliType.AuthConfig) ([]byte, []byte, error) { - if len(image) == 0 { - return nil, nil, pkg.NewValidationFailedErr(errors.New("empty image provided")) - } - - ref, err := name.ParseReference(image) - if err != nil { - return nil, nil, pkg.NewValidationFailedErr(errors.Wrap(err, "ref parse")) - } - +func (s *notaryService) getRepositoryDigestHash(ref name.Reference, imagePullCredentials map[string]cliType.AuthConfig) ([]byte, []byte, error) { remoteOptions := make([]remote.Option, 0) // check if we have credentials for the registry, and use them @@ -233,22 +217,18 @@ func getImageDigestHash(ref name.Reference, remoteOptions ...remote.Option) ([]b return digestBytes, manifestBytes, nil } -func (s *notaryService) loggedGetNotaryImageDigestHash(ctx context.Context, imgRepo, imgTag string) ([]byte, error) { +func (s *notaryService) loggedGetNotaryImageDigestHash(ctx context.Context, ref name.Reference) ([]byte, error) { const message = "request to notary" closeLog := helpers.LogStartTime(ctx, message) defer closeLog() - result, err := s.getNotaryImageDigestHash(ctx, imgRepo, imgTag) + result, err := s.getNotaryImageDigestHash(ctx, ref) return result, err } -func (s *notaryService) getNotaryImageDigestHash(ctx context.Context, imgRepo, imgTag string) ([]byte, error) { - if len(imgRepo) == 0 || len(imgTag) == 0 { - return nil, pkg.NewValidationFailedErr(errors.New("empty arguments provided")) - } - +func (s *notaryService) getNotaryImageDigestHash(ctx context.Context, ref name.Reference) ([]byte, error) { const messageNewRepoClient = "request to notary (NewRepoClient)" closeLog := helpers.LogStartTime(ctx, messageNewRepoClient) - c, err := s.RepoFactory.NewRepoClient(imgRepo, s.NotaryConfig) + c, err := s.RepoFactory.NewRepoClient(ref.Context().Name(), s.NotaryConfig) closeLog() if err != nil { return nil, pkg.NewUnknownResultErr(err) @@ -256,7 +236,7 @@ func (s *notaryService) getNotaryImageDigestHash(ctx context.Context, imgRepo, i const messageGetTargetByName = "request to notary (GetTargetByName)" closeLog = helpers.LogStartTime(ctx, messageGetTargetByName) - target, err := c.GetTargetByName(imgTag) + target, err := c.GetTargetByName(ref.Identifier()) closeLog() if err != nil { return nil, parseNotaryErr(err) diff --git a/internal/validate/image_test.go b/internal/validate/image_test.go index 2eb1503..ae01b6a 100644 --- a/internal/validate/image_test.go +++ b/internal/validate/image_test.go @@ -54,12 +54,12 @@ var ( hash: []byte{33, 98, 102, 200, 111, 196, 220, 239, 86, 25, 147, 11, 211, 148, 36, 88, 36, 194, 175, 82, 253, 33, 186, 124, 111, 160, 230, 24, 101, 125, 76, 59}, } differentHashImage = image{ - name: "nginx", + name: "docker.io/library/nginx", tag: "latest", hash: []byte{1, 2, 3, 4}, } untrustedImage = image{ - name: "nginx", + name: "docker.io/library/nginx", tag: "untrusted", } unknownImage = image{ @@ -98,26 +98,31 @@ func Test_Validate_ProperIndex_ShouldPass(t *testing.T) { func Test_Validate_InvalidImageName_ShouldReturnError(t *testing.T) { cfg := validate.ServiceConfig{NotaryConfig: validate.NotaryConfig{}} f := setupMockFactory() + expectedErrMsg := "image name could not be parsed" tests := []struct { - name string - imageName string - expectedErrMsg string + name string + imageName string }{ { - name: "image name without semicolon", - imageName: "makapaka", - expectedErrMsg: "image name is not formatted correctly", + name: "image name without tag and domain", + imageName: "makapaka", }, { - name: "", - imageName: ":", - expectedErrMsg: "empty arguments provided", + name: "image name without domain", + imageName: "makapaka:latest", }, { - name: "image name with more than one semicolon", //TODO: IMO it's proper image name, but now is not allowed - imageName: "repo:port/image-name:tag", - expectedErrMsg: "image name is not formatted correctly", + name: "image name without tag", + imageName: "domain.com/makapaka", + }, + { + name: "", + imageName: ":", + }, + { + name: "image name with more than two semicolon in hash", + imageName: "repo.com:123/image-name:tag:hash", }, } for _, tt := range tests { @@ -126,7 +131,7 @@ func Test_Validate_InvalidImageName_ShouldReturnError(t *testing.T) { err := s.Validate(context.TODO(), tt.imageName, emptyAuthData) - require.ErrorContains(t, err, tt.expectedErrMsg) + require.ErrorContains(t, err, expectedErrMsg) }) } }