diff --git a/go.mod b/go.mod index 59cdffa94..7e2ba10a4 100644 --- a/go.mod +++ b/go.mod @@ -21,11 +21,11 @@ require ( github.com/fluxcd/pkg/apis/acl v0.12.0 github.com/fluxcd/pkg/apis/event v0.32.0 github.com/fluxcd/pkg/apis/kustomize v1.23.0 - github.com/fluxcd/pkg/apis/meta v1.35.0 - github.com/fluxcd/pkg/auth v0.62.0 - github.com/fluxcd/pkg/cache v0.17.0 + github.com/fluxcd/pkg/apis/meta v1.36.1-0.20261020182949-c5194b17fdad + github.com/fluxcd/pkg/auth v0.63.1-0.20261020182949-c5194b17fdad + github.com/fluxcd/pkg/cache v0.17.1-0.20261020182949-c5194b17fdad github.com/fluxcd/pkg/chartutil v1.32.0 - github.com/fluxcd/pkg/runtime v0.119.0 + github.com/fluxcd/pkg/runtime v0.120.1-0.20261020182949-c5194b17fdad github.com/fluxcd/pkg/ssa v0.80.0 github.com/fluxcd/pkg/testserver v0.15.0 github.com/fluxcd/source-controller/api v1.9.6 @@ -111,6 +111,7 @@ require ( github.com/extism/go-sdk v1.7.1 // indirect github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/fluxcd/pkg/apis/crypto v0.4.1-0.20261020182949-c5194b17fdad // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.3 // indirect github.com/go-errors/errors v1.5.1 // indirect @@ -180,6 +181,7 @@ require ( github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.10.2 // indirect + github.com/spiffe/go-spiffe/v2 v2.9.0 // indirect github.com/stretchr/testify v1.12.1 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect diff --git a/go.sum b/go.sum index 01d2f7126..210fbdcaf 100644 --- a/go.sum +++ b/go.sum @@ -150,20 +150,22 @@ github.com/fluxcd/cli-utils v1.4.0 h1:zlTiCWcVDQvvwZXILYeu5rpbnGt1sS+w32y3+rkOpi github.com/fluxcd/cli-utils v1.4.0/go.mod h1:OupUOj2boEKTbiguMbGMz5Z/d59JUcb1c7pPESe8Pjw= github.com/fluxcd/pkg/apis/acl v0.12.0 h1:UWpZDqKxpKDW38wdXIMHV8Uv3POPu2vUrra0JH1frH0= github.com/fluxcd/pkg/apis/acl v0.12.0/go.mod h1:PJrGIu5Bw/6/siO95uhS+pi5oKSU+gVJqHqsPHb8Kok= +github.com/fluxcd/pkg/apis/crypto v0.4.1-0.20261020182949-c5194b17fdad h1:SAzeZibDaqyyq7CaccwDEySCcsOMGGuHmEN28LcRPvM= +github.com/fluxcd/pkg/apis/crypto v0.4.1-0.20261020182949-c5194b17fdad/go.mod h1:bEwkDgBjLXMVJ2BwmVbbj32PkirmNSicabgl2Jvw7Dk= github.com/fluxcd/pkg/apis/event v0.32.0 h1:3zyjdUspwQN+EQJPlUXwCvCMbeqtB+azlNmeenAcbuE= github.com/fluxcd/pkg/apis/event v0.32.0/go.mod h1:ALma8dTqo+NbI+z4BawSzt0pAPjetCSNNUSSS/9p0VU= github.com/fluxcd/pkg/apis/kustomize v1.23.0 h1:tT/sUURyh6ixQG4+Y/ZoUu+hh5/DQLRraWBgy42ubqQ= github.com/fluxcd/pkg/apis/kustomize v1.23.0/go.mod h1:Jm/LmEsqJIldhhTmhkORAq8JKVZK3ZV2Q88rntjGxEI= -github.com/fluxcd/pkg/apis/meta v1.35.0 h1:/k5d7uEFrxkz/hf6JTrdsKBr3UQWVHW2J4G6VSSae/k= -github.com/fluxcd/pkg/apis/meta v1.35.0/go.mod h1:1MLvK2Xu3UdTKBzg+Q1Oj7H9PHeiKsUB1UW3uwxAwiM= -github.com/fluxcd/pkg/auth v0.62.0 h1:ZhrkfoLTaH5Q095sNGLZ47zLYw/AHw4nVnPAspGwT/c= -github.com/fluxcd/pkg/auth v0.62.0/go.mod h1:46lKYjDvx5Q7fsq09ZC1BxXJcstTOfm2X/NFLLFn7wA= -github.com/fluxcd/pkg/cache v0.17.0 h1:197ST7bSJD2Nw4G01y5cMjtWQpwQsyjpl2ox4MaGeU8= -github.com/fluxcd/pkg/cache v0.17.0/go.mod h1:OW3fncHhPnPZFZav5FdEa//cyMHSndI0JS+IM69LXOU= +github.com/fluxcd/pkg/apis/meta v1.36.1-0.20261020182949-c5194b17fdad h1:p/W9d+gf9FuNNmiwUZJT7ReGzTzwuYLUago5szqkq5I= +github.com/fluxcd/pkg/apis/meta v1.36.1-0.20261020182949-c5194b17fdad/go.mod h1:1MLvK2Xu3UdTKBzg+Q1Oj7H9PHeiKsUB1UW3uwxAwiM= +github.com/fluxcd/pkg/auth v0.63.1-0.20261020182949-c5194b17fdad h1:lUjY+hEXSYG8RdJhuNYl5mjkrnLpwuG/+A+iSybCRlE= +github.com/fluxcd/pkg/auth v0.63.1-0.20261020182949-c5194b17fdad/go.mod h1:gWbB4MzuHWalVyI0o4lE84aINidor9+S63ithckm+94= +github.com/fluxcd/pkg/cache v0.17.1-0.20261020182949-c5194b17fdad h1:Lf7S9vl776ZW7SDxdQtZcEu1vjWroZS86ePd8gost4M= +github.com/fluxcd/pkg/cache v0.17.1-0.20261020182949-c5194b17fdad/go.mod h1:OW3fncHhPnPZFZav5FdEa//cyMHSndI0JS+IM69LXOU= github.com/fluxcd/pkg/chartutil v1.32.0 h1:rqBhgiGsrS4am/jsCfKYlQvPQ+jATug10BtO95UHdq0= github.com/fluxcd/pkg/chartutil v1.32.0/go.mod h1:pBqCvJVDLpKKnU+k9A+TnceZn+DnnivfVGhzNzHJXZU= -github.com/fluxcd/pkg/runtime v0.119.0 h1:cY2KpQ4WwpQKG+pFToslSIXUjKK8ckHQ7cvPUc/QHiY= -github.com/fluxcd/pkg/runtime v0.119.0/go.mod h1:7LJFbZPXwl++ATADx6bt63Jh9CverBoXQxbluxPwspw= +github.com/fluxcd/pkg/runtime v0.120.1-0.20261020182949-c5194b17fdad h1:AcQzxznBRZyRthcVgFjPV49uLfu7eDOoHO1B5WyRsO8= +github.com/fluxcd/pkg/runtime v0.120.1-0.20261020182949-c5194b17fdad/go.mod h1:Cw0CQ/tVKS9lgJoyKVouZr1qVnoJcYnSHw3QDzIOOjA= github.com/fluxcd/pkg/ssa v0.80.0 h1:GgakpCFz4pY+unpOscPYlc5eVLUsB7hw9TmWSNr3yp8= github.com/fluxcd/pkg/ssa v0.80.0/go.mod h1:mUwKP2+KZXm1mal1gSs9SjgcexyqzXbPfHKxdFv5ntM= github.com/fluxcd/pkg/testserver v0.15.0 h1:6Pao9oK1Cu5ssh9c7QoDnnyfPUtxHbw7gJ+5tSOmkWc= @@ -396,6 +398,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.9.0 h1:g5qTg3qIP1HXV+GzuyEfSJFVxUCpEP5Ui1OINVJHPB4= +github.com/spiffe/go-spiffe/v2 v2.9.0/go.mod h1:w2CLWKLMTX/PPYUEUPv3ltH0RXsw5S8suwNF46w9/Aw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index 7306a664c..fc734c4c2 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -91,14 +91,15 @@ type HelmReleaseReconciler struct { // Kubernetes configuration - FieldManager string - DisallowedFieldManagers []string - DefaultServiceAccount string - GetClusterConfig func() (*rest.Config, error) - ClientOpts runtimeClient.Options - KubeConfigOpts runtimeClient.KubeConfigOptions - APIReader client.Reader - TokenCache *cache.TokenCache + FieldManager string + DisallowedFieldManagers []string + DefaultServiceAccount string + DefaultKubeConfigServiceAccount string + GetClusterConfig func() (*rest.Config, error) + ClientOpts runtimeClient.Options + KubeConfigOpts runtimeClient.KubeConfigOptions + APIReader client.Reader + TokenCache *cache.TokenCache // Retry and requeue configuration @@ -108,11 +109,12 @@ type HelmReleaseReconciler struct { // Feature gates - AdditiveCELDependencyCheck bool - AllowExternalArtifact bool - DefaultToRetryOnFailure bool - DirectSourceFetch bool - DisableChartDigestTracking bool + AdditiveCELDependencyCheck bool + AllowExternalArtifact bool + DefaultToRetryOnFailure bool + DirectSourceFetch bool + DisableChartDigestTracking bool + ObjectLevelWorkloadIdentity bool } const terminalErrorMessage = "Reconciliation failed terminally due to configuration error" @@ -722,17 +724,23 @@ func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, obj * var err error switch { case kc.ConfigMapRef != nil && kc.SecretRef == nil: - var opts []auth.Option + var opts []authutils.Option + if r.DefaultKubeConfigServiceAccount != "" { + opts = append(opts, authutils.WithDefaultServiceAccount(r.DefaultKubeConfigServiceAccount)) + } + if r.ObjectLevelWorkloadIdentity { + opts = append(opts, authutils.WithObjectLevelWorkloadIdentity()) + } + authOpts := []auth.Option{ + auth.WithObject(v2.GroupVersion.Group, v2.HelmReleaseKind, obj), + } if r.TokenCache != nil { - involvedObject := cache.InvolvedObject{ - Kind: v2.HelmReleaseKind, - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - Operation: cache.OperationReconcile, - } - opts = append(opts, auth.WithCache(*r.TokenCache, involvedObject)) + authOpts = append(authOpts, + auth.WithCache(*r.TokenCache), + auth.WithCacheOperation(cache.OperationReconcile)) } - restConfig, err = authutils.GetRESTConfig(ctx, *kc, obj.GetNamespace(), r.Client, opts...) + opts = append(opts, authutils.WithAuthOptions(authOpts...)) + restConfig, err = authutils.GetRESTConfig(ctx, *kc, r.Client, opts...) case kc.SecretRef != nil && kc.ConfigMapRef == nil: secretName := types.NamespacedName{ Namespace: obj.GetNamespace(), diff --git a/internal/controller/helmrelease_controller_test.go b/internal/controller/helmrelease_controller_test.go index 040c66135..e7989d2db 100644 --- a/internal/controller/helmrelease_controller_test.go +++ b/internal/controller/helmrelease_controller_test.go @@ -3457,13 +3457,16 @@ users: ) tests := []struct { - name string - env map[string]string - getConfig func() (*rest.Config, error) - spec v2.HelmReleaseSpec - secret *corev1.Secret - want genericclioptions.RESTClientGetter - wantErr string + name string + env map[string]string + getConfig func() (*rest.Config, error) + spec v2.HelmReleaseSpec + secret *corev1.Secret + configMap *corev1.ConfigMap + defaultKubeConfigServiceAccount string + objectLevelWorkloadIdentity bool + want genericclioptions.RESTClientGetter + wantErr string }{ { name: "builds in-cluster RESTClientGetter for HelmRelease", @@ -3529,6 +3532,85 @@ users: }, wantErr: "does not contain a 'invalid-key' key", }, + { + name: "error on missing KubeConfig ConfigMap", + spec: v2.HelmReleaseSpec{ + KubeConfig: &meta.KubeConfigReference{ + ConfigMapRef: &meta.LocalObjectReference{ + Name: "kubeconfig", + }, + }, + }, + wantErr: "failed to get configmap", + }, + { + name: "error when KubeConfig ConfigMap uses a ServiceAccount without the feature gate", + spec: v2.HelmReleaseSpec{ + KubeConfig: &meta.KubeConfigReference{ + ConfigMapRef: &meta.LocalObjectReference{ + Name: "kubeconfig", + }, + }, + }, + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubeconfig", + Namespace: namespace, + }, + Data: map[string]string{ + meta.KubeConfigKeyProvider: "generic", + meta.KubeConfigKeyAddress: "https://1.2.3.4", + meta.KubeConfigKeyServiceAccountName: "auth-sa", + }, + }, + wantErr: "ObjectLevelWorkloadIdentity feature gate is not enabled", + }, + { + name: "error when the default KubeConfig ServiceAccount is missing", + spec: v2.HelmReleaseSpec{ + KubeConfig: &meta.KubeConfigReference{ + ConfigMapRef: &meta.LocalObjectReference{ + Name: "kubeconfig", + }, + }, + }, + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubeconfig", + Namespace: namespace, + }, + Data: map[string]string{ + meta.KubeConfigKeyProvider: "generic", + meta.KubeConfigKeyAddress: "https://1.2.3.4", + }, + }, + defaultKubeConfigServiceAccount: "auth-sa", + objectLevelWorkloadIdentity: true, + wantErr: "failed to get service account 'some-namespace/auth-sa'", + }, + { + name: "error when the KubeConfig ConfigMap ServiceAccount is missing", + spec: v2.HelmReleaseSpec{ + KubeConfig: &meta.KubeConfigReference{ + ConfigMapRef: &meta.LocalObjectReference{ + Name: "kubeconfig", + }, + }, + }, + configMap: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubeconfig", + Namespace: namespace, + }, + Data: map[string]string{ + meta.KubeConfigKeyProvider: "generic", + meta.KubeConfigKeyAddress: "https://1.2.3.4", + meta.KubeConfigKeyServiceAccountName: "auth-sa", + }, + }, + objectLevelWorkloadIdentity: true, + wantErr: "failed to get service account 'some-namespace/auth-sa'", + }, } for _, tt := range tests { @@ -3543,10 +3625,15 @@ users: if tt.secret != nil { c.WithObjects(tt.secret) } + if tt.configMap != nil { + c.WithObjects(tt.configMap) + } r := &HelmReleaseReconciler{ - Client: c.Build(), - GetClusterConfig: tt.getConfig, + Client: c.Build(), + GetClusterConfig: tt.getConfig, + DefaultKubeConfigServiceAccount: tt.defaultKubeConfigServiceAccount, + ObjectLevelWorkloadIdentity: tt.objectLevelWorkloadIdentity, } getter, err := r.buildRESTClientGetter(context.Background(), &v2.HelmRelease{ diff --git a/internal/features/features.go b/internal/features/features.go index e385bc731..9953b58de 100644 --- a/internal/features/features.go +++ b/internal/features/features.go @@ -132,16 +132,19 @@ var features = map[string]bool{ // DefaultToRetryOnFailure // opt-in from v1.5.2 DefaultToRetryOnFailure: false, + // ObjectLevelWorkloadIdentity + // opt-in from Flux v2.6. + auth.FeatureGateObjectLevelWorkloadIdentity: false, } -func init() { - auth.SetFeatureGates(features) +var retiredFeatures = map[string]string{ + // TODO: retire ObjectLevelWorkloadIdentity in Flux 2.11. } // FeatureGates contains a list of all supported feature gates and // their default values. -func FeatureGates() map[string]bool { - return features +func FeatureGates() (map[string]bool, map[string]string) { + return features, retiredFeatures } // Enabled verifies whether the feature is enabled or not. diff --git a/main.go b/main.go index e1510fb8e..9b9ffb23b 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "errors" "fmt" "os" "time" @@ -166,12 +167,10 @@ func main() { os.Exit(1) } - switch enabled, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity); { - case err != nil: + objectLevelWorkloadIdentity, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity) + if err != nil { setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity) os.Exit(1) - case enabled: - auth.EnableObjectLevelWorkloadIdentity() } switch enabled, err := features.Enabled(features.UseHelm3Defaults); { @@ -194,12 +193,9 @@ func main() { os.Exit(1) } - if defaultKubeConfigServiceAccount != "" { - auth.SetDefaultKubeConfigServiceAccount(defaultKubeConfigServiceAccount) - } - - if auth.InconsistentObjectLevelConfiguration() { - setupLog.Error(auth.ErrInconsistentObjectLevelConfiguration, "invalid configuration") + if defaultKubeConfigServiceAccount != "" && !objectLevelWorkloadIdentity { + setupLog.Error(errors.New("cannot set the default kubeconfig service account when the "+ + auth.FeatureGateObjectLevelWorkloadIdentity+" feature gate is not enabled"), "invalid configuration") os.Exit(1) } @@ -380,24 +376,26 @@ func main() { watchConfigs := !disableConfigWatchers if err = (&controller.HelmReleaseReconciler{ - Client: mgr.GetClient(), - APIReader: mgr.GetAPIReader(), - Recorder: eventRecorder, - Metrics: metricsH, - GetClusterConfig: ctrl.GetConfig, - ClientOpts: clientOptions, - KubeConfigOpts: kubeConfigOpts, - FieldManager: controllerName, - DefaultToRetryOnFailure: defaultToRetryOnFailure, - DisableChartDigestTracking: disableChartDigestTracking, - AdditiveCELDependencyCheck: additiveCELDependencyCheck, - DirectSourceFetch: directSourceFetch, - TokenCache: tokenCache, - DependencyRequeueInterval: requeueDependency, - ArtifactFetchRetries: httpRetry, - ArtifactFetchTimeout: httpTimeout, - AllowExternalArtifact: allowExternalArtifact, - DisallowedFieldManagers: disallowedFieldManagers, + Client: mgr.GetClient(), + APIReader: mgr.GetAPIReader(), + Recorder: eventRecorder, + Metrics: metricsH, + GetClusterConfig: ctrl.GetConfig, + ClientOpts: clientOptions, + KubeConfigOpts: kubeConfigOpts, + FieldManager: controllerName, + DefaultKubeConfigServiceAccount: defaultKubeConfigServiceAccount, + ObjectLevelWorkloadIdentity: objectLevelWorkloadIdentity, + DefaultToRetryOnFailure: defaultToRetryOnFailure, + DisableChartDigestTracking: disableChartDigestTracking, + AdditiveCELDependencyCheck: additiveCELDependencyCheck, + DirectSourceFetch: directSourceFetch, + TokenCache: tokenCache, + DependencyRequeueInterval: requeueDependency, + ArtifactFetchRetries: httpRetry, + ArtifactFetchTimeout: httpTimeout, + AllowExternalArtifact: allowExternalArtifact, + DisallowedFieldManagers: disallowedFieldManagers, }).SetupWithManager(ctx, mgr, controller.HelmReleaseReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), WatchConfigs: watchConfigs,