Skip to content

Commit c25690c

Browse files
authored
Merge pull request #1449 from fluxcd/rfc-0010-feature-gate
[RFC-0010] Introduce feature gate
2 parents d775ed3 + 0d6ab9f commit c25690c

File tree

8 files changed

+175
-54
lines changed

8 files changed

+175
-54
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/kustomize v1.10.0
7-
github.com/fluxcd/pkg/apis/meta v1.11.0
7+
github.com/fluxcd/pkg/apis/meta v1.12.0
88
k8s.io/apiextensions-apiserver v0.33.0
99
k8s.io/apimachinery v0.33.0
1010
sigs.k8s.io/controller-runtime v0.20.4

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/kustomize v1.10.0 h1:47EeSzkQvlQZdH92vHMe2lK2iR8aOSEJq95avw5idts=
77
github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ=
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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ require (
2323
github.com/fluxcd/pkg/apis/acl v0.7.0
2424
github.com/fluxcd/pkg/apis/event v0.17.0
2525
github.com/fluxcd/pkg/apis/kustomize v1.10.0
26-
github.com/fluxcd/pkg/apis/meta v1.11.0
27-
github.com/fluxcd/pkg/auth v0.12.0
26+
github.com/fluxcd/pkg/apis/meta v1.12.0
27+
github.com/fluxcd/pkg/auth v0.14.0
2828
github.com/fluxcd/pkg/cache v0.9.0
2929
github.com/fluxcd/pkg/http/fetch v0.16.0
3030
github.com/fluxcd/pkg/kustomize v1.17.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,10 @@ github.com/fluxcd/pkg/apis/event v0.17.0 h1:foEINE++pCJlWVhWjYDXfkVmGKu8mQ4BDBlb
186186
github.com/fluxcd/pkg/apis/event v0.17.0/go.mod h1:0fLhLFiHlRTDKPDXdRnv+tS7mCMIQ0fJxnEfmvGM/5A=
187187
github.com/fluxcd/pkg/apis/kustomize v1.10.0 h1:47EeSzkQvlQZdH92vHMe2lK2iR8aOSEJq95avw5idts=
188188
github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ=
189-
github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0=
190-
github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
191-
github.com/fluxcd/pkg/auth v0.12.0 h1:35o0ziYMLZVgJwNvJBGsv/wd903B2fMagcrnm1ptUjc=
192-
github.com/fluxcd/pkg/auth v0.12.0/go.mod h1:gQD2VT5OhIR1E8ZTEsTaho3bDQZidr9P10smH/awcew=
189+
github.com/fluxcd/pkg/apis/meta v1.12.0 h1:XW15TKZieC2b7MN8VS85stqZJOx+/b8jATQ/xTUhVYg=
190+
github.com/fluxcd/pkg/apis/meta v1.12.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
191+
github.com/fluxcd/pkg/auth v0.14.0 h1:AA9nmbFzTN5jcGROJK51LvQoDetMrXJLAo4Sd6WHpFI=
192+
github.com/fluxcd/pkg/auth v0.14.0/go.mod h1:o91WIZZshLooBALXY/MVn0mmdUw3eATrqGXrG1M7nTE=
193193
github.com/fluxcd/pkg/cache v0.9.0 h1:EGKfOLMG3fOwWnH/4Axl5xd425mxoQbZzlZoLfd8PDk=
194194
github.com/fluxcd/pkg/cache v0.9.0/go.mod h1:jMwabjWfsC5lW8hE7NM3wtGNwSJ38Javx6EKbEi7INU=
195195
github.com/fluxcd/pkg/envsubst v1.4.0 h1:pYsb6wrmXOSfHXuXQHaaBBMt3LumhgCb8SMdBNAwV/U=

internal/controller/kustomization_configuration_error_test.go

Lines changed: 136 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ import (
2929

3030
"github.com/fluxcd/pkg/apis/kustomize"
3131
"github.com/fluxcd/pkg/apis/meta"
32+
"github.com/fluxcd/pkg/auth"
3233
"github.com/fluxcd/pkg/runtime/conditions"
3334
"github.com/fluxcd/pkg/testserver"
3435
sourcev1 "github.com/fluxcd/source-controller/api/v1"
3536

3637
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
38+
"github.com/fluxcd/kustomize-controller/internal/decryptor"
3739
)
3840

39-
func TestKustomizationReconciler_InvalidCELExpression(t *testing.T) {
41+
func TestKustomizationReconciler_ConfigurationError(t *testing.T) {
4042
g := NewWithT(t)
4143
id := "invalid-config-" + randStringRunes(5)
4244
revision := "v1.0.0"
@@ -72,53 +74,143 @@ data: {}
7274
err = applyGitRepository(repositoryName, artifact, revision)
7375
g.Expect(err).NotTo(HaveOccurred())
7476

75-
kustomizationKey := types.NamespacedName{
76-
Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)),
77-
Namespace: id,
78-
}
79-
kustomization := &kustomizev1.Kustomization{
80-
ObjectMeta: metav1.ObjectMeta{
81-
Name: kustomizationKey.Name,
82-
Namespace: kustomizationKey.Namespace,
83-
},
84-
Spec: kustomizev1.KustomizationSpec{
85-
Interval: metav1.Duration{Duration: 2 * time.Minute},
86-
Path: "./",
87-
SourceRef: kustomizev1.CrossNamespaceSourceReference{
88-
Name: repositoryName.Name,
89-
Namespace: repositoryName.Namespace,
90-
Kind: sourcev1.GitRepositoryKind,
77+
t.Run("invalid cel expression", func(t *testing.T) {
78+
g := NewWithT(t)
79+
80+
kustomizationKey := types.NamespacedName{
81+
Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)),
82+
Namespace: id,
83+
}
84+
kustomization := &kustomizev1.Kustomization{
85+
ObjectMeta: metav1.ObjectMeta{
86+
Name: kustomizationKey.Name,
87+
Namespace: kustomizationKey.Namespace,
9188
},
92-
TargetNamespace: id,
93-
Prune: true,
94-
Timeout: &metav1.Duration{Duration: time.Second},
95-
Wait: true,
96-
HealthCheckExprs: []kustomize.CustomHealthCheck{{
97-
APIVersion: "v1",
98-
Kind: "ConfigMap",
99-
HealthCheckExpressions: kustomize.HealthCheckExpressions{
100-
InProgress: "foo.",
101-
Current: "true",
89+
Spec: kustomizev1.KustomizationSpec{
90+
TargetNamespace: id,
91+
Interval: metav1.Duration{Duration: 2 * time.Minute},
92+
SourceRef: kustomizev1.CrossNamespaceSourceReference{
93+
Name: repositoryName.Name,
94+
Namespace: repositoryName.Namespace,
95+
Kind: sourcev1.GitRepositoryKind,
10296
},
103-
}},
104-
},
105-
}
97+
Prune: true,
98+
Timeout: &metav1.Duration{Duration: time.Second},
99+
Wait: true,
100+
HealthCheckExprs: []kustomize.CustomHealthCheck{{
101+
APIVersion: "v1",
102+
Kind: "ConfigMap",
103+
HealthCheckExpressions: kustomize.HealthCheckExpressions{
104+
InProgress: "foo.",
105+
Current: "true",
106+
},
107+
}},
108+
},
109+
}
106110

107-
err = k8sClient.Create(context.Background(), kustomization)
108-
g.Expect(err).NotTo(HaveOccurred())
111+
err = k8sClient.Create(context.Background(), kustomization)
112+
g.Expect(err).NotTo(HaveOccurred())
109113

110-
g.Eventually(func() bool {
111-
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
112-
return conditions.IsFalse(resultK, meta.ReadyCondition)
113-
}, timeout, time.Second).Should(BeTrue())
114-
logStatus(t, resultK)
114+
g.Eventually(func() bool {
115+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
116+
return conditions.IsFalse(resultK, meta.ReadyCondition)
117+
}, timeout, time.Second).Should(BeTrue())
115118

116-
g.Expect(resultK.Status.ObservedGeneration).To(Equal(resultK.GetGeneration()))
119+
g.Expect(resultK.Status.ObservedGeneration).To(Equal(resultK.GetGeneration()))
117120

118-
g.Expect(conditions.IsTrue(resultK, meta.StalledCondition)).To(BeTrue())
119-
for _, cond := range []string{meta.ReadyCondition, meta.StalledCondition} {
120-
g.Expect(conditions.GetReason(resultK, cond)).To(Equal(meta.InvalidCELExpressionReason))
121-
g.Expect(conditions.GetMessage(resultK, cond)).To(ContainSubstring(
122-
"failed to create custom status evaluator for healthchecks[0]: failed to parse the expression InProgress: failed to parse the CEL expression 'foo.': ERROR: <input>:1:5: Syntax error: no viable alternative at input '.'"))
123-
}
121+
g.Expect(conditions.IsTrue(resultK, meta.StalledCondition)).To(BeTrue())
122+
for _, cond := range []string{meta.ReadyCondition, meta.StalledCondition} {
123+
g.Expect(conditions.GetReason(resultK, cond)).To(Equal(meta.InvalidCELExpressionReason))
124+
g.Expect(conditions.GetMessage(resultK, cond)).To(ContainSubstring(
125+
"failed to create custom status evaluator for healthchecks[0]: failed to parse the expression InProgress: failed to parse the CEL expression 'foo.': ERROR: <input>:1:5: Syntax error: no viable alternative at input '.'"))
126+
}
127+
})
128+
129+
t.Run("object level workload identity feature gate disabled", func(t *testing.T) {
130+
g := NewWithT(t)
131+
132+
kustomizationKey := types.NamespacedName{
133+
Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)),
134+
Namespace: id,
135+
}
136+
kustomization := &kustomizev1.Kustomization{
137+
ObjectMeta: metav1.ObjectMeta{
138+
Name: kustomizationKey.Name,
139+
Namespace: kustomizationKey.Namespace,
140+
},
141+
Spec: kustomizev1.KustomizationSpec{
142+
TargetNamespace: id,
143+
Interval: metav1.Duration{Duration: 2 * time.Minute},
144+
SourceRef: kustomizev1.CrossNamespaceSourceReference{
145+
Name: repositoryName.Name,
146+
Namespace: repositoryName.Namespace,
147+
Kind: sourcev1.GitRepositoryKind,
148+
},
149+
Prune: true,
150+
Decryption: &kustomizev1.Decryption{
151+
Provider: decryptor.DecryptionProviderSOPS,
152+
ServiceAccountName: "foo",
153+
},
154+
},
155+
}
156+
157+
err = k8sClient.Create(context.Background(), kustomization)
158+
g.Expect(err).NotTo(HaveOccurred())
159+
160+
g.Eventually(func() bool {
161+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
162+
return conditions.IsFalse(resultK, meta.ReadyCondition)
163+
}, timeout, time.Second).Should(BeTrue())
164+
165+
// In this case the controller does not update the observed generation
166+
// because if the feature gate is enabled then the generation of the
167+
// object can be properly observed.
168+
g.Expect(resultK.Status.ObservedGeneration).To(Equal(int64(-1)))
169+
170+
g.Expect(conditions.IsTrue(resultK, meta.StalledCondition)).To(BeTrue())
171+
for _, cond := range []string{meta.ReadyCondition, meta.StalledCondition} {
172+
g.Expect(conditions.GetReason(resultK, cond)).To(Equal(meta.FeatureGateDisabledReason))
173+
g.Expect(conditions.GetMessage(resultK, cond)).To(ContainSubstring(
174+
"to use spec.decryption.serviceAccountName for decryption authentication please enable the ObjectLevelWorkloadIdentity feature gate in the controller"))
175+
}
176+
})
177+
178+
t.Run("object level workload identity feature gate enabled", func(t *testing.T) {
179+
g := NewWithT(t)
180+
181+
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
182+
183+
kustomizationKey := types.NamespacedName{
184+
Name: fmt.Sprintf("invalid-config-%s", randStringRunes(5)),
185+
Namespace: id,
186+
}
187+
kustomization := &kustomizev1.Kustomization{
188+
ObjectMeta: metav1.ObjectMeta{
189+
Name: kustomizationKey.Name,
190+
Namespace: kustomizationKey.Namespace,
191+
},
192+
Spec: kustomizev1.KustomizationSpec{
193+
TargetNamespace: id,
194+
Interval: metav1.Duration{Duration: 2 * time.Minute},
195+
SourceRef: kustomizev1.CrossNamespaceSourceReference{
196+
Name: repositoryName.Name,
197+
Namespace: repositoryName.Namespace,
198+
Kind: sourcev1.GitRepositoryKind,
199+
},
200+
Prune: true,
201+
Decryption: &kustomizev1.Decryption{
202+
Provider: decryptor.DecryptionProviderSOPS,
203+
ServiceAccountName: "foo",
204+
},
205+
},
206+
}
207+
208+
err = k8sClient.Create(context.Background(), kustomization)
209+
g.Expect(err).NotTo(HaveOccurred())
210+
211+
g.Eventually(func() bool {
212+
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(kustomization), resultK)
213+
return conditions.IsTrue(resultK, meta.ReadyCondition)
214+
}, timeout, time.Second).Should(BeTrue())
215+
})
124216
}

internal/controller/kustomization_controller.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import (
5252
apiacl "github.com/fluxcd/pkg/apis/acl"
5353
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
5454
"github.com/fluxcd/pkg/apis/meta"
55+
"github.com/fluxcd/pkg/auth"
5556
"github.com/fluxcd/pkg/cache"
5657
"github.com/fluxcd/pkg/http/fetch"
5758
generator "github.com/fluxcd/pkg/kustomize"
@@ -246,6 +247,18 @@ func (r *KustomizationReconciler) Reconcile(ctx context.Context, req ctrl.Reques
246247
return ctrl.Result{}, nil
247248
}
248249

250+
// Check object-level workload identity feature gate.
251+
if d := obj.Spec.Decryption; d != nil && d.ServiceAccountName != "" && !auth.IsObjectLevelWorkloadIdentityEnabled() {
252+
const gate = auth.FeatureGateObjectLevelWorkloadIdentity
253+
const msgFmt = "to use spec.decryption.serviceAccountName for decryption authentication please enable the %s feature gate in the controller"
254+
msg := fmt.Sprintf(msgFmt, gate)
255+
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FeatureGateDisabledReason, msgFmt, gate)
256+
conditions.MarkStalled(obj, meta.FeatureGateDisabledReason, msgFmt, gate)
257+
log.Error(auth.ErrObjectLevelWorkloadIdentityNotEnabled, msg)
258+
r.event(obj, "", "", eventv1.EventSeverityError, msg, nil)
259+
return ctrl.Result{}, nil
260+
}
261+
249262
// Resolve the source reference and requeue the reconciliation if the source is not found.
250263
artifactSource, err := r.getSource(ctx, obj)
251264
if err != nil {

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
// 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
@@ -68,6 +71,10 @@ var features = map[string]bool{
6871
GroupChangeLog: false,
6972
}
7073

74+
func init() {
75+
auth.SetFeatureGates(features)
76+
}
77+
7178
// FeatureGates contains a list of all supported feature gates and
7279
// their default values.
7380
func FeatureGates() map[string]bool {

main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737

3838
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
3939
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
40+
"github.com/fluxcd/pkg/auth"
4041
pkgcache "github.com/fluxcd/pkg/cache"
4142
"github.com/fluxcd/pkg/runtime/acl"
4243
runtimeClient "github.com/fluxcd/pkg/runtime/client"
@@ -136,6 +137,14 @@ func main() {
136137
os.Exit(1)
137138
}
138139

140+
switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); {
141+
case err != nil:
142+
setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity)
143+
os.Exit(1)
144+
case enabled:
145+
auth.EnableObjectLevelWorkloadIdentity()
146+
}
147+
139148
if err := intervalJitterOptions.SetGlobalJitter(nil); err != nil {
140149
setupLog.Error(err, "unable to set global jitter")
141150
os.Exit(1)

0 commit comments

Comments
 (0)