diff --git a/api/v1beta1/imagerepository_types.go b/api/v1beta1/imagerepository_types.go index 811ef37..afc904e 100644 --- a/api/v1beta1/imagerepository_types.go +++ b/api/v1beta1/imagerepository_types.go @@ -75,7 +75,7 @@ type ImageRepositorySpec struct { } type AccessFrom struct { - NamespaceSelectors []NamespaceSelector `json:"namespaceSelector,omitempty"` + NamespaceSelectors []NamespaceSelector `json:"namespaceSelectors,omitempty"` } type NamespaceSelector struct { diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml index 8d65898..e36a297 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -296,7 +296,7 @@ spec: accessFrom: description: AccessFrom defines an ACL for allowing cross-namespace references to the ImageRepository object based on the caller's namespace labels. properties: - namespaceSelector: + namespaceSelectors: items: properties: matchLabels: diff --git a/config/samples/image_v1beta1_imagerepository.yaml b/config/samples/image_v1beta1_imagerepository.yaml index 4694922..460a0f6 100644 --- a/config/samples/image_v1beta1_imagerepository.yaml +++ b/config/samples/image_v1beta1_imagerepository.yaml @@ -6,3 +6,7 @@ metadata: spec: image: ghcr.io/stefanprodan/podinfo interval: 1m0s + accessFrom: + namespaceSelectors: + - matchLabels: + kubernetes.io/metadata.name: flux-system diff --git a/controllers/imagepolicy_controller.go b/controllers/imagepolicy_controller.go index 84d2411..8c02ecc 100644 --- a/controllers/imagepolicy_controller.go +++ b/controllers/imagepolicy_controller.go @@ -332,29 +332,17 @@ func (r *ImagePolicyReconciler) hasAccessToRepository(ctx context.Context, polic } policyLabels := policyNamespace.GetLabels() - // deny access if the policy namespace has no labels - if len(policyLabels) == 0 { - return false, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to missing lables on namespace '%s'", - repo.Namespace, repo.Name, policy.Namespace) - } - // check if the policy namespace labels match any ACL - var allowed bool for _, selector := range acl.NamespaceSelectors { sel, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: selector.MatchLabels}) if err != nil { return false, err } if sel.Matches(labels.Set(policyLabels)) { - allowed = true - break + return true, nil } } - if !allowed { - return allowed, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to lables mismatch on namespace '%s'", - repo.Namespace, repo.Name, policy.Namespace) - } - - return allowed, nil + return false, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to labels mismatch on namespace '%s'", + repo.Namespace, repo.Name, policy.Namespace) } diff --git a/controllers/policy_test.go b/controllers/policy_test.go index bb97b55..3f8b4b7 100644 --- a/controllers/policy_test.go +++ b/controllers/policy_test.go @@ -458,6 +458,85 @@ var _ = Describe("ImagePolicy controller", func() { }) }) + When("is in different namespace with no empty match labels", func() { + It("grants access", func() { + policyNamespace := &corev1.Namespace{} + policyNamespace.Name = "acl-" + randStringRunes(5) + + Expect(k8sClient.Create(context.Background(), policyNamespace)).To(Succeed()) + defer k8sClient.Delete(context.Background(), policyNamespace) + + versions := []string{"1.0.0", "1.0.1"} + imgRepo := loadImages(registryServer, "acl-image-"+randStringRunes(5), versions) + + repo := imagev1.ImageRepository{ + Spec: imagev1.ImageRepositorySpec{ + Interval: metav1.Duration{Duration: reconciliationInterval}, + Image: imgRepo, + AccessFrom: &imagev1.AccessFrom{ + NamespaceSelectors: []imagev1.NamespaceSelector{ + { + MatchLabels: make(map[string]string), + }, + }, + }, + }, + } + repoObjectName := types.NamespacedName{ + Name: "acl-repo-" + randStringRunes(5), + Namespace: "default", + } + repo.Name = repoObjectName.Name + repo.Namespace = repoObjectName.Namespace + + ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) + defer cancel() + + r := imageRepoReconciler + Expect(r.Create(ctx, &repo)).To(Succeed()) + + Eventually(func() bool { + err := r.Get(ctx, repoObjectName, &repo) + return err == nil && repo.Status.LastScanResult != nil + }, timeout, interval).Should(BeTrue()) + Expect(repo.Status.CanonicalImageName).To(Equal(imgRepo)) + Expect(repo.Status.LastScanResult.TagCount).To(Equal(len(versions))) + + polObjectName := types.NamespacedName{ + Name: "acl-pol-" + randStringRunes(5), + Namespace: policyNamespace.Name, + } + pol := imagev1.ImagePolicy{ + Spec: imagev1.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: repoObjectName.Name, + Namespace: repoObjectName.Namespace, + }, + Policy: imagev1.ImagePolicyChoice{ + SemVer: &imagev1.SemVerPolicy{ + Range: "1.0.x", + }, + }, + }, + } + pol.Namespace = polObjectName.Namespace + pol.Name = polObjectName.Name + + ctx, cancel = context.WithTimeout(context.Background(), contextTimeout) + defer cancel() + + Expect(r.Create(ctx, &pol)).To(Succeed()) + + Eventually(func() bool { + err := r.Get(ctx, polObjectName, &pol) + return err == nil && pol.Status.LatestImage != "" + }, timeout, interval).Should(BeTrue()) + Expect(pol.Status.LatestImage).To(Equal(imgRepo + ":1.0.1")) + + Expect(r.Delete(ctx, &pol)).To(Succeed()) + }) + }) + When("is in different namespace with matching ACL", func() { It("grants access", func() { policyNamespace := &corev1.Namespace{} diff --git a/docs/api/image-reflector.md b/docs/api/image-reflector.md index 6d0a765..2b1639e 100644 --- a/docs/api/image-reflector.md +++ b/docs/api/image-reflector.md @@ -30,7 +30,7 @@ Resource Types: -namespaceSelector
+namespaceSelectors
[]NamespaceSelector diff --git a/docs/spec/v1beta1/imagerepositories.md b/docs/spec/v1beta1/imagerepositories.md index 701c1db..a3fd2e4 100644 --- a/docs/spec/v1beta1/imagerepositories.md +++ b/docs/spec/v1beta1/imagerepositories.md @@ -132,9 +132,9 @@ spec: secretRef: name: regcred accessFrom: - - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: flux-system + namespaceSelectors: + - matchLabels: + kubernetes.io/metadata.name: flux-system ``` **Note** that the `kubernetes.io/metadata.name` is a readonly label added by Kubernetes >= 1.21