Skip to content

Commit 51b3eb4

Browse files
committed
Add ExternalArtifact feature gate
Signed-off-by: Stefan Prodan <[email protected]>
1 parent 049a805 commit 51b3eb4

File tree

9 files changed

+149
-84
lines changed

9 files changed

+149
-84
lines changed

docs/spec/v1/kustomizations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ Artifact containing the YAML manifests. It has two required fields:
117117
+ [GitRepository](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/gitrepositories.md)
118118
+ [OCIRepository](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/ocirepositories.md)
119119
+ [Bucket](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/buckets.md)
120+
+ [ExternalArtifact](https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1/externalartifacts.md) (requires `--feature-gates=ExternalArtifact=true` flag)
120121
- `name`: The Name of the referred Source object.
121122

122123
#### Cross-namespace references

internal/controller/kustomization_acl_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ stringData:
120120

121121
t.Run("fails to reconcile from cross-namespace source", func(t *testing.T) {
122122
reconciler.NoCrossNamespaceRefs = true
123+
defer func() { reconciler.NoCrossNamespaceRefs = false }()
123124

124125
revision = "v2.0.0"
125126
err = applyGitRepository(repositoryName, artifact, revision)
@@ -132,5 +133,6 @@ stringData:
132133
}, timeout, time.Second).Should(BeTrue())
133134

134135
g.Expect(readyCondition.Reason).To(Equal(apiacl.AccessDeniedReason))
136+
g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.StalledCondition)).Should(BeTrue())
135137
})
136138
}

internal/controller/kustomization_controller.go

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import (
7070
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
7171
intcache "github.com/fluxcd/kustomize-controller/internal/cache"
7272
"github.com/fluxcd/kustomize-controller/internal/decryptor"
73+
"github.com/fluxcd/kustomize-controller/internal/features"
7374
"github.com/fluxcd/kustomize-controller/internal/inventory"
7475
intruntime "github.com/fluxcd/kustomize-controller/internal/runtime"
7576
)
@@ -89,26 +90,37 @@ type KustomizationReconciler struct {
8990
kuberecorder.EventRecorder
9091
runtimeCtrl.Metrics
9192

92-
artifactFetchRetries int
93-
requeueDependency time.Duration
93+
// Kubernetes options
9494

95-
Mapper apimeta.RESTMapper
96-
APIReader client.Reader
97-
ClusterReader engine.ClusterReaderFactory
98-
ControllerName string
99-
statusManager string
100-
NoCrossNamespaceRefs bool
101-
NoRemoteBases bool
95+
APIReader client.Reader
96+
ClusterReader engine.ClusterReaderFactory
97+
ConcurrentSSA int
98+
ControllerName string
99+
KubeConfigOpts runtimeClient.KubeConfigOptions
100+
Mapper apimeta.RESTMapper
101+
StatusManager string
102+
103+
// Multi-tenancy and security options
104+
105+
DefaultServiceAccount string
106+
DisallowedFieldManagers []string
107+
NoCrossNamespaceRefs bool
108+
NoRemoteBases bool
109+
SOPSAgeSecret string
110+
TokenCache *cache.TokenCache
111+
112+
// Retry and requeue options
113+
114+
ArtifactFetchRetries int
115+
DependencyRequeueInterval time.Duration
116+
117+
// Feature gates
118+
119+
AdditiveCELDependencyCheck bool
120+
AllowExternalArtifact bool
102121
FailFast bool
103-
DefaultServiceAccount string
104-
SOPSAgeSecret string
105-
KubeConfigOpts runtimeClient.KubeConfigOptions
106-
ConcurrentSSA int
107-
DisallowedFieldManagers []string
108-
StrictSubstitutions bool
109122
GroupChangeLog bool
110-
AdditiveCELDependencyCheck bool
111-
TokenCache *cache.TokenCache
123+
StrictSubstitutions bool
112124
}
113125

114126
func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
@@ -207,9 +219,9 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
207219

208220
if acl.IsAccessDenied(err) {
209221
conditions.MarkFalse(obj, meta.ReadyCondition, apiacl.AccessDeniedReason, "%s", err)
210-
log.Error(err, "Access denied to cross-namespace source")
222+
conditions.MarkStalled(obj, apiacl.AccessDeniedReason, "%s", err)
211223
r.event(obj, "", "", eventv1.EventSeverityError, err.Error(), nil)
212-
return ctrl.Result{RequeueAfter: obj.GetRetryInterval()}, nil
224+
return ctrl.Result{}, reconcile.TerminalError(err)
213225
}
214226

215227
// Retry with backoff on transient errors.
@@ -218,10 +230,10 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
218230

219231
// Requeue the reconciliation if the source artifact is not found.
220232
if artifactSource.GetArtifact() == nil {
221-
msg := fmt.Sprintf("Source artifact not found, retrying in %s", r.requeueDependency.String())
233+
msg := fmt.Sprintf("Source artifact not found, retrying in %s", r.DependencyRequeueInterval.String())
222234
conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", msg)
223235
log.Info(msg)
224-
return ctrl.Result{RequeueAfter: r.requeueDependency}, nil
236+
return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, nil
225237
}
226238
revision := artifactSource.GetArtifact().Revision
227239
originRevision := getOriginRevision(artifactSource)
@@ -241,10 +253,10 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
241253

242254
// Retry on transient errors.
243255
conditions.MarkFalse(obj, meta.ReadyCondition, meta.DependencyNotReadyReason, "%s", err)
244-
msg := fmt.Sprintf("Dependencies do not meet ready condition, retrying in %s", r.requeueDependency.String())
256+
msg := fmt.Sprintf("Dependencies do not meet ready condition, retrying in %s", r.DependencyRequeueInterval.String())
245257
log.Info(msg)
246258
r.event(obj, revision, originRevision, eventv1.EventSeverityInfo, msg, nil)
247-
return ctrl.Result{RequeueAfter: r.requeueDependency}, nil
259+
return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, nil
248260
}
249261
log.Info("All dependencies are ready, proceeding with reconciliation")
250262
}
@@ -254,10 +266,10 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
254266

255267
// Requeue at the specified retry interval if the artifact tarball is not found.
256268
if errors.Is(reconcileErr, fetch.ErrFileNotFound) {
257-
msg := fmt.Sprintf("Source is not ready, artifact not found, retrying in %s", r.requeueDependency.String())
269+
msg := fmt.Sprintf("Source is not ready, artifact not found, retrying in %s", r.DependencyRequeueInterval.String())
258270
conditions.MarkFalse(obj, meta.ReadyCondition, meta.ArtifactFailedReason, "%s", msg)
259271
log.Info(msg)
260-
return ctrl.Result{RequeueAfter: r.requeueDependency}, nil
272+
return ctrl.Result{RequeueAfter: r.DependencyRequeueInterval}, nil
261273
}
262274

263275
// Broadcast the reconciliation failure and requeue at the specified retry interval.
@@ -318,7 +330,7 @@ func (r *KustomizationReconciler) reconcile(
318330
// Download artifact and extract files to the tmp dir.
319331
fetcher := fetch.New(
320332
fetch.WithLogger(ctrl.LoggerFrom(ctx)),
321-
fetch.WithRetries(r.artifactFetchRetries),
333+
fetch.WithRetries(r.ArtifactFetchRetries),
322334
fetch.WithMaxDownloadSize(tar.UnlimitedUntarSize),
323335
fetch.WithUntar(tar.WithMaxUntarSize(tar.UnlimitedUntarSize)),
324336
fetch.WithHostnameOverwrite(os.Getenv("SOURCE_CONTROLLER_LOCALHOST")),
@@ -613,6 +625,8 @@ func (r *KustomizationReconciler) evalReadyExpr(
613625
return celExpr.EvaluateBoolean(ctx, vars)
614626
}
615627

628+
// getSource resolves the source reference and returns the source object containing the artifact.
629+
// It returns an error if the source is not found or if access is denied.
616630
func (r *KustomizationReconciler) getSource(ctx context.Context,
617631
obj *kustomizev1.Kustomization) (sourcev1.Source, error) {
618632
var src sourcev1.Source
@@ -625,12 +639,20 @@ func (r *KustomizationReconciler) getSource(ctx context.Context,
625639
Name: obj.Spec.SourceRef.Name,
626640
}
627641

642+
// Check if cross-namespace references are allowed.
628643
if r.NoCrossNamespaceRefs && sourceNamespace != obj.GetNamespace() {
629644
return src, acl.AccessDeniedError(
630645
fmt.Sprintf("can't access '%s/%s', cross-namespace references have been blocked",
631646
obj.Spec.SourceRef.Kind, namespacedName))
632647
}
633648

649+
// Check if ExternalArtifact kind is allowed.
650+
if obj.Spec.SourceRef.Kind == sourcev1.ExternalArtifactKind && !r.AllowExternalArtifact {
651+
return src, acl.AccessDeniedError(
652+
fmt.Sprintf("can't access '%s/%s', %s feature gate is disabled",
653+
obj.Spec.SourceRef.Kind, namespacedName, features.ExternalArtifact))
654+
}
655+
634656
switch obj.Spec.SourceRef.Kind {
635657
case sourcev1.OCIRepositoryKind:
636658
var repository sourcev1.OCIRepository
@@ -1204,7 +1226,7 @@ func (r *KustomizationReconciler) patch(ctx context.Context,
12041226
patchOpts = append(patchOpts,
12051227
patch.WithOwnedConditions{Conditions: ownedConditions},
12061228
patch.WithForceOverwriteConditions{},
1207-
patch.WithFieldOwner(r.statusManager),
1229+
patch.WithFieldOwner(r.StatusManager),
12081230
)
12091231

12101232
// Patch the object status, conditions and finalizers.

internal/controller/kustomization_externalartifact_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"testing"
2525
"time"
2626

27+
apiacl "github.com/fluxcd/pkg/apis/acl"
2728
"github.com/fluxcd/pkg/apis/meta"
2829
"github.com/fluxcd/pkg/testserver"
2930
sourcev1 "github.com/fluxcd/source-controller/api/v1"
@@ -41,6 +42,7 @@ func TestKustomizationReconciler_ExternalArtifact(t *testing.T) {
4142
g := NewWithT(t)
4243
id := "ea-" + randStringRunes(5)
4344
revision := "v1.0.0"
45+
reconciler.AllowExternalArtifact = true
4446

4547
err := createNamespace(id)
4648
g.Expect(err).NotTo(HaveOccurred(), "failed to create test namespace")
@@ -129,6 +131,7 @@ stringData:
129131

130132
t.Run("watches for external artifact revision change", func(t *testing.T) {
131133
newRev := "v2.0.0"
134+
132135
err = applyExternalArtifact(eaName, artifact, newRev)
133136
g.Expect(err).NotTo(HaveOccurred())
134137

@@ -142,6 +145,27 @@ stringData:
142145
g.Expect(resultK.Status.History[0].LastReconciledStatus).To(Equal(meta.ReconciliationSucceededReason))
143146
g.Expect(resultK.Status.History[0].Metadata).To(ContainElements(newRev))
144147
})
148+
149+
t.Run("fails when external artifact feature gate is disable", func(t *testing.T) {
150+
newRev := "v3.0.0"
151+
reconciler.AllowExternalArtifact = false
152+
153+
err = applyExternalArtifact(eaName, artifact, newRev)
154+
g.Expect(err).NotTo(HaveOccurred())
155+
156+
g.Eventually(func() bool {
157+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
158+
readyCondition = apimeta.FindStatusCondition(resultK.Status.Conditions, meta.ReadyCondition)
159+
return apimeta.IsStatusConditionFalse(resultK.Status.Conditions, meta.ReadyCondition)
160+
}, timeout, time.Second).Should(BeTrue())
161+
162+
g.Expect(readyCondition.Reason).To(Equal(apiacl.AccessDeniedReason))
163+
g.Expect(apimeta.IsStatusConditionTrue(resultK.Status.Conditions, meta.StalledCondition)).Should(BeTrue())
164+
165+
events := getEvents(resultK.GetName(), nil)
166+
g.Expect(events[len(events)-1].Reason).To(BeIdenticalTo(apiacl.AccessDeniedReason))
167+
g.Expect(events[len(events)-1].Message).To(ContainSubstring("feature gate is disabled"))
168+
})
145169
}
146170

147171
func applyExternalArtifact(objKey client.ObjectKey, artifactName string, revision string) error {

internal/controller/kustomization_indexers.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import (
2020
"context"
2121
"fmt"
2222

23-
"github.com/fluxcd/pkg/apis/meta"
24-
"github.com/fluxcd/pkg/runtime/conditions"
25-
"github.com/fluxcd/pkg/runtime/dependency"
2623
ctrl "sigs.k8s.io/controller-runtime"
2724
"sigs.k8s.io/controller-runtime/pkg/client"
2825
"sigs.k8s.io/controller-runtime/pkg/handler"
2926
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3027

28+
"github.com/fluxcd/pkg/apis/meta"
29+
"github.com/fluxcd/pkg/runtime/conditions"
30+
"github.com/fluxcd/pkg/runtime/dependency"
31+
3132
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
3233
)
3334

internal/controller/kustomization_manager.go

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ package controller
1919
import (
2020
"context"
2121
"fmt"
22-
"time"
2322

24-
"github.com/fluxcd/pkg/runtime/predicates"
25-
sourcev1 "github.com/fluxcd/source-controller/api/v1"
2623
corev1 "k8s.io/api/core/v1"
2724
"k8s.io/client-go/util/workqueue"
2825
ctrl "sigs.k8s.io/controller-runtime"
@@ -33,15 +30,17 @@ import (
3330
"sigs.k8s.io/controller-runtime/pkg/predicate"
3431
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3532

33+
"github.com/fluxcd/pkg/runtime/predicates"
34+
sourcev1 "github.com/fluxcd/source-controller/api/v1"
35+
3636
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
3737
)
3838

3939
// KustomizationReconcilerOptions contains options for the KustomizationReconciler.
4040
type KustomizationReconcilerOptions struct {
41-
HTTPRetry int
42-
DependencyRequeueInterval time.Duration
43-
RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
44-
WatchConfigsPredicate predicate.Predicate
41+
RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
42+
WatchConfigsPredicate predicate.Predicate
43+
WatchExternalArtifacts bool
4544
}
4645

4746
// SetupWithManager sets up the controller with the Manager.
@@ -57,12 +56,6 @@ func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl
5756
indexSecret = ".metadata.secret"
5857
)
5958

60-
// Index the Kustomizations by the ExternalArtifact references they (may) point at.
61-
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexExternalArtifact,
62-
r.indexBy(sourcev1.ExternalArtifactKind)); err != nil {
63-
return fmt.Errorf("failed creating index %s: %w", indexExternalArtifact, err)
64-
}
65-
6659
// Index the Kustomizations by the OCIRepository references they (may) point at.
6760
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexOCIRepository,
6861
r.indexBy(sourcev1.OCIRepositoryKind)); err != nil {
@@ -81,6 +74,14 @@ func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl
8174
return fmt.Errorf("failed creating index %s: %w", indexBucket, err)
8275
}
8376

77+
// Index the Kustomizations by the ExternalArtifact references they (may) point at (if enabled).
78+
if opts.WatchExternalArtifacts {
79+
if err := mgr.GetCache().IndexField(ctx, &kustomizev1.Kustomization{}, indexExternalArtifact,
80+
r.indexBy(sourcev1.ExternalArtifactKind)); err != nil {
81+
return fmt.Errorf("failed creating index %s: %w", indexExternalArtifact, err)
82+
}
83+
}
84+
8485
// Index the Kustomization by the ConfigMap references they point to.
8586
if err := mgr.GetFieldIndexer().IndexField(ctx, &kustomizev1.Kustomization{}, indexConfigMap,
8687
func(o client.Object) []string {
@@ -128,19 +129,10 @@ func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl
128129
return fmt.Errorf("failed creating index %s: %w", indexSecret, err)
129130
}
130131

131-
r.requeueDependency = opts.DependencyRequeueInterval
132-
r.statusManager = fmt.Sprintf("gotk-%s", r.ControllerName)
133-
r.artifactFetchRetries = opts.HTTPRetry
134-
135-
return ctrl.NewControllerManagedBy(mgr).
132+
ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
136133
For(&kustomizev1.Kustomization{}, builder.WithPredicates(
137134
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
138135
)).
139-
Watches(
140-
&sourcev1.ExternalArtifact{},
141-
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexExternalArtifact)),
142-
builder.WithPredicates(SourceRevisionChangePredicate{}),
143-
).
144136
Watches(
145137
&sourcev1.OCIRepository{},
146138
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexOCIRepository)),
@@ -165,9 +157,15 @@ func (r *KustomizationReconciler) SetupWithManager(ctx context.Context, mgr ctrl
165157
&corev1.Secret{},
166158
handler.EnqueueRequestsFromMapFunc(r.requestsForConfigDependency(indexSecret)),
167159
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate),
168-
).
169-
WithOptions(controller.Options{
170-
RateLimiter: opts.RateLimiter,
171-
}).
172-
Complete(r)
160+
)
161+
162+
if opts.WatchExternalArtifacts {
163+
ctrlBuilder = ctrlBuilder.Watches(
164+
&sourcev1.ExternalArtifact{},
165+
handler.EnqueueRequestsFromMapFunc(r.requestsForRevisionChangeOf(indexExternalArtifact)),
166+
builder.WithPredicates(SourceRevisionChangePredicate{}),
167+
)
168+
}
169+
170+
return ctrlBuilder.WithOptions(controller.Options{RateLimiter: opts.RateLimiter}).Complete(r)
173171
}

internal/controller/suite_test.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,19 +174,21 @@ func TestMain(m *testing.M) {
174174
kstatusInProgressCheck = kcheck.NewInProgressChecker(testEnv.Client)
175175
kstatusInProgressCheck.DisableFetch = true
176176
reconciler = &KustomizationReconciler{
177-
ControllerName: controllerName,
178-
Client: testEnv,
179-
Mapper: testEnv.GetRESTMapper(),
180-
APIReader: testEnv,
181-
EventRecorder: testEnv.GetEventRecorderFor(controllerName),
182-
Metrics: testMetricsH,
183-
ConcurrentSSA: 4,
184-
DisallowedFieldManagers: []string{overrideManagerName},
185-
SOPSAgeSecret: sopsAgeSecret,
177+
ControllerName: controllerName,
178+
StatusManager: fmt.Sprintf("gotk-%s", controllerName),
179+
Client: testEnv,
180+
Mapper: testEnv.GetRESTMapper(),
181+
APIReader: testEnv,
182+
EventRecorder: testEnv.GetEventRecorderFor(controllerName),
183+
Metrics: testMetricsH,
184+
DependencyRequeueInterval: 2 * time.Second,
185+
ConcurrentSSA: 4,
186+
DisallowedFieldManagers: []string{overrideManagerName},
187+
SOPSAgeSecret: sopsAgeSecret,
186188
}
187189
if err := (reconciler).SetupWithManager(ctx, testEnv, KustomizationReconcilerOptions{
188-
DependencyRequeueInterval: 2 * time.Second,
189-
WatchConfigsPredicate: predicate.Not(predicate.Funcs{}),
190+
WatchConfigsPredicate: predicate.Not(predicate.Funcs{}),
191+
WatchExternalArtifacts: true,
190192
}); err != nil {
191193
panic(fmt.Sprintf("Failed to start KustomizationReconciler: %v", err))
192194
}

0 commit comments

Comments
 (0)