From 2eaf8fac24fc1a83bf3a7f648537f7a5ad5de299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Thu, 13 Apr 2023 13:52:15 -0500 Subject: [PATCH 01/11] Add policy-controller annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- cmd/webhook/main.go | 15 +- pkg/webhook/validator.go | 348 +++++++++++++++ pkg/webhook/validator_test.go | 813 ++++++++++++++++++++++++++++++++++ 3 files changed, 1172 insertions(+), 4 deletions(-) diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index daafa8580..94e092766 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -202,6 +202,11 @@ func NewValidatingAdmissionController(ctx context.Context, cmw configmap.Watcher } func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { + store := config.NewStore(logging.FromContext(ctx).Named("config-store")) + store.WatchConfigs(cmw) + policyControllerConfigStore := policycontrollerconfig.NewStore(logging.FromContext(ctx).Named("config-policy-controller")) + policyControllerConfigStore.WatchConfigs(cmw) + kc := kubeclient.Get(ctx) validator := cwebhook.NewValidator(ctx) @@ -218,10 +223,12 @@ func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) // A function that infuses the context passed to Validate/SetDefaults with custom metadata. func(ctx context.Context) context.Context { ctx = context.WithValue(ctx, kubeclient.Key{}, kc) - ctx = policyduckv1beta1.WithPodScalableDefaulter(ctx, validator.ResolvePodScalable) - ctx = duckv1.WithPodDefaulter(ctx, validator.ResolvePod) - ctx = duckv1.WithPodSpecDefaulter(ctx, validator.ResolvePodSpecable) - ctx = duckv1.WithCronJobDefaulter(ctx, validator.ResolveCronJob) + ctx = store.ToContext(ctx) + ctx = policyControllerConfigStore.ToContext(ctx) + ctx = policyduckv1beta1.WithPodScalableDefaulter(ctx, validator.PodScalableDefaulter) + ctx = duckv1.WithPodDefaulter(ctx, validator.PodDefaulter) + ctx = duckv1.WithPodSpecDefaulter(ctx, validator.PodSpecableDefaulter) + ctx = duckv1.WithCronJobDefaulter(ctx, validator.CronJobDefaulter) return ctx }, diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index d17a239c6..a75fb20cb 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -961,6 +961,30 @@ func (v *Validator) ResolvePodSpecable(ctx context.Context, wp *duckv1.WithPod) v.resolvePodSpec(ctx, &wp.Spec.Template.Spec, opt) } +// PodDefaulter implements duckv1.PodValidator +func (v *Validator) PodDefaulter(ctx context.Context, p *duckv1.Pod) { + v.ResolvePod(ctx, p) + v.AnnotatePod(ctx, p) +} + +// PodSpecableDefaulter implements duckv1.PodSpecValidator +func (v *Validator) PodSpecableDefaulter(ctx context.Context, wp *duckv1.WithPod) { + v.ResolvePodSpecable(ctx, wp) + v.AnnotatePodSpecable(ctx, wp) +} + +// PodScalableDefaulter implements policyduckv1beta1.PodScalableValidator +func (v *Validator) PodScalableDefaulter(ctx context.Context, ps *policyduckv1beta1.PodScalable) { + v.ResolvePodScalable(ctx, ps) + v.AnnotatePodScalable(ctx, ps) +} + +// CronJobDefaulter implements duckv1.CronJobValidator +func (v *Validator) CronJobDefaulter(ctx context.Context, c *duckv1.CronJob) { + v.ResolveCronJob(ctx, c) + v.AnnotateCronJob(ctx, c) +} + // ResolvePod implements duckv1.PodValidator func (v *Validator) ResolvePod(ctx context.Context, p *duckv1.Pod) { // Don't mess with things that are being deleted or already deleted or @@ -1063,6 +1087,330 @@ func (v *Validator) resolvePodSpec(ctx context.Context, ps *corev1.PodSpec, opt resolveEphemeralContainers(ps.EphemeralContainers) } +const ResultsAnnotationKey = "policy.sigstore.dev/policy-controller-results" + +// AnnotatePod implements duckv1.PodValidator +func (v *Validator) AnnotatePod(ctx context.Context, p *duckv1.Pod) { + // Don't mess with things that are being deleted or already deleted or + // status update. + if isDeletedOrStatusUpdate(ctx, p.DeletionTimestamp) { + return + } + + // Attach the spec/metadata for down the line to be attached if it's + // required by policy to be included in the PolicyResult. + ctx = IncludeSpec(ctx, p.Spec) + ctx = IncludeObjectMeta(ctx, p.ObjectMeta) + + imagePullSecrets := make([]string, 0, len(p.Spec.ImagePullSecrets)) + for _, s := range p.Spec.ImagePullSecrets { + imagePullSecrets = append(imagePullSecrets, s.Name) + } + + ns := getNamespace(ctx, p.Namespace) + opt := k8schain.Options{ + Namespace: ns, + ServiceAccountName: p.Spec.ServiceAccountName, + ImagePullSecrets: imagePullSecrets, + } + + v.annotatePodSpec(ctx, ns, p.Kind, p.APIVersion, &p.ObjectMeta, &p.Spec, opt) +} + +func (v *Validator) AnnotatePodSpecable(ctx context.Context, wp *duckv1.WithPod) { + // Don't mess with things that are being deleted or already deleted or + // status update. + if isDeletedOrStatusUpdate(ctx, wp.DeletionTimestamp) { + return + } + + // Attach the spec/metadata for down the line to be attached if it's + // required by policy to be included in the PolicyResult. + ctx = IncludeSpec(ctx, wp.Spec) + ctx = IncludeObjectMeta(ctx, wp.ObjectMeta) + ctx = IncludeTypeMeta(ctx, wp.TypeMeta) + + imagePullSecrets := make([]string, 0, len(wp.Spec.Template.Spec.ImagePullSecrets)) + for _, s := range wp.Spec.Template.Spec.ImagePullSecrets { + imagePullSecrets = append(imagePullSecrets, s.Name) + } + ns := getNamespace(ctx, wp.Namespace) + opt := k8schain.Options{ + Namespace: ns, + ServiceAccountName: wp.Spec.Template.Spec.ServiceAccountName, + ImagePullSecrets: imagePullSecrets, + } + + v.annotatePodSpec(ctx, ns, wp.Kind, wp.APIVersion, &wp.ObjectMeta, &wp.Spec.Template.Spec, opt) +} + +func (v *Validator) AnnotatePodScalable(ctx context.Context, ps *policyduckv1beta1.PodScalable) { + // If we are deleting (or already deleted) or updating status, don't block. + if isDeletedOrStatusUpdate(ctx, ps.DeletionTimestamp) { + return + } + + // If we are being scaled down don't block it. + if ps.IsScalingDown(ctx) { + logging.FromContext(ctx).Debugf("Skipping annotations due to scale down request %s/%s", &ps.ObjectMeta.Name, &ps.ObjectMeta.Namespace) + return + } + + // Attach the spec for down the line to be attached if it's required by + // policy to be included in the PolicyResult. + ctx = IncludeSpec(ctx, ps.Spec) + ctx = IncludeObjectMeta(ctx, ps.ObjectMeta) + ctx = IncludeTypeMeta(ctx, ps.TypeMeta) + + imagePullSecrets := make([]string, 0, len(ps.Spec.Template.Spec.ImagePullSecrets)) + for _, s := range ps.Spec.Template.Spec.ImagePullSecrets { + imagePullSecrets = append(imagePullSecrets, s.Name) + } + ns := getNamespace(ctx, ps.Namespace) + opt := k8schain.Options{ + Namespace: ns, + ServiceAccountName: ps.Spec.Template.Spec.ServiceAccountName, + ImagePullSecrets: imagePullSecrets, + } + + v.annotatePodSpec(ctx, ns, ps.Kind, ps.APIVersion, &ps.ObjectMeta, &ps.Spec.Template.Spec, opt) +} + +func (v *Validator) AnnotateCronJob(ctx context.Context, c *duckv1.CronJob) { + // If we are deleting (or already deleted) or updating status, don't block. + if isDeletedOrStatusUpdate(ctx, c.DeletionTimestamp) { + return + } + + // Attach the spec/metadata for down the line to be attached if it's + // required by policy to be included in the PolicyResult. + ctx = IncludeSpec(ctx, c.Spec) + ctx = IncludeObjectMeta(ctx, c.ObjectMeta) + ctx = IncludeTypeMeta(ctx, c.TypeMeta) + + imagePullSecrets := make([]string, 0, len(c.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets)) + for _, s := range c.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets { + imagePullSecrets = append(imagePullSecrets, s.Name) + } + ns := getNamespace(ctx, c.Namespace) + opt := k8schain.Options{ + Namespace: ns, + ServiceAccountName: c.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, + ImagePullSecrets: imagePullSecrets, + } + + v.annotatePodSpec(ctx, ns, c.Kind, c.APIVersion, &c.ObjectMeta, &c.Spec.JobTemplate.Spec.Template.Spec, opt) +} + +func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVersion string, objectMeta *metav1.ObjectMeta, ps *corev1.PodSpec, opt k8schain.Options) { + kc, err := k8schain.New(ctx, kubeclient.Get(ctx), opt) + if err != nil { + logging.FromContext(ctx).Warnf("Unable to build k8schain: %v", err) + return + } + + labels := objectMeta.Labels + annotations := make([]*ContainerAnnotation, 0) + + checkContainers := func(cs []corev1.Container, field string) { + results := make(chan *ContainerAnnotation, len(cs)) + wg := new(sync.WaitGroup) + for i, c := range cs { + i := i + c := c + wg.Add(1) + go func() { + defer wg.Done() + + // Require digests, otherwise the validation is meaningless + // since the tag can move. + fe := refOrFieldError(c.Image, field, i) + if fe != nil { + results <- &ContainerAnnotation{ + Index: i, + Name: c.Name, + Image: c.Image, + Field: field, + Result: fe.Message, + } + return + } + + containerAnnotation := v.generateContainerImageAnnotation(ctx, c.Image, namespace, c.Name, field, i, kind, apiVersion, labels, kc, ociremote.WithRemoteOptions( + remote.WithContext(ctx), + remote.WithAuthFromKeychain(kc), + )) + results <- containerAnnotation + }() + } + for i := 0; i < len(cs); i++ { + select { + case <-ctx.Done(): + logging.FromContext(ctx).Warnf("context was canceled before annotations completed") + case result, ok := <-results: + if !ok { + logging.FromContext(ctx).Warnf("Annotation results channel failed to produce a result") + } else { + if result != nil { + annotations = append(annotations, result) + } + } + } + } + wg.Wait() + } + + checkEphemeralContainers := func(cs []corev1.EphemeralContainer, field string) { + results := make(chan *ContainerAnnotation, len(cs)) + wg := new(sync.WaitGroup) + for i, c := range cs { + i := i + c := c + wg.Add(1) + go func() { + defer wg.Done() + + // Require digests, otherwise the validation is meaningless + // since the tag can move. + fe := refOrFieldError(c.Image, field, i) + if fe != nil { + results <- &ContainerAnnotation{ + Index: i, + Name: c.Name, + Image: c.Image, + Field: field, + Result: fe.Message, + } + return + } + + containerAnnotation := v.generateContainerImageAnnotation(ctx, c.Image, namespace, c.Name, field, i, kind, apiVersion, labels, kc, ociremote.WithRemoteOptions( + remote.WithContext(ctx), + remote.WithAuthFromKeychain(kc), + )) + results <- containerAnnotation + }() + } + for i := 0; i < len(cs); i++ { + select { + case <-ctx.Done(): + logging.FromContext(ctx).Warnf("context was canceled before annotations completed") + case result, ok := <-results: + if !ok { + logging.FromContext(ctx).Warnf("Annotation results channel failed to produce a result") + } else { + if result != nil { + annotations = append(annotations, result) + } + } + } + } + wg.Wait() + } + + checkContainers(ps.InitContainers, "initContainers") + checkContainers(ps.Containers, "containers") + checkEphemeralContainers(ps.EphemeralContainers, "ephemeralContainers") + resultAnnotations := ResultAnnotations{ + ContainerResults: annotations, + } + + annotationBytes, err := json.Marshal(resultAnnotations) + if err != nil { + logging.FromContext(ctx).Warnf("Unable to marshal annotatios: %v", err) + return + } + if objectMeta.Annotations == nil { + objectMeta.Annotations = make(map[string]string) + } + objectMeta.Annotations[ResultsAnnotationKey] = string(annotationBytes) +} + +// ResultAnnotations is a list of ContainerAnnotations that will be added to +// the resource during the mutation phase +type ResultAnnotations struct { + ContainerResults []*ContainerAnnotation `json:"containerResults"` +} + +// ContainerAnnotation stores the results of the validations so the +// users can see which policies were evaluated for each container +type ContainerAnnotation struct { + Index int `json:"index"` + Name string `json:"name"` + Image string `json:"image"` + Field string `json:"field"` + Result string `json:"result"` + ResultMsg string `json:"resultMsg"` + PolicyResults map[string]*PolicyResult `json:"policyResults,omitempty"` + PolicyErrors map[string][]string `json:"policyErrors,omitempty"` +} + +func (v *Validator) generateContainerImageAnnotation(ctx context.Context, containerImage string, namespace, containerName string, field string, index int, kind, apiVersion string, labels map[string]string, kc authn.Keychain, ociRemoteOpts ...ociremote.Option) *ContainerAnnotation { + annotation := &ContainerAnnotation{ + Index: index, + Name: containerName, + Image: containerImage, + Field: field, + Result: "deny", + PolicyResults: make(map[string]*PolicyResult), + PolicyErrors: make(map[string][]string), + } + ref, err := name.ParseReference(containerImage) + if err != nil { + annotation.ResultMsg = err.Error() + return annotation + } + config := config.FromContext(ctx) + + if config != nil { + policies, err := config.ImagePolicyConfig.GetMatchingPolicies(ref.Name(), kind, apiVersion, labels) + if err != nil { + annotation.ResultMsg = err.Error() + return annotation + } + + // If there is at least one policy that matches, that means it + // has to be satisfied. + if len(policies) > 0 { + signatures, fieldErrors := validatePolicies(ctx, namespace, ref, policies, kc, ociRemoteOpts...) + annotation.PolicyResults = signatures + for failingPolicy, policyErrs := range fieldErrors { + for _, policyErr := range policyErrs{ + var fe *apis.FieldError + if errors.As(policyErr, &fe) { + if fe.Filter(apis.WarningLevel) != nil { + annotation.Result = "warn" + } + annotation.PolicyErrors[failingPolicy] = append(annotation.PolicyErrors[failingPolicy], strings.Trim(fe.Message, "\n")) + } else { + annotation.PolicyErrors[failingPolicy] = append(annotation.PolicyErrors[failingPolicy], strings.Trim(policyErr.Error(), "\n")) + } + } + } + if len(signatures) != len(policies) { + annotation.ResultMsg = fmt.Sprintf("Failed to validate at least one policy for %s wanted %d policies, only validated %d", ref.Name(), len(policies), len(signatures)) + } else { + annotation.ResultMsg = fmt.Sprintf("Validated %d policies for image %s", len(signatures), containerImage) + annotation.Result = "allow" + } + return annotation + } + + // Container matched no policies + noMatchingError := setNoMatchingPoliciesError(ctx, containerImage, field, index) + if noMatchingError != nil { + annotation.ResultMsg = noMatchingError.Message + } else{ + annotation.ResultMsg = fmt.Sprintf("No matching policies for %s", containerImage) + annotation.Result = "allow" + } + + return annotation + } + + return nil +} + // getNamespace tries to extract the namespace from the HTTPRequest // if the namespace passed as argument is empty. This is a workaround // for a bug in k8s <= 1.24. diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go index 5ae23ca1b..12eb55d07 100644 --- a/pkg/webhook/validator_test.go +++ b/pkg/webhook/validator_test.go @@ -34,6 +34,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v2/pkg/cosign" @@ -3271,3 +3272,815 @@ func TestCheckOptsFromAuthority(t *testing.T) { }) } } + +func TestAnnotatePod(t *testing.T) { + tag := name.MustParseReference("gcr.io/distroless/static:nonroot") + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + // Resolved via crane digest on 2022/09/29 + digestNewer := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e") + + ctx, _ := rtesting.SetupFakeContext(t) + + // Non-existent URL for testing complete failure + badURL := apis.HTTP("http://example.com/") + + fulcioURL, err := apis.ParseURL("https://fulcio.sigstore.dev") + if err != nil { + t.Fatalf("Failed to parse fake Fulcio URL") + } + + rekorServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Write([]byte(rekorResponse)) + })) + t.Cleanup(rekorServer.Close) + rekorURL, err := apis.ParseURL(rekorServer.URL) + if err != nil { + t.Fatalf("Failed to parse fake Rekor URL") + } + + var authorityKeyCosignPub *ecdsa.PublicKey + + pems := parsePems([]byte(authorityKeyCosignPubString)) + if len(pems) > 0 { + key, _ := x509.ParsePKIXPublicKey(pems[0].Bytes) + authorityKeyCosignPub = key.(*ecdsa.PublicKey) + } else { + t.Errorf("Error parsing authority key from string") + } + + kc := fakekube.Get(ctx) + // Setup service acc and fakeSignaturePullSecrets for "default" and "cosign-system" namespace + for _, ns := range []string{"default", system.Namespace()} { + kc.CoreV1().ServiceAccounts(ns).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + }, metav1.CreateOptions{}) + + kc.CoreV1().Secrets(ns).Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fakeSignaturePullSecrets", + }, + Data: map[string][]byte{ + "dockerconfigjson": []byte(`{"auths":{"https://index.docker.io/v1/":{"username":"username","password":"password","auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}`), + }, + }, metav1.CreateOptions{}) + } + + v := NewValidator(ctx) + + cvs := cosignVerifySignatures + defer func() { + cosignVerifySignatures = cvs + }() + // Let's just say that everything is verified. + pass := func(_ context.Context, _ name.Reference, _ *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + sig, err := static.NewSignature(nil, "") + if err != nil { + return nil, false, err + } + return []oci.Signature{sig}, true, nil + } + // Let's just say that everything is not verified. + fail := func(_ context.Context, _ name.Reference, _ *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + return nil, false, errors.New("bad signature") + } + + // Let's say it is verified if it is the expected Public Key + authorityPublicKeyCVS := func(ctx context.Context, signedImgRef name.Reference, co *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + actualPublicKey, _ := co.SigVerifier.PublicKey() + actualECDSAPubkey := actualPublicKey.(*ecdsa.PublicKey) + actualKeyData := elliptic.Marshal(actualECDSAPubkey, actualECDSAPubkey.X, actualECDSAPubkey.Y) + + expectedKeyData := elliptic.Marshal(authorityKeyCosignPub, authorityKeyCosignPub.X, authorityKeyCosignPub.Y) + + if bytes.Equal(actualKeyData, expectedKeyData) { + return pass(ctx, signedImgRef, co) + } + + return fail(ctx, signedImgRef, co) + } + + tests := []struct { + name string + ps *corev1.PodSpec + want string + cvs func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + customContext context.Context + }{{ + name: "simple, no error", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}}]}`, + cvs: pass, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Key: &webhookcip.KeyRef{ + Data: authorityKeyCosignPubString, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + HashAlgorithm: signaturealgo.DefaultSignatureAlgorithm, + HashAlgorithmCode: crypto.SHA256, + }, + }, + }, + }, + }, + }, + }, + ), + }, { + name: "bad reference", + ps: &corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Image: "in@valid", + }}, + }, + want: `{"containerResults":[{"index":0,"name":"user-container","image":"in@valid","field":"containers","result":"could not parse reference: in@valid","resultMsg":""}]}`, + cvs: fail, + }, { + name: "not digest", + ps: &corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Image: tag.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot","field":"containers","result":"invalid value: gcr.io/distroless/static:nonroot must be an image digest","resultMsg":""}]}`, + cvs: fail, + }, { + name: "simple, no error, authority key", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Key: &webhookcip.KeyRef{ + Data: authorityKeyCosignPubString, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + HashAlgorithm: signaturealgo.DefaultSignatureAlgorithm, + HashAlgorithmCode: crypto.SHA256, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: authorityPublicKeyCVS, + }, { + name: "simple, error, authority keyless, bad fulcio", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: badURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: fail, + }, { + name: "simple, error, authority keyless, good fulcio, no rekor", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: fail, + }, { + name: "simple, authority keyless checks out, good fulcio, bad cip policy", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless-bad-cip":["failed evaluating cue policy for ClusterImagePolicy: failed to compile the cue policy with error: string literal not terminated"]}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless-bad-cip":["failed evaluating cue policy for ClusterImagePolicy: failed to compile the cue policy with error: string literal not terminated"]}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless-bad-cip": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + Policy: &webhookcip.AttestationPolicy{ + Name: "invalid json policy", + Type: "cue", + Data: `{"wontgo`, + }, + }, + }, + }, + }, + ), + cvs: pass, + }, { + name: "simple, no error, authority keyless, good fulcio", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy-keyless":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy-keyless":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: pass, + }, { + name: "simple, error, authority keyless, good fulcio, bad rekor", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + CTLog: &v1alpha1.TLog{ + URL: rekorURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: fail, + }, { + name: "simple with 2 containers, error, authority keyless, good fulcio, bad rekor", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }, { + Name: "user-container-2", + Image: digestNewer.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}},{"index":1,"name":"user-container-2","image":"gcr.io/distroless/static:nonroot@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e","field":"containers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e: bad signature"]}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"deny","resultMsg":"Failed to validate at least one policy for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4 wanted 1 policies, only validated 0","policyErrors":{"cluster-image-policy-keyless":["signature keyless validation failed for authority for gcr.io/distroless/static@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4: bad signature"]}}]}`, + customContext: config.ToContext(context.Background(), + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy-keyless": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Keyless: &webhookcip.KeylessRef{ + URL: fulcioURL, + }, + CTLog: &v1alpha1.TLog{ + URL: rekorURL, + }, + }, + }, + }, + }, + }, + }, + ), + cvs: fail, + }, { + name: "simple, no error, authority source signaturePullSecrets, non existing secret", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}}]}`, + customContext: config.ToContext(ctx, + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Key: &webhookcip.KeyRef{ + Data: authorityKeyCosignPubString, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + HashAlgorithmCode: crypto.SHA256, + HashAlgorithm: signaturealgo.DefaultSignatureAlgorithm, + }, + Sources: []v1alpha1.Source{{ + OCI: "example.com/alternative/signature", + SignaturePullSecrets: []corev1.LocalObjectReference{{ + Name: "non-existing-secret", + }}, + }}, + }, + }, + }, + }, + }, + }, + ), + cvs: pass, + }, { + name: "simple, no error, authority source signaturePullSecrets, valid secret", + ps: &corev1.PodSpec{ + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }, { + Name: "user-container-2", + Image: digestNewer.String(), + }}, + }, + want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":1,"name":"user-container-2","image":"gcr.io/distroless/static:nonroot@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:2a9e2b4fa771d31fe3346a873be845bfc2159695b9f90ca08e950497006ccc2e","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}}]}`, + customContext: config.ToContext(ctx, + &config.Config{ + ImagePolicyConfig: &config.ImagePolicyConfig{ + Policies: map[string]webhookcip.ClusterImagePolicy{ + "cluster-image-policy": { + Images: []v1alpha1.ImagePattern{{ + Glob: "gcr.io/*/*", + }}, + Authorities: []webhookcip.Authority{ + { + Key: &webhookcip.KeyRef{ + Data: authorityKeyCosignPubString, + PublicKeys: []crypto.PublicKey{authorityKeyCosignPub}, + HashAlgorithm: signaturealgo.DefaultSignatureAlgorithm, + HashAlgorithmCode: crypto.SHA256, + }, + Sources: []v1alpha1.Source{{ + OCI: "example.com/alternative/signature", + SignaturePullSecrets: []corev1.LocalObjectReference{{ + Name: "fakeSignaturePullSecrets", + }}, + }}, + }, + }, + }, + }, + }, + }, + ), + cvs: authorityPublicKeyCVS, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, mode := range []string{"", "enforce", "warn"} { + cosignVerifySignatures = test.cvs + testContext := context.Background() + // By default we want errors. However, iff the mode above is + // warn, and we're using a custom context and therefore + // triggering the CIP.mode twiddling below, check for warnings. + if test.customContext != nil { + // If we are testing with custom context, loop through + // all the modes here. It's a bit silly that we spin through + // all the tests 3 times, but for now this is better than + // duplicating all the CIPs with just different modes. + testContext = test.customContext + + // Twiddle the mode for tests. + cfg := config.FromContext(testContext) + newPolicies := make(map[string]webhookcip.ClusterImagePolicy, len(cfg.ImagePolicyConfig.Policies)) + for k, v := range cfg.ImagePolicyConfig.Policies { + v.Mode = mode + newPolicies[k] = v + } + cfg.ImagePolicyConfig.Policies = newPolicies + config.ToContext(testContext, cfg) + } + + testContext = context.WithValue(testContext, kubeclient.Key{}, kc) + got := &metav1.ObjectMeta{} + // Check the core mechanics + v.annotatePodSpec(testContext, system.Namespace(), "Pod", "v1", got, test.ps, k8schain.Options{}) + want := test.want + if mode == "warn" { + want = strings.ReplaceAll(test.want, `"result":"deny"`, `"result":"warn"`) + } + if !annotationsMatch(t, test.name, got.Annotations[ResultsAnnotationKey], want) { + t.Errorf("annotatePodSpec = %s", cmp.Diff(got.Annotations[ResultsAnnotationKey], want)) + } + + // Check wrapped in a Pod + pod := &duckv1.Pod{ + Spec: *test.ps.DeepCopy(), + } + v.AnnotatePod(testContext, pod) + + if !annotationsMatch(t, test.name, pod.Annotations[ResultsAnnotationKey], want) { + t.Errorf("AnnotatePod = %s", cmp.Diff(pod.Annotations[ResultsAnnotationKey], want)) + } + + // Check that we don't block things being deleted. + pod = &duckv1.Pod{ + Spec: *test.ps.DeepCopy(), + } + if v.AnnotatePod(apis.WithinDelete(testContext), pod); pod.Annotations != nil { + t.Errorf("AnnotatePod() = %v, wanted nil", pod.Annotations) + } + + // Check wrapped in a WithPod + withPod := &duckv1.WithPod{ + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: *test.ps, + }, + }, + } + v.AnnotatePodSpecable(testContext, withPod) + + if !annotationsMatch(t, test.name, withPod.Annotations[ResultsAnnotationKey], want) { + t.Errorf("AnnotatePodSpecable = %s", cmp.Diff(withPod.Annotations[ResultsAnnotationKey], want)) + } + + // Check that we don't block things being deleted. + withPod = &duckv1.WithPod{ + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: *test.ps, + }, + }, + } + if v.AnnotatePodSpecable(apis.WithinDelete(testContext), withPod); withPod.Annotations != nil { + t.Errorf("AnnotatePodSpecable() = %v, wanted nil", withPod.Annotations) + } + + // Check wrapped in a podScalable + podScalable := &policyduckv1beta1.PodScalable{ + Spec: policyduckv1beta1.PodScalableSpec{ + Replicas: ptr.Int32(3), + Template: corev1.PodTemplateSpec{ + Spec: *test.ps, + }, + }, + } + v.AnnotatePodScalable(testContext, podScalable) + + if !annotationsMatch(t, test.name, podScalable.Annotations[ResultsAnnotationKey], want) { + t.Errorf("AnnotatePodScalable = %s", cmp.Diff(podScalable.Annotations[ResultsAnnotationKey], want)) + } + + // Check that we don't block things being deleted. + podScalable = &policyduckv1beta1.PodScalable{ + Spec: policyduckv1beta1.PodScalableSpec{ + Replicas: ptr.Int32(3), + Template: corev1.PodTemplateSpec{ + Spec: *test.ps, + }, + }, + } + if v.AnnotatePodScalable(apis.WithinDelete(testContext), podScalable); podScalable.Annotations != nil { + t.Errorf("AnnotatePodScalable() = %v, wanted nil", podScalable.Annotations) + } + + // Check that we don't block things being scaled down. + original := podScalable.DeepCopy() + original.Spec.Replicas = ptr.Int32(4) + v.AnnotatePodScalable(apis.WithinUpdate(testContext, original), podScalable) + if !annotationsMatch(t, test.name, podScalable.Annotations[ResultsAnnotationKey], original.Annotations[ResultsAnnotationKey]) { + t.Errorf("AnnotatePodScalable() scaling down = %v", cmp.Diff(podScalable.Annotations[ResultsAnnotationKey], original)) + } + + // Check that we fail as expected if being scaled up. + original.Spec.Replicas = ptr.Int32(2) + v.ValidatePodScalable(apis.WithinUpdate(testContext, original), podScalable) + if !annotationsMatch(t, test.name, podScalable.Annotations[ResultsAnnotationKey], original.Annotations[ResultsAnnotationKey]) { + t.Errorf("AnnotatePodScalable() scaling up = %s", cmp.Diff(podScalable.Annotations[ResultsAnnotationKey], original.Annotations[ResultsAnnotationKey])) + } + } + }) + } +} + +func TestAnnotateCronJob(t *testing.T) { + tag := name.MustParseReference("gcr.io/distroless/static:nonroot") + // Resolved via crane digest on 2021/09/25 + digest := name.MustParseReference("gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4") + + ctx, _ := rtesting.SetupFakeContext(t) + + kc := fakekube.Get(ctx) + kc.CoreV1().ServiceAccounts("default").Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + }, metav1.CreateOptions{}) + + v := NewValidator(ctx) + + cvs := cosignVerifySignatures + defer func() { + cosignVerifySignatures = cvs + }() + + // Let's just say that everything is not verified. + fail := func(ctx context.Context, signedImgRef name.Reference, co *cosign.CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + return nil, false, errors.New("bad signature") + } + + tests := []struct { + name string + c *duckv1.CronJob + want string + cvs func(context.Context, name.Reference, *cosign.CheckOpts) ([]oci.Signature, bool, error) + }{{ + name: "k8schain ignore (bad service account)", + c: &duckv1.CronJob{ + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + ServiceAccountName: "not-found", + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + }, + }, + }, + }, + }, + want: `{"containerResults":[]}`, + }, { + name: "k8schain ignore (bad pull secret)", + c: &duckv1.CronJob{ + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{{ + Name: "not-found", + }}, + InitContainers: []corev1.Container{{ + Name: "setup-stuff", + Image: digest.String(), + }}, + Containers: []corev1.Container{{ + Name: "user-container", + Image: digest.String(), + }}, + }, + }, + }, + }, + }, + }, + want: `{"containerResults":[]}`, + }, { + name: "bad reference", + c: &duckv1.CronJob{ + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Image: "in@valid", + }}, + }, + }, + }, + }, + }, + }, + want: `{"containerResults":[{"index":0,"name":"user-container","image":"in@valid","field":"containers","result":"could not parse reference: in@valid","resultMsg":""}]}`, + cvs: fail, + }, { + name: "not digest", + c: &duckv1.CronJob{ + Spec: batchv1.CronJobSpec{ + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "user-container", + Image: tag.String(), + }}, + }, + }, + }, + }, + }, + }, + want: `{"containerResults":[{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot","field":"containers","result":"invalid value: gcr.io/distroless/static:nonroot must be an image digest","resultMsg":""}]}`, + cvs: fail, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cosignVerifySignatures = test.cvs + + testContext := context.WithValue(context.Background(), kubeclient.Key{}, kc) + + // Check the core mechanics + cronJob := test.c.DeepCopy() + want := test.want + v.AnnotateCronJob(testContext, cronJob) + if !annotationsMatch(t, test.name, cronJob.Annotations[ResultsAnnotationKey], want) { + t.Errorf("AnnotateCronJob = %s", cmp.Diff(cronJob.Annotations[ResultsAnnotationKey], want)) + } + + // Check that we don't block things being deleted. + cronJob = test.c.DeepCopy() + if v.AnnotateCronJob(apis.WithinDelete(testContext), cronJob); cronJob.Annotations != nil { + t.Errorf("AnnotateCronJob() = %v, wanted nil", cronJob.Annotations) + } + // Check that we don't block things already deleted. + cronJob = test.c.DeepCopy() + cronJob.DeletionTimestamp = &metav1.Time{Time: time.Now()} + if v.AnnotateCronJob(context.Background(), cronJob); cronJob.Annotations != nil { + t.Errorf("AnnotateCronJob() = %v, wanted nil", cronJob.Annotations) + } + }) + } +} +// Results are returned in different order due to race conditions +func annotationsMatch(t *testing.T, name, got, want string) bool { + if cmp.Equal(got, want) { + return true + } + var gotParsed ResultAnnotations + var wantParsed ResultAnnotations + err := json.Unmarshal([]byte(got), &gotParsed) + + //Checks whether the error is nil or not + if err != nil { + t.Errorf("Failed to parse received JSON in %s = %s", name, got) + } + + err = json.Unmarshal([]byte(want), &wantParsed) + + //Checks whether the error is nil or not + if err != nil { + t.Errorf("Failed to parse wanted JSON in %s = %s", name, want) + } + + if len(gotParsed.ContainerResults) != len(wantParsed.ContainerResults) { + return false + } + + // Sort Container results by Index to compare them + sortFunc := cmpopts.SortSlices(func(a, b *ContainerAnnotation) bool { + return a.Index < b.Index + }) + + return cmp.Equal(gotParsed.ContainerResults, wantParsed.ContainerResults, sortFunc) +} \ No newline at end of file From 2e437a09a197adf9db9717d344fa763444ce30ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Thu, 13 Apr 2023 14:07:55 -0500 Subject: [PATCH 02/11] Run gofmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- pkg/webhook/validator.go | 58 +++++++++++++++++------------------ pkg/webhook/validator_test.go | 13 ++++---- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index a75fb20cb..f49783295 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -1227,12 +1227,12 @@ func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVer fe := refOrFieldError(c.Image, field, i) if fe != nil { results <- &ContainerAnnotation{ - Index: i, - Name: c.Name, - Image: c.Image, - Field: field, - Result: fe.Message, - } + Index: i, + Name: c.Name, + Image: c.Image, + Field: field, + Result: fe.Message, + } return } @@ -1275,12 +1275,12 @@ func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVer fe := refOrFieldError(c.Image, field, i) if fe != nil { results <- &ContainerAnnotation{ - Index: i, - Name: c.Name, - Image: c.Image, - Field: field, - Result: fe.Message, - } + Index: i, + Name: c.Name, + Image: c.Image, + Field: field, + Result: fe.Message, + } return } @@ -1329,31 +1329,31 @@ func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVer // ResultAnnotations is a list of ContainerAnnotations that will be added to // the resource during the mutation phase type ResultAnnotations struct { - ContainerResults []*ContainerAnnotation `json:"containerResults"` + ContainerResults []*ContainerAnnotation `json:"containerResults"` } // ContainerAnnotation stores the results of the validations so the // users can see which policies were evaluated for each container type ContainerAnnotation struct { - Index int `json:"index"` - Name string `json:"name"` - Image string `json:"image"` - Field string `json:"field"` - Result string `json:"result"` - ResultMsg string `json:"resultMsg"` - PolicyResults map[string]*PolicyResult `json:"policyResults,omitempty"` - PolicyErrors map[string][]string `json:"policyErrors,omitempty"` + Index int `json:"index"` + Name string `json:"name"` + Image string `json:"image"` + Field string `json:"field"` + Result string `json:"result"` + ResultMsg string `json:"resultMsg"` + PolicyResults map[string]*PolicyResult `json:"policyResults,omitempty"` + PolicyErrors map[string][]string `json:"policyErrors,omitempty"` } func (v *Validator) generateContainerImageAnnotation(ctx context.Context, containerImage string, namespace, containerName string, field string, index int, kind, apiVersion string, labels map[string]string, kc authn.Keychain, ociRemoteOpts ...ociremote.Option) *ContainerAnnotation { annotation := &ContainerAnnotation{ - Index: index, - Name: containerName, - Image: containerImage, - Field: field, - Result: "deny", + Index: index, + Name: containerName, + Image: containerImage, + Field: field, + Result: "deny", PolicyResults: make(map[string]*PolicyResult), - PolicyErrors: make(map[string][]string), + PolicyErrors: make(map[string][]string), } ref, err := name.ParseReference(containerImage) if err != nil { @@ -1375,7 +1375,7 @@ func (v *Validator) generateContainerImageAnnotation(ctx context.Context, contai signatures, fieldErrors := validatePolicies(ctx, namespace, ref, policies, kc, ociRemoteOpts...) annotation.PolicyResults = signatures for failingPolicy, policyErrs := range fieldErrors { - for _, policyErr := range policyErrs{ + for _, policyErr := range policyErrs { var fe *apis.FieldError if errors.As(policyErr, &fe) { if fe.Filter(apis.WarningLevel) != nil { @@ -1400,7 +1400,7 @@ func (v *Validator) generateContainerImageAnnotation(ctx context.Context, contai noMatchingError := setNoMatchingPoliciesError(ctx, containerImage, field, index) if noMatchingError != nil { annotation.ResultMsg = noMatchingError.Message - } else{ + } else { annotation.ResultMsg = fmt.Sprintf("No matching policies for %s", containerImage) annotation.Result = "allow" } diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go index 12eb55d07..62982b4ea 100644 --- a/pkg/webhook/validator_test.go +++ b/pkg/webhook/validator_test.go @@ -3382,7 +3382,7 @@ func TestAnnotatePod(t *testing.T) { }}, }, want: `{"containerResults":[{"index":0,"name":"setup-stuff","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"initContainers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}},{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","field":"containers","result":"allow","resultMsg":"Validated 1 policies for image gcr.io/distroless/static:nonroot@sha256:be5d77c62dbe7fedfb0a4e5ec2f91078080800ab1f18358e5f31fcc8faa023c4","policyResults":{"cluster-image-policy":{"authorityMatches":{"":{"signatures":[{"id":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}}}}}]}`, - cvs: pass, + cvs: pass, customContext: config.ToContext(context.Background(), &config.Config{ ImagePolicyConfig: &config.ImagePolicyConfig{ @@ -3415,7 +3415,7 @@ func TestAnnotatePod(t *testing.T) { }}, }, want: `{"containerResults":[{"index":0,"name":"user-container","image":"in@valid","field":"containers","result":"could not parse reference: in@valid","resultMsg":""}]}`, - cvs: fail, + cvs: fail, }, { name: "not digest", ps: &corev1.PodSpec{ @@ -3425,7 +3425,7 @@ func TestAnnotatePod(t *testing.T) { }}, }, want: `{"containerResults":[{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot","field":"containers","result":"invalid value: gcr.io/distroless/static:nonroot must be an image digest","resultMsg":""}]}`, - cvs: fail, + cvs: fail, }, { name: "simple, no error, authority key", ps: &corev1.PodSpec{ @@ -4001,7 +4001,7 @@ func TestAnnotateCronJob(t *testing.T) { }, }, want: `{"containerResults":[{"index":0,"name":"user-container","image":"in@valid","field":"containers","result":"could not parse reference: in@valid","resultMsg":""}]}`, - cvs: fail, + cvs: fail, }, { name: "not digest", c: &duckv1.CronJob{ @@ -4021,7 +4021,7 @@ func TestAnnotateCronJob(t *testing.T) { }, }, want: `{"containerResults":[{"index":0,"name":"user-container","image":"gcr.io/distroless/static:nonroot","field":"containers","result":"invalid value: gcr.io/distroless/static:nonroot must be an image digest","resultMsg":""}]}`, - cvs: fail, + cvs: fail, }} for _, test := range tests { @@ -4052,6 +4052,7 @@ func TestAnnotateCronJob(t *testing.T) { }) } } + // Results are returned in different order due to race conditions func annotationsMatch(t *testing.T, name, got, want string) bool { if cmp.Equal(got, want) { @@ -4083,4 +4084,4 @@ func annotationsMatch(t *testing.T, name, got, want string) bool { }) return cmp.Equal(gotParsed.ContainerResults, wantParsed.ContainerResults, sortFunc) -} \ No newline at end of file +} From 08b6d1b97fe4976b0afad6fa0c89086e2efb2feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Thu, 13 Apr 2023 14:34:22 -0500 Subject: [PATCH 03/11] Fix linting issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- pkg/webhook/validator.go | 12 ++++-------- pkg/webhook/validator_test.go | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index f49783295..8a5e36c9c 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -1250,10 +1250,8 @@ func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVer case result, ok := <-results: if !ok { logging.FromContext(ctx).Warnf("Annotation results channel failed to produce a result") - } else { - if result != nil { - annotations = append(annotations, result) - } + } else if result != nil { + annotations = append(annotations, result) } } } @@ -1298,10 +1296,8 @@ func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVer case result, ok := <-results: if !ok { logging.FromContext(ctx).Warnf("Annotation results channel failed to produce a result") - } else { - if result != nil { - annotations = append(annotations, result) - } + } else if result != nil { + annotations = append(annotations, result) } } } diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go index 62982b4ea..e8469378e 100644 --- a/pkg/webhook/validator_test.go +++ b/pkg/webhook/validator_test.go @@ -4062,14 +4062,14 @@ func annotationsMatch(t *testing.T, name, got, want string) bool { var wantParsed ResultAnnotations err := json.Unmarshal([]byte(got), &gotParsed) - //Checks whether the error is nil or not + // Checks whether the error is nil or not if err != nil { t.Errorf("Failed to parse received JSON in %s = %s", name, got) } err = json.Unmarshal([]byte(want), &wantParsed) - //Checks whether the error is nil or not + // Checks whether the error is nil or not if err != nil { t.Errorf("Failed to parse wanted JSON in %s = %s", name, want) } From 8b16c2acc259e1f820f5d820d915883891b32094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Mon, 17 Apr 2023 12:44:24 -0500 Subject: [PATCH 04/11] Add feature flag to enable annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- config/config-policy-controller.yaml | 1 + pkg/config/store.go | 15 ++++++++++++- pkg/config/store_test.go | 8 +++++++ pkg/config/testdata/annotate-results.yaml | 26 +++++++++++++++++++++++ pkg/webhook/validator.go | 6 ++++++ pkg/webhook/validator_test.go | 26 ++++++++++++++++++++--- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 pkg/config/testdata/annotate-results.yaml diff --git a/config/config-policy-controller.yaml b/config/config-policy-controller.yaml index 7b5a848ef..c4ed22288 100644 --- a/config/config-policy-controller.yaml +++ b/config/config-policy-controller.yaml @@ -25,3 +25,4 @@ data: # # ################################ no-match-policy: warn + annotate-validation-results: false diff --git a/pkg/config/store.go b/pkg/config/store.go index 32f98ce8c..1c2c8ef31 100644 --- a/pkg/config/store.go +++ b/pkg/config/store.go @@ -43,6 +43,8 @@ const ( NoMatchPolicyKey = "no-match-policy" FailOnEmptyAuthorities = "fail-on-empty-authorities" + + AnnotateResultsKey = "annotate-validation-results" ) // PolicyControllerConfig controls the behaviour of policy-controller that needs @@ -56,10 +58,12 @@ type PolicyControllerConfig struct { NoMatchPolicy string `json:"no-match-policy"` // FailOnEmptyAuthorities configures the validating webhook to allow creating CIP without a list authorities FailOnEmptyAuthorities bool `json:"fail-on-empty-authorities"` + // AnnotateResults configures writing the validation results as an annotation in the resource + AnnotateResults bool `json:"annotate-validation-results"` } func NewPolicyControllerConfigFromMap(data map[string]string) (*PolicyControllerConfig, error) { - ret := &PolicyControllerConfig{NoMatchPolicy: "deny", FailOnEmptyAuthorities: true} + ret := &PolicyControllerConfig{NoMatchPolicy: "deny", FailOnEmptyAuthorities: true, AnnotateResults: false} switch data[NoMatchPolicyKey] { case DenyAll: ret.NoMatchPolicy = DenyAll @@ -76,6 +80,14 @@ func NewPolicyControllerConfigFromMap(data map[string]string) (*PolicyController return ret, err } ret.FailOnEmptyAuthorities = true + + if val, ok := data[AnnotateResultsKey]; ok { + var err error + ret.AnnotateResults, err = strconv.ParseBool(val) + return ret, err + } + ret.AnnotateResults = false + return ret, nil } @@ -102,6 +114,7 @@ func FromContextOrDefaults(ctx context.Context) *PolicyControllerConfig { return &PolicyControllerConfig{ NoMatchPolicy: DenyAll, FailOnEmptyAuthorities: true, + AnnotateResults: false, } } diff --git a/pkg/config/store_test.go b/pkg/config/store_test.go index 769de3d70..44ed474c7 100644 --- a/pkg/config/store_test.go +++ b/pkg/config/store_test.go @@ -27,6 +27,7 @@ import ( type testData struct { noMatchPolicy string failOnEmptyAuthorities bool + AnnotateResults bool } var testfiles = map[string]testData{ @@ -35,6 +36,7 @@ var testfiles = map[string]testData{ "warn-all": {noMatchPolicy: WarnAll, failOnEmptyAuthorities: true}, "deny-all-default": {noMatchPolicy: DenyAll, failOnEmptyAuthorities: true}, "allow-empty-authorities": {noMatchPolicy: DenyAll, failOnEmptyAuthorities: false}, + "annotate-results": {noMatchPolicy: AllowAll, failOnEmptyAuthorities: true, AnnotateResults: true}, } func TestStoreLoadWithContext(t *testing.T) { @@ -55,6 +57,9 @@ func TestStoreLoadWithContext(t *testing.T) { if diff := cmp.Diff(want.failOnEmptyAuthorities, expected.FailOnEmptyAuthorities); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } + if diff := cmp.Diff(want.AnnotateResults, expected.AnnotateResults); diff != "" { + t.Error("Unexpected defaults config (-want, +got):", diff) + } if diff := cmp.Diff(expected, config); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } @@ -74,6 +79,9 @@ func TestStoreLoadWithContextOrDefaults(t *testing.T) { if diff := cmp.Diff(DenyAll, expected.NoMatchPolicy); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } + if diff := cmp.Diff(false, expected.AnnotateResults); diff != "" { + t.Error("Unexpected defaults config (-want, +got):", diff) + } if diff := cmp.Diff(expected, config); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } diff --git a/pkg/config/testdata/annotate-results.yaml b/pkg/config/testdata/annotate-results.yaml new file mode 100644 index 000000000..03d46931e --- /dev/null +++ b/pkg/config/testdata/annotate-results.yaml @@ -0,0 +1,26 @@ +# Copyright 2022 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-policy-controller + namespace: cosign-system + labels: + policy.sigstore.dev/release: devel + +data: + _example: | + no-match-policy: allow + annotate-validation-results: true diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index 8a5e36c9c..25a7ad2a3 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -1203,6 +1203,12 @@ func (v *Validator) AnnotateCronJob(ctx context.Context, c *duckv1.CronJob) { } func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVersion string, objectMeta *metav1.ObjectMeta, ps *corev1.PodSpec, opt k8schain.Options) { + pcConfig := policycontrollerconfig.FromContextOrDefaults(ctx) + if !pcConfig.AnnotateResults { + // Annotation is disabled + return + } + kc, err := k8schain.New(ctx, kubeclient.Get(ctx), opt) if err != nil { logging.FromContext(ctx).Warnf("Unable to build k8schain: %v", err) diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go index e8469378e..691824f1d 100644 --- a/pkg/webhook/validator_test.go +++ b/pkg/webhook/validator_test.go @@ -3798,8 +3798,19 @@ func TestAnnotatePod(t *testing.T) { config.ToContext(testContext, cfg) } - testContext = context.WithValue(testContext, kubeclient.Key{}, kc) + // Check disabled annotations + noAnnotationsContext := context.WithValue(testContext, kubeclient.Key{}, kc) got := &metav1.ObjectMeta{} + v.annotatePodSpec(noAnnotationsContext, system.Namespace(), "Pod", "v1", got, test.ps, k8schain.Options{}) + + if got.Annotations != nil { + t.Errorf("annotatePodSpec() = %v, wanted nil", got.Annotations) + } + + testContext = context.WithValue(testContext, kubeclient.Key{}, kc) + testContext = policycontrollerconfig.ToContext(testContext, &policycontrollerconfig.PolicyControllerConfig{AnnotateResults: true}) + + got = &metav1.ObjectMeta{} // Check the core mechanics v.annotatePodSpec(testContext, system.Namespace(), "Pod", "v1", got, test.ps, k8schain.Options{}) want := test.want @@ -4028,14 +4039,23 @@ func TestAnnotateCronJob(t *testing.T) { t.Run(test.name, func(t *testing.T) { cosignVerifySignatures = test.cvs + // Check disabled annotations testContext := context.WithValue(context.Background(), kubeclient.Key{}, kc) + cronJob := test.c.DeepCopy() + v.AnnotateCronJob(testContext, cronJob) + if cronJob.Annotations != nil { + t.Errorf("AnnotateCronJob() = %v, wanted nil", cronJob.Annotations) + } + + // Enable annotations + testContext = policycontrollerconfig.ToContext(testContext, &policycontrollerconfig.PolicyControllerConfig{AnnotateResults: true}) // Check the core mechanics - cronJob := test.c.DeepCopy() + cronJob = test.c.DeepCopy() want := test.want v.AnnotateCronJob(testContext, cronJob) if !annotationsMatch(t, test.name, cronJob.Annotations[ResultsAnnotationKey], want) { - t.Errorf("AnnotateCronJob = %s", cmp.Diff(cronJob.Annotations[ResultsAnnotationKey], want)) + t.Errorf("AnnotateCronJob() = %s", cmp.Diff(cronJob.Annotations[ResultsAnnotationKey], want)) } // Check that we don't block things being deleted. From 68bf7d0c109d86b9fd7689f26f64cb863556d426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 10 May 2023 09:40:15 -0500 Subject: [PATCH 05/11] Rename annotations variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- pkg/config/store.go | 20 ++++++++++---------- pkg/config/store_test.go | 12 ++++++------ pkg/webhook/validator.go | 2 +- pkg/webhook/validator_test.go | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/config/store.go b/pkg/config/store.go index 1c2c8ef31..40d64fab2 100644 --- a/pkg/config/store.go +++ b/pkg/config/store.go @@ -44,7 +44,7 @@ const ( FailOnEmptyAuthorities = "fail-on-empty-authorities" - AnnotateResultsKey = "annotate-validation-results" + AnnotateValidationResultsKey = "annotate-validation-results" ) // PolicyControllerConfig controls the behaviour of policy-controller that needs @@ -58,12 +58,12 @@ type PolicyControllerConfig struct { NoMatchPolicy string `json:"no-match-policy"` // FailOnEmptyAuthorities configures the validating webhook to allow creating CIP without a list authorities FailOnEmptyAuthorities bool `json:"fail-on-empty-authorities"` - // AnnotateResults configures writing the validation results as an annotation in the resource - AnnotateResults bool `json:"annotate-validation-results"` + // AnnotateValidationResults configures writing the validation results as an annotation in the resource + AnnotateValidationResults bool `json:"annotate-validation-results"` } func NewPolicyControllerConfigFromMap(data map[string]string) (*PolicyControllerConfig, error) { - ret := &PolicyControllerConfig{NoMatchPolicy: "deny", FailOnEmptyAuthorities: true, AnnotateResults: false} + ret := &PolicyControllerConfig{NoMatchPolicy: "deny", FailOnEmptyAuthorities: true, AnnotateValidationResults: false} switch data[NoMatchPolicyKey] { case DenyAll: ret.NoMatchPolicy = DenyAll @@ -81,12 +81,12 @@ func NewPolicyControllerConfigFromMap(data map[string]string) (*PolicyController } ret.FailOnEmptyAuthorities = true - if val, ok := data[AnnotateResultsKey]; ok { + if val, ok := data[AnnotateValidationResultsKey]; ok { var err error - ret.AnnotateResults, err = strconv.ParseBool(val) + ret.AnnotateValidationResults, err = strconv.ParseBool(val) return ret, err } - ret.AnnotateResults = false + ret.AnnotateValidationResults = false return ret, nil } @@ -112,9 +112,9 @@ func FromContextOrDefaults(ctx context.Context) *PolicyControllerConfig { return cfg } return &PolicyControllerConfig{ - NoMatchPolicy: DenyAll, - FailOnEmptyAuthorities: true, - AnnotateResults: false, + NoMatchPolicy: DenyAll, + FailOnEmptyAuthorities: true, + AnnotateValidationResults: false, } } diff --git a/pkg/config/store_test.go b/pkg/config/store_test.go index 44ed474c7..a9639cdbf 100644 --- a/pkg/config/store_test.go +++ b/pkg/config/store_test.go @@ -25,9 +25,9 @@ import ( ) type testData struct { - noMatchPolicy string - failOnEmptyAuthorities bool - AnnotateResults bool + noMatchPolicy string + failOnEmptyAuthorities bool + AnnotateValidationResults bool } var testfiles = map[string]testData{ @@ -36,7 +36,7 @@ var testfiles = map[string]testData{ "warn-all": {noMatchPolicy: WarnAll, failOnEmptyAuthorities: true}, "deny-all-default": {noMatchPolicy: DenyAll, failOnEmptyAuthorities: true}, "allow-empty-authorities": {noMatchPolicy: DenyAll, failOnEmptyAuthorities: false}, - "annotate-results": {noMatchPolicy: AllowAll, failOnEmptyAuthorities: true, AnnotateResults: true}, + "annotate-results": {noMatchPolicy: AllowAll, failOnEmptyAuthorities: true, AnnotateValidationResults: true}, } func TestStoreLoadWithContext(t *testing.T) { @@ -57,7 +57,7 @@ func TestStoreLoadWithContext(t *testing.T) { if diff := cmp.Diff(want.failOnEmptyAuthorities, expected.FailOnEmptyAuthorities); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } - if diff := cmp.Diff(want.AnnotateResults, expected.AnnotateResults); diff != "" { + if diff := cmp.Diff(want.AnnotateValidationResults, expected.AnnotateValidationResults); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } if diff := cmp.Diff(expected, config); diff != "" { @@ -79,7 +79,7 @@ func TestStoreLoadWithContextOrDefaults(t *testing.T) { if diff := cmp.Diff(DenyAll, expected.NoMatchPolicy); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } - if diff := cmp.Diff(false, expected.AnnotateResults); diff != "" { + if diff := cmp.Diff(false, expected.AnnotateValidationResults); diff != "" { t.Error("Unexpected defaults config (-want, +got):", diff) } if diff := cmp.Diff(expected, config); diff != "" { diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index 25a7ad2a3..2324fe666 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -1204,7 +1204,7 @@ func (v *Validator) AnnotateCronJob(ctx context.Context, c *duckv1.CronJob) { func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVersion string, objectMeta *metav1.ObjectMeta, ps *corev1.PodSpec, opt k8schain.Options) { pcConfig := policycontrollerconfig.FromContextOrDefaults(ctx) - if !pcConfig.AnnotateResults { + if !pcConfig.AnnotateValidationResults { // Annotation is disabled return } diff --git a/pkg/webhook/validator_test.go b/pkg/webhook/validator_test.go index 691824f1d..cfb186a8e 100644 --- a/pkg/webhook/validator_test.go +++ b/pkg/webhook/validator_test.go @@ -3808,7 +3808,7 @@ func TestAnnotatePod(t *testing.T) { } testContext = context.WithValue(testContext, kubeclient.Key{}, kc) - testContext = policycontrollerconfig.ToContext(testContext, &policycontrollerconfig.PolicyControllerConfig{AnnotateResults: true}) + testContext = policycontrollerconfig.ToContext(testContext, &policycontrollerconfig.PolicyControllerConfig{AnnotateValidationResults: true}) got = &metav1.ObjectMeta{} // Check the core mechanics @@ -4048,7 +4048,7 @@ func TestAnnotateCronJob(t *testing.T) { } // Enable annotations - testContext = policycontrollerconfig.ToContext(testContext, &policycontrollerconfig.PolicyControllerConfig{AnnotateResults: true}) + testContext = policycontrollerconfig.ToContext(testContext, &policycontrollerconfig.PolicyControllerConfig{AnnotateValidationResults: true}) // Check the core mechanics cronJob = test.c.DeepCopy() From ba4e13f2d39075e344b26057073587d0633a10ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 10 May 2023 11:08:53 -0500 Subject: [PATCH 06/11] Add e2e test for annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- .../workflows/kind-cluster-image-policy.yaml | 1 + ...t_cluster_image_policy_with_annotations.sh | 188 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 test/e2e_test_cluster_image_policy_with_annotations.sh diff --git a/.github/workflows/kind-cluster-image-policy.yaml b/.github/workflows/kind-cluster-image-policy.yaml index eef665b19..c98f1ccc2 100644 --- a/.github/workflows/kind-cluster-image-policy.yaml +++ b/.github/workflows/kind-cluster-image-policy.yaml @@ -50,6 +50,7 @@ jobs: - cluster_image_policy_with_include_typemeta - cluster_image_policy_from_configmap_with_fetch_config_file - cluster_image_policy_from_url + - cluster_image_policy_with_annotations env: KO_DOCKER_REPO: "registry.local:5000/policy-controller" diff --git a/test/e2e_test_cluster_image_policy_with_annotations.sh b/test/e2e_test_cluster_image_policy_with_annotations.sh new file mode 100644 index 000000000..c9abd0117 --- /dev/null +++ b/test/e2e_test_cluster_image_policy_with_annotations.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# +# Copyright 2023 The Sigstore Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +set -ex + +if [[ -z "${OIDC_TOKEN}" ]]; then + if [[ -z "${ISSUER_URL}" ]]; then + echo "Must specify either env variable OIDC_TOKEN or ISSUER_URL" + exit 1 + else + export OIDC_TOKEN=`curl -s ${ISSUER_URL}` + fi +fi + +if [[ -z "${KO_DOCKER_REPO}" ]]; then + echo "Must specify env variable KO_DOCKER_REPO" + exit 1 +fi + +if [[ -z "${FULCIO_URL}" ]]; then + echo "Must specify env variable FULCIO_URL" + exit 1 +fi + +if [[ -z "${REKOR_URL}" ]]; then + echo "Must specify env variable REKOR_URL" + exit 1 +fi + +if [[ -z "${TUF_ROOT_FILE}" ]]; then + echo "must specify env variable TUF_ROOT_FILE" + exit 1 +fi + +if [[ -z "${TUF_MIRROR}" ]]; then + echo "must specify env variable TUF_MIRROR" + exit 1 +fi + +if [[ "${NON_REPRODUCIBLE}"=="1" ]]; then + echo "creating non-reproducible build by adding a timestamp" + export TIMESTAMP=`date +%s` +else + export TIMESTAMP="TIMESTAMP" +fi + +# Initialize cosign with our TUF root +cosign initialize --mirror ${TUF_MIRROR} --root ${TUF_ROOT_FILE} + +# To simplify testing annotations, use this function to execute a kubectl to create +# our job and verify that the annotation is as expected. +assert_annotation() { + local KUBECTL_OUT_FILE="/tmp/kubectl.annotation.out" + match="$@" + echo looking for ${match} + kubectl delete job job-that-warns -n ${NS} --ignore-not-found=true + if ! kubectl create -n ${NS} job job-that-warns --image=${demoimage} 2> ${KUBECTL_OUT_FILE} ; then + echo Failed to create Job when expected to annotate! + exit 1 + else + annotation_key="policy.sigstore.dev/policy-controller-results" + echo Successfully created job, checking annotation: "${annotation_key}" + if ! grep -q "${annotation_key}" ${KUBECTL_OUT_FILE} ; then + echo Did not get expected annotation message, wanted "${annotation_key}", got + cat ${KUBECTL_OUT_FILE} + exit 1 + fi + + echo Successfully created job, checking annotation result: "${match}" + if ! grep -q "${match}" ${KUBECTL_OUT_FILE} ; then + echo Did not get expected annotation message, wanted "${match}", got + cat ${KUBECTL_OUT_FILE} + exit 1 + fi + fi +} + +# Publish the first test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world TIMESTAMP") +} +EOF + +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage=`ko publish -B example.com/demo` +echo Created image $demoimage +popd +echo '::endgroup::' + +# Publish the second test image +echo '::group:: publish test image demoimage' +pushd $(mktemp -d) +go mod init example.com/demo +cat < main.go +package main +import "fmt" +func main() { + fmt.Println("hello world 2 TIMESTAMP") +} +EOF +sed -i'' -e "s@TIMESTAMP@${TIMESTAMP}@g" main.go +cat main.go +export demoimage2=`ko publish -B example.com/demo` +popd +echo '::endgroup::' + +echo '::group:: Deploy ClusterImagePolicy with keyless signing' +kubectl apply -f ./test/testdata/policy-controller/e2e/cip-keyless-warn.yaml +echo '::endgroup::' + +echo '::group:: Sign demo image' +cosign sign --rekor-url ${REKOR_URL} --fulcio-url ${FULCIO_URL} --yes --allow-insecure-registry ${demoimage} --identity-token ${OIDC_TOKEN} +echo '::endgroup::' + +echo '::group:: Verify demo image' +cosign verify --rekor-url ${REKOR_URL} --allow-insecure-registry --certificate-identity-regexp='.*' --certificate-oidc-issuer-regexp='.*' ${demoimage} +echo '::endgroup::' + +echo '::group:: Create test namespace and label for verification' +kubectl create namespace demo-keyless-signing +kubectl label namespace demo-keyless-signing policy.sigstore.dev/include=true +export NS=demo-keyless-signing +echo '::endgroup::' + +echo '::group:: Change annotate-validation-results to true' +kubectl patch configmap/config-policy-controller \ + --namespace cosign-system \ + --type merge \ + --patch '{"data":{"annotate-validation-results":"true"}}' +# allow for propagation +sleep 5 +echo '::endgroup::' + +# We signed this above, this should work +echo '::group:: test job success' +expected_annotation='"result":"allow"' +assert_annotation ${expected_annotation} +echo '::endgroup::' + +# We did not sign this, should warn but not fail +echo '::group:: test job admission with warning' +expected_annotation='"result":"warn"' +demoimage=$demoimage2 +assert_annotation ${expected_annotation} +echo '::endgroup::' + +# Change to an image that does not match any policies +demoimage2="quay.io/jetstack/cert-manager-acmesolver:v1.9.1" + +echo '::group:: Change no-match policy to warn' +kubectl patch configmap/config-policy-controller \ + --namespace cosign-system \ + --type merge \ + --patch '{"data":{"no-match-policy":"warn"}}' +# allow for propagation +sleep 5 +echo '::endgroup::' + +echo '::group:: test job admission with deny' +expected_annotation='"result":"deny"' +assert_annotation ${expected_annotation} +echo '::endgroup::' + +echo '::group::' Cleanup +kubectl delete cip --all +kubectl delete ns ${NS} +echo '::endgroup::' From dd37f3ff2560cc6da6d790933679c52002f1dfb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 10 May 2023 11:24:41 -0500 Subject: [PATCH 07/11] Make annotations e2e script executable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- test/e2e_test_cluster_image_policy_with_annotations.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test/e2e_test_cluster_image_policy_with_annotations.sh diff --git a/test/e2e_test_cluster_image_policy_with_annotations.sh b/test/e2e_test_cluster_image_policy_with_annotations.sh old mode 100644 new mode 100755 From 79be61dbf9d1c406a31632f02db10f5371fc8e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 10 May 2023 11:44:36 -0500 Subject: [PATCH 08/11] Add yaml output flag to e2e annotation test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- test/e2e_test_cluster_image_policy_with_annotations.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e_test_cluster_image_policy_with_annotations.sh b/test/e2e_test_cluster_image_policy_with_annotations.sh index c9abd0117..02b60ebf1 100755 --- a/test/e2e_test_cluster_image_policy_with_annotations.sh +++ b/test/e2e_test_cluster_image_policy_with_annotations.sh @@ -68,7 +68,7 @@ assert_annotation() { match="$@" echo looking for ${match} kubectl delete job job-that-warns -n ${NS} --ignore-not-found=true - if ! kubectl create -n ${NS} job job-that-warns --image=${demoimage} 2> ${KUBECTL_OUT_FILE} ; then + if ! kubectl create -n ${NS} job job-that-warns --image=${demoimage} -o yaml 2> ${KUBECTL_OUT_FILE} ; then echo Failed to create Job when expected to annotate! exit 1 else From 87cc2b5e1cdb5a59b5b92b5f5199993f64c2e011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 10 May 2023 12:03:18 -0500 Subject: [PATCH 09/11] Redirect output correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- test/e2e_test_cluster_image_policy_with_annotations.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e_test_cluster_image_policy_with_annotations.sh b/test/e2e_test_cluster_image_policy_with_annotations.sh index 02b60ebf1..a3af1ddc5 100755 --- a/test/e2e_test_cluster_image_policy_with_annotations.sh +++ b/test/e2e_test_cluster_image_policy_with_annotations.sh @@ -68,7 +68,7 @@ assert_annotation() { match="$@" echo looking for ${match} kubectl delete job job-that-warns -n ${NS} --ignore-not-found=true - if ! kubectl create -n ${NS} job job-that-warns --image=${demoimage} -o yaml 2> ${KUBECTL_OUT_FILE} ; then + if ! kubectl create -n ${NS} job job-that-warns --image=${demoimage} -o yaml > ${KUBECTL_OUT_FILE} ; then echo Failed to create Job when expected to annotate! exit 1 else From 293d9c046e6b4b4797da6c779063264a3343b033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 10 May 2023 12:30:59 -0500 Subject: [PATCH 10/11] Fix variable assignment in e2e test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres --- test/e2e_test_cluster_image_policy_with_annotations.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e_test_cluster_image_policy_with_annotations.sh b/test/e2e_test_cluster_image_policy_with_annotations.sh index a3af1ddc5..1c4ac0890 100755 --- a/test/e2e_test_cluster_image_policy_with_annotations.sh +++ b/test/e2e_test_cluster_image_policy_with_annotations.sh @@ -166,7 +166,7 @@ assert_annotation ${expected_annotation} echo '::endgroup::' # Change to an image that does not match any policies -demoimage2="quay.io/jetstack/cert-manager-acmesolver:v1.9.1" +demoimage="quay.io/jetstack/cert-manager-acmesolver:v1.9.1" echo '::group:: Change no-match policy to warn' kubectl patch configmap/config-policy-controller \ From 93ed0c193ea46f8cb772788e4f5c031f9ba1cf10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Torres?= Date: Wed, 17 May 2023 08:06:53 -0500 Subject: [PATCH 11/11] Force GH actions run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrés Torres