diff --git a/controllers/imageupdateautomation_controller.go b/controllers/imageupdateautomation_controller.go index db7b7fed..44cbb46b 100644 --- a/controllers/imageupdateautomation_controller.go +++ b/controllers/imageupdateautomation_controller.go @@ -29,7 +29,6 @@ import ( "time" "github.com/Masterminds/sprig/v3" - gogit "github.com/go-git/go-git/v5" libgit2 "github.com/libgit2/git2go/v31" @@ -55,7 +54,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" + apiacl "github.com/fluxcd/pkg/apis/acl" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/events" "github.com/fluxcd/pkg/runtime/logger" "github.com/fluxcd/pkg/runtime/metrics" @@ -91,6 +92,7 @@ type ImageUpdateAutomationReconciler struct { EventRecorder kuberecorder.EventRecorder ExternalEventRecorder *events.Recorder MetricsRecorder *metrics.Recorder + NoCrossNamespaceRef bool } type ImageUpdateAutomationReconcilerOptions struct { @@ -179,6 +181,19 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr } debuglog.Info("fetching git repository", "gitrepository", originName) + if r.NoCrossNamespaceRef && gitRepoNamespace != auto.GetNamespace() { + err := acl.AccessDeniedError(fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked", + auto.Spec.SourceRef.Kind, originName)) + log.Error(err, "access denied to cross-namespaced resource") + imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, apiacl.AccessDeniedReason, + err.Error()) + if err := r.patchStatus(ctx, req, auto.Status); err != nil { + return ctrl.Result{Requeue: true}, err + } + r.event(ctx, auto, events.EventSeverityError, err.Error()) + return ctrl.Result{}, nil + } + if err := r.Get(ctx, originName, &origin); err != nil { if client.IgnoreNotFound(err) == nil { imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.GitNotAvailableReason, "referenced git repository is missing") diff --git a/controllers/update_test.go b/controllers/update_test.go index 4ecc4e38..0db8d478 100644 --- a/controllers/update_test.go +++ b/controllers/update_test.go @@ -30,6 +30,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/fluxcd/pkg/apis/acl" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -268,6 +269,7 @@ Images: }) AfterEach(func() { + imageAutoReconciler.NoCrossNamespaceRef = false Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) }) @@ -290,8 +292,9 @@ Images: Context("ref cross-ns GitRepository", func() { var ( - localRepo *git.Repository - commitMessage string + localRepo *git.Repository + commitMessage string + updateBySetters *imagev1.ImageUpdateAutomation ) const ( @@ -410,7 +413,7 @@ Images: Namespace: namespace.Name, Name: "update-test", } - updateBySetters := &imagev1.ImageUpdateAutomation{ + updateBySetters = &imagev1.ImageUpdateAutomation{ ObjectMeta: metav1.ObjectMeta{ Name: updateKey.Name, Namespace: updateKey.Namespace, @@ -465,6 +468,28 @@ Images: Expect(commit.Author.Name).To(Equal(authorName)) Expect(commit.Author.Email).To(Equal(authorEmail)) }) + + It("fails to reconcile if cross-namespace flag is set", func() { + imageAutoReconciler.NoCrossNamespaceRef = true + + // trigger reconcile + var updatePatch imagev1.ImageUpdateAutomation + Expect(k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(updateBySetters), &updatePatch)).To(Succeed()) + updatePatch.Spec.Interval = metav1.Duration{Duration: 5 * time.Minute} + Expect(k8sClient.Patch(context.Background(), &updatePatch, client.Merge)).To(Succeed()) + + resultAuto := &imagev1.ImageUpdateAutomation{} + var readyCondition *metav1.Condition + + Eventually(func() bool { + _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(updateBySetters), resultAuto) + readyCondition = apimeta.FindStatusCondition(resultAuto.Status.Conditions, meta.ReadyCondition) + return apimeta.IsStatusConditionFalse(resultAuto.Status.Conditions, meta.ReadyCondition) + }, timeout, time.Second).Should(BeTrue()) + + Expect(readyCondition).ToNot(BeNil()) + Expect(readyCondition.Reason).To(Equal(acl.AccessDeniedReason)) + }) }) Context("update path", func() { diff --git a/docs/spec/v1beta1/imageupdateautomations.md b/docs/spec/v1beta1/imageupdateautomations.md index 8c685b68..0b381097 100644 --- a/docs/spec/v1beta1/imageupdateautomations.md +++ b/docs/spec/v1beta1/imageupdateautomations.md @@ -32,7 +32,7 @@ type ImageUpdateAutomationSpec struct { // SourceRef refers to the resource giving access details // to a git repository. // +required - SourceRef SourceReference `json:"sourceRef"` + SourceRef CrossNamespaceSourceReference `json:"sourceRef"` // GitSpec contains all the git-specific definitions. This is // technically optional, but in practice mandatory until there are // other kinds of source allowed. @@ -62,25 +62,51 @@ repository to be updated. The `kind` field in the reference currently only suppo `GitRepository`, which is the default. ```go -// SourceReference contains enough information to let you locate the -// typed, referenced source object. -type SourceReference struct { - // API version of the referent +// CrossNamespaceSourceReference contains enough information to let you locate the +// typed Kubernetes resource object at cluster level. +type CrossNamespaceSourceReference struct { + // API version of the referent. // +optional APIVersion string `json:"apiVersion,omitempty"` - // Kind of the referent + // Kind of the referent. // +kubebuilder:validation:Enum=GitRepository // +kubebuilder:default=GitRepository // +required Kind string `json:"kind"` - // Name of the referent + // Name of the referent. // +required Name string `json:"name"` + + // Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference. + // +optional + Namespace string `json:"namespace,omitempty"` } ``` +### Cross-namespace references + +A ImageUpdateAutomation can refer to a GitRepository from a different namespace with +`spec.sourceRef.namespace` e.g.: + +```yaml +apiVersion: image.toolkit.fluxcd.io/v1beta1 +kind: ImageUpdateAutomation +metadata: + name: webapp + namespace: apps +spec: + interval: 5m + sourceRef: + kind: GitRepository # the only valid value, but good practice to be explicit here + name: apps + namespace: flux-system +``` + +On multi-tenant clusters, platform admins can disable cross-namespace references with the +`--no-cross-namespace-refs=true` flag. + To be able to commit changes back, the referenced `GitRepository` object must refer to credentials with write access; e.g., if using a GitHub deploy key, "Allow write access" should be checked when creating it. Only the `url`, `ref`, and `secretRef` fields of the `GitRepository` are used. diff --git a/go.mod b/go.mod index 20486296..eb1af230 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/fluxcd/image-reflector-controller/api v0.15.0 github.com/fluxcd/pkg/apis/meta v0.10.2 github.com/fluxcd/pkg/gittestserver v0.5.0 - github.com/fluxcd/pkg/runtime v0.12.3 + github.com/fluxcd/pkg/runtime v0.12.4 github.com/fluxcd/pkg/ssh v0.2.0 // If you bump this, change SOURCE_VER in the Makefile to match github.com/fluxcd/source-controller v0.21.0 diff --git a/go.sum b/go.sum index 4f0c0780..13283537 100644 --- a/go.sum +++ b/go.sum @@ -366,8 +366,9 @@ github.com/fluxcd/pkg/gitutil v0.1.0 h1:VO3kJY/CKOCO4ysDNqfdpTg04icAKBOSb3lbR5uE github.com/fluxcd/pkg/gitutil v0.1.0/go.mod h1:Ybz50Ck5gkcnvF0TagaMwtlRy3X3wXuiri1HVsK5id4= github.com/fluxcd/pkg/helmtestserver v0.4.0/go.mod h1:JOI9f3oXUFIWmMKWMBan7FjglAU+fRTO/sPPV/Kj3gQ= github.com/fluxcd/pkg/lockedfile v0.1.0/go.mod h1:EJLan8t9MiOcgTs8+puDjbE6I/KAfHbdvIy9VUgIjm8= -github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= +github.com/fluxcd/pkg/runtime v0.12.4 h1:gA27RG/+adN2/7Qe03PB46Iwmye/MusPCpuS4zQ2fW0= +github.com/fluxcd/pkg/runtime v0.12.4/go.mod h1:gspNvhAqodZgSmK1ZhMtvARBf/NGAlxmaZaIOHkJYsc= github.com/fluxcd/pkg/ssh v0.2.0 h1:e9V+HReOL7czm7edVzYS1e+CnFKz1/kHiUNfLRpBdH8= github.com/fluxcd/pkg/ssh v0.2.0/go.mod h1:EpQC7Ztdlbi8S/dlYXqVDZtHtLpN3FNl3N6zWujVzbA= github.com/fluxcd/pkg/testserver v0.1.0/go.mod h1:fvt8BHhXw6c1+CLw1QFZxcQprlcXzsrL4rzXaiGM+Iw= diff --git a/main.go b/main.go index c3ba18cc..882ecfa8 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ import ( ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" + "github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/pkg/runtime/events" "github.com/fluxcd/pkg/runtime/leaderelection" @@ -64,6 +65,7 @@ func main() { eventsAddr string healthAddr string clientOptions client.Options + aclOptions acl.Options logOptions logger.Options leaderElectionOptions leaderelection.Options watchAllNamespaces bool @@ -79,6 +81,7 @@ func main() { clientOptions.BindFlags(flag.CommandLine) logOptions.BindFlags(flag.CommandLine) leaderElectionOptions.BindFlags(flag.CommandLine) + aclOptions.BindFlags(flag.CommandLine) flag.Parse() log := logger.NewLogger(logOptions) @@ -130,6 +133,7 @@ func main() { EventRecorder: mgr.GetEventRecorderFor(controllerName), ExternalEventRecorder: eventRecorder, MetricsRecorder: metricsRecorder, + NoCrossNamespaceRef: aclOptions.NoCrossNamespaceRefs, }).SetupWithManager(mgr, controllers.ImageUpdateAutomationReconcilerOptions{ MaxConcurrentReconciles: concurrent, }); err != nil {