Skip to content

Commit 5f80bd8

Browse files
authored
Merge pull request #762 from fluxcd/rfc-0010-feature-gate
[RFC-0010] Introduce feature gate
2 parents 289f074 + 67c6619 commit 5f80bd8

File tree

12 files changed

+206
-14
lines changed

12 files changed

+206
-14
lines changed

api/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.24.0
44

55
require (
66
github.com/fluxcd/pkg/apis/acl v0.7.0
7-
github.com/fluxcd/pkg/apis/meta v1.11.0
7+
github.com/fluxcd/pkg/apis/meta v1.12.0
88
k8s.io/apimachinery v0.33.0
99
sigs.k8s.io/controller-runtime v0.20.4
1010
)

api/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
55
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
66
github.com/fluxcd/pkg/apis/acl v0.7.0 h1:dMhZJH+g6ZRPjs4zVOAN9vHBd1DcavFgcIFkg5ooOE0=
77
github.com/fluxcd/pkg/apis/acl v0.7.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ=
8-
github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0=
9-
github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
8+
github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg=
9+
github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
1010
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
1111
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
1212
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ require (
1212
github.com/fluxcd/image-reflector-controller/api v0.34.0
1313
github.com/fluxcd/pkg/apis/acl v0.7.0
1414
github.com/fluxcd/pkg/apis/event v0.17.0
15-
github.com/fluxcd/pkg/apis/meta v1.11.0
16-
github.com/fluxcd/pkg/auth v0.12.0
15+
github.com/fluxcd/pkg/apis/meta v1.12.0
16+
github.com/fluxcd/pkg/auth v0.14.0
1717
github.com/fluxcd/pkg/cache v0.9.0
1818
github.com/fluxcd/pkg/runtime v0.59.0
1919
github.com/fluxcd/pkg/version v0.7.0
@@ -28,6 +28,7 @@ require (
2828
k8s.io/client-go v0.33.0
2929
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e
3030
sigs.k8s.io/controller-runtime v0.20.4
31+
sigs.k8s.io/yaml v1.4.0
3132
)
3233

3334
require (
@@ -168,5 +169,4 @@ require (
168169
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
169170
sigs.k8s.io/randfill v1.0.0 // indirect
170171
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
171-
sigs.k8s.io/yaml v1.4.0 // indirect
172172
)

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ github.com/fluxcd/pkg/apis/acl v0.7.0 h1:dMhZJH+g6ZRPjs4zVOAN9vHBd1DcavFgcIFkg5o
161161
github.com/fluxcd/pkg/apis/acl v0.7.0/go.mod h1:uv7pXXR/gydiX4MUwlQa7vS8JONEDztynnjTvY3JxKQ=
162162
github.com/fluxcd/pkg/apis/event v0.17.0 h1:foEINE++pCJlWVhWjYDXfkVmGKu8mQ4BDBlbYi5NU7M=
163163
github.com/fluxcd/pkg/apis/event v0.17.0/go.mod h1:0fLhLFiHlRTDKPDXdRnv+tS7mCMIQ0fJxnEfmvGM/5A=
164-
github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0=
165-
github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
166-
github.com/fluxcd/pkg/auth v0.12.0 h1:35o0ziYMLZVgJwNvJBGsv/wd903B2fMagcrnm1ptUjc=
167-
github.com/fluxcd/pkg/auth v0.12.0/go.mod h1:gQD2VT5OhIR1E8ZTEsTaho3bDQZidr9P10smH/awcew=
164+
github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg=
165+
github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
166+
github.com/fluxcd/pkg/auth v0.14.0 h1:AA9nmbFzTN5jcGROJK51LvQoDetMrXJLAo4Sd6WHpFI=
167+
github.com/fluxcd/pkg/auth v0.14.0/go.mod h1:o91WIZZshLooBALXY/MVn0mmdUw3eATrqGXrG1M7nTE=
168168
github.com/fluxcd/pkg/cache v0.9.0 h1:EGKfOLMG3fOwWnH/4Axl5xd425mxoQbZzlZoLfd8PDk=
169169
github.com/fluxcd/pkg/cache v0.9.0/go.mod h1:jMwabjWfsC5lW8hE7NM3wtGNwSJ38Javx6EKbEi7INU=
170170
github.com/fluxcd/pkg/runtime v0.59.0 h1:3OrFkMJB39NcQ2vhhoxqls59sQVSn8U+thhyLbsQoA4=

internal/controller/imagepolicy_controller.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242

4343
aclapi "github.com/fluxcd/pkg/apis/acl"
4444
"github.com/fluxcd/pkg/apis/meta"
45+
"github.com/fluxcd/pkg/auth"
4546
"github.com/fluxcd/pkg/cache"
4647
"github.com/fluxcd/pkg/runtime/acl"
4748
"github.com/fluxcd/pkg/runtime/conditions"
@@ -321,6 +322,15 @@ func (r *ImagePolicyReconciler) reconcile(ctx context.Context, sp *patch.SerialP
321322
return
322323
}
323324

325+
// Check object-level workload identity feature gate.
326+
if repo.Spec.Provider != "generic" && repo.Spec.ServiceAccountName != "" && !auth.IsObjectLevelWorkloadIdentityEnabled() {
327+
const msgFmt = "to use spec.serviceAccountName in the ImageRepository for provider authentication please enable the %s feature gate in the controller"
328+
conditions.MarkStalled(obj, meta.FeatureGateDisabledReason, msgFmt,
329+
auth.FeatureGateObjectLevelWorkloadIdentity)
330+
result, retErr = ctrl.Result{}, nil
331+
return
332+
}
333+
324334
// Construct a policer from the spec.policy.
325335
// Read the tags from database and use the policy to obtain a result for the
326336
// latest tag.
@@ -391,6 +401,7 @@ func (r *ImagePolicyReconciler) updateImageRefs(ctx context.Context,
391401
if !shouldFetch {
392402
latestRef.Digest = obj.Status.LatestRef.Digest
393403
}
404+
394405
case imagev1.ReflectAlways:
395406
shouldFetch = true
396407
}

internal/controller/imagepolicy_controller_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636

3737
aclapis "github.com/fluxcd/pkg/apis/acl"
3838
"github.com/fluxcd/pkg/apis/meta"
39+
"github.com/fluxcd/pkg/auth"
3940
"github.com/fluxcd/pkg/runtime/acl"
4041
"github.com/fluxcd/pkg/runtime/conditions"
4142
"github.com/fluxcd/pkg/runtime/patch"
@@ -160,6 +161,151 @@ func TestImagePolicyReconciler_invalidImage(t *testing.T) {
160161
}).Should(BeTrue())
161162
}
162163

164+
func TestImagePolicyReconciler_objectLevelWorkloadIdentityFeatureGate(t *testing.T) {
165+
t.Run("disabled", func(t *testing.T) {
166+
g := NewWithT(t)
167+
168+
namespaceName := "imagepolicy-" + randStringRunes(5)
169+
namespace := &corev1.Namespace{
170+
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
171+
}
172+
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
173+
t.Cleanup(func() {
174+
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
175+
})
176+
177+
imageRepo := &imagev1.ImageRepository{
178+
ObjectMeta: metav1.ObjectMeta{
179+
Namespace: namespaceName,
180+
Name: "repo",
181+
},
182+
Spec: imagev1.ImageRepositorySpec{
183+
Image: "ghcr.io/stefanprodan/podinfo",
184+
Provider: "aws",
185+
ServiceAccountName: "foo",
186+
},
187+
}
188+
g.Expect(k8sClient.Create(ctx, imageRepo)).NotTo(HaveOccurred())
189+
t.Cleanup(func() {
190+
g.Expect(k8sClient.Delete(ctx, imageRepo)).NotTo(HaveOccurred())
191+
})
192+
193+
g.Eventually(func() bool {
194+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
195+
return err == nil && conditions.IsStalled(imageRepo) &&
196+
conditions.GetReason(imageRepo, meta.StalledCondition) == meta.FeatureGateDisabledReason &&
197+
conditions.GetMessage(imageRepo, meta.StalledCondition) == "to use spec.serviceAccountName for provider authentication please enable the ObjectLevelWorkloadIdentity feature gate in the controller"
198+
}).Should(BeTrue())
199+
200+
g.Eventually(func() bool {
201+
p := patch.NewSerialPatcher(imageRepo, k8sClient)
202+
imageRepo.Spec.Suspend = true
203+
imageRepo.Status.Conditions = nil
204+
conditions.MarkTrue(imageRepo, meta.ReadyCondition, "success", "image repository is ready")
205+
return p.Patch(ctx, imageRepo) == nil
206+
}).Should(BeTrue())
207+
208+
imagePolicy := &imagev1.ImagePolicy{
209+
ObjectMeta: metav1.ObjectMeta{
210+
Namespace: namespaceName,
211+
Name: "test-imagepolicy",
212+
},
213+
Spec: imagev1.ImagePolicySpec{
214+
ImageRepositoryRef: meta.NamespacedObjectReference{
215+
Name: imageRepo.Name,
216+
},
217+
Policy: imagev1.ImagePolicyChoice{
218+
Alphabetical: &imagev1.AlphabeticalPolicy{},
219+
},
220+
},
221+
}
222+
g.Expect(k8sClient.Create(ctx, imagePolicy)).NotTo(HaveOccurred())
223+
t.Cleanup(func() {
224+
g.Expect(k8sClient.Delete(ctx, imagePolicy)).NotTo(HaveOccurred())
225+
})
226+
227+
g.Eventually(func() bool {
228+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imagePolicy), imagePolicy)
229+
logPolicyStatus(t, imagePolicy)
230+
return err == nil && conditions.IsStalled(imagePolicy) &&
231+
conditions.GetReason(imagePolicy, meta.StalledCondition) == meta.FeatureGateDisabledReason &&
232+
conditions.GetMessage(imagePolicy, meta.StalledCondition) == "to use spec.serviceAccountName in the ImageRepository for provider authentication please enable the ObjectLevelWorkloadIdentity feature gate in the controller"
233+
}).Should(BeTrue())
234+
})
235+
236+
t.Run("enabled", func(t *testing.T) {
237+
g := NewWithT(t)
238+
239+
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
240+
241+
namespaceName := "imagepolicy-" + randStringRunes(5)
242+
namespace := &corev1.Namespace{
243+
ObjectMeta: metav1.ObjectMeta{Name: namespaceName},
244+
}
245+
g.Expect(k8sClient.Create(ctx, namespace)).ToNot(HaveOccurred())
246+
t.Cleanup(func() {
247+
g.Expect(k8sClient.Delete(ctx, namespace)).NotTo(HaveOccurred())
248+
})
249+
250+
imageRepo := &imagev1.ImageRepository{
251+
ObjectMeta: metav1.ObjectMeta{
252+
Namespace: namespaceName,
253+
Name: "repo",
254+
},
255+
Spec: imagev1.ImageRepositorySpec{
256+
Image: "ghcr.io/stefanprodan/podinfo",
257+
Provider: "aws",
258+
ServiceAccountName: "foo",
259+
},
260+
}
261+
g.Expect(k8sClient.Create(ctx, imageRepo)).NotTo(HaveOccurred())
262+
t.Cleanup(func() {
263+
g.Expect(k8sClient.Delete(ctx, imageRepo)).NotTo(HaveOccurred())
264+
})
265+
266+
g.Eventually(func() bool {
267+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imageRepo), imageRepo)
268+
logRepoStatus(t, imageRepo)
269+
return err == nil && !conditions.IsReady(imageRepo) &&
270+
conditions.GetReason(imageRepo, meta.ReadyCondition) == imagev1.AuthenticationFailedReason
271+
}).Should(BeTrue())
272+
273+
g.Eventually(func() bool {
274+
p := patch.NewSerialPatcher(imageRepo, k8sClient)
275+
imageRepo.Spec.Suspend = true
276+
imageRepo.Status.Conditions = nil
277+
conditions.MarkTrue(imageRepo, meta.ReadyCondition, "success", "image repository is ready")
278+
return p.Patch(ctx, imageRepo) == nil
279+
}).Should(BeTrue())
280+
281+
imagePolicy := &imagev1.ImagePolicy{
282+
ObjectMeta: metav1.ObjectMeta{
283+
Namespace: namespaceName,
284+
Name: "test-imagepolicy",
285+
},
286+
Spec: imagev1.ImagePolicySpec{
287+
ImageRepositoryRef: meta.NamespacedObjectReference{
288+
Name: imageRepo.Name,
289+
},
290+
Policy: imagev1.ImagePolicyChoice{
291+
Alphabetical: &imagev1.AlphabeticalPolicy{},
292+
},
293+
},
294+
}
295+
g.Expect(k8sClient.Create(ctx, imagePolicy)).NotTo(HaveOccurred())
296+
t.Cleanup(func() {
297+
g.Expect(k8sClient.Delete(ctx, imagePolicy)).NotTo(HaveOccurred())
298+
})
299+
300+
g.Eventually(func() bool {
301+
err := k8sClient.Get(ctx, client.ObjectKeyFromObject(imagePolicy), imagePolicy)
302+
logPolicyStatus(t, imagePolicy)
303+
return err == nil && !conditions.IsReady(imagePolicy) &&
304+
conditions.GetReason(imagePolicy, meta.ReadyCondition) == imagev1.DependencyNotReadyReason
305+
}).Should(BeTrue())
306+
})
307+
}
308+
163309
func TestImagePolicyReconciler_intervalNotConfigured(t *testing.T) {
164310
g := NewWithT(t)
165311

internal/controller/imagerepository_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,15 @@ func (r *ImageRepositoryReconciler) reconcile(ctx context.Context, sp *patch.Ser
222222
notify(ctx, r.EventRecorder, oldObj, obj, nextScanMsg)
223223
}()
224224

225+
// Check object-level workload identity feature gate.
226+
if obj.Spec.Provider != "generic" && obj.Spec.ServiceAccountName != "" && !auth.IsObjectLevelWorkloadIdentityEnabled() {
227+
const msgFmt = "to use spec.serviceAccountName for provider authentication please enable the %s feature gate in the controller"
228+
conditions.MarkStalled(obj, meta.FeatureGateDisabledReason, msgFmt,
229+
auth.FeatureGateObjectLevelWorkloadIdentity)
230+
result, retErr = ctrl.Result{}, nil
231+
return
232+
}
233+
225234
// Set reconciling condition.
226235
reconcile.ProgressiveStatus(false, obj, meta.ProgressingReason, "reconciliation in progress")
227236

internal/controller/suite_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/client-go/tools/record"
3131
ctrl "sigs.k8s.io/controller-runtime"
3232
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/yaml"
3334

3435
"github.com/fluxcd/pkg/runtime/controller"
3536
"github.com/fluxcd/pkg/runtime/testenv"
@@ -144,3 +145,13 @@ func randStringRunes(n int) string {
144145
}
145146
return string(b)
146147
}
148+
149+
func logRepoStatus(t *testing.T, obj *imagev1.ImageRepository) {
150+
sts, _ := yaml.Marshal(obj.Status)
151+
t.Log(string(sts))
152+
}
153+
154+
func logPolicyStatus(t *testing.T, obj *imagev1.ImagePolicy) {
155+
sts, _ := yaml.Marshal(obj.Status)
156+
t.Log(string(sts))
157+
}

internal/features/features.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ limitations under the License.
1818
// supports, and their default states.
1919
package features
2020

21-
import feathelper "github.com/fluxcd/pkg/runtime/features"
21+
import (
22+
"github.com/fluxcd/pkg/auth"
23+
feathelper "github.com/fluxcd/pkg/runtime/features"
24+
)
2225

2326
const (
2427
// CacheSecretsAndConfigMaps controls whether Secrets and ConfigMaps should
@@ -35,6 +38,10 @@ var features = map[string]bool{
3538
CacheSecretsAndConfigMaps: false,
3639
}
3740

41+
func init() {
42+
auth.SetFeatureGates(features)
43+
}
44+
3845
// FeatureGates contains a list of all supported feature gates and their default
3946
// values.
4047
func FeatureGates() map[string]bool {

main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ func main() {
136136
os.Exit(1)
137137
}
138138

139+
switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); {
140+
case err != nil:
141+
setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity)
142+
os.Exit(1)
143+
case enabled:
144+
auth.EnableObjectLevelWorkloadIdentity()
145+
}
146+
139147
badgerOpts := badger.DefaultOptions(storagePath)
140148
badgerOpts.ValueLogFileSize = storageValueLogFileSize
141149
badgerDB, err := badger.Open(badgerOpts)

0 commit comments

Comments
 (0)