Skip to content

Commit 3fd9c27

Browse files
committed
Add DeploymentConfig feature gate for registry+v1 bundle deployment customization
Wraps the deploymentConfig API feature (spec.config.inline.deploymentConfig) behind a new Alpha feature gate so it can be disabled by default and is only enabled in experimental manifests. When the gate is disabled, deploymentConfig is removed from the bundle's config schema before validation, giving users a clear "unknown field" error rather than silently ignoring the configuration. Assisted-by: Claude Signed-off-by: Todd Short <[email protected]>
1 parent dfd25de commit 3fd9c27

File tree

9 files changed

+142
-7
lines changed

9 files changed

+142
-7
lines changed

cmd/operator-controller/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ func run() error {
479479
CertificateProvider: certProvider,
480480
IsWebhookSupportEnabled: certProvider != nil,
481481
IsSingleOwnNamespaceEnabled: features.OperatorControllerFeatureGate.Enabled(features.SingleOwnNamespaceInstallSupport),
482+
IsDeploymentConfigEnabled: features.OperatorControllerFeatureGate.Enabled(features.DeploymentConfig),
482483
}
483484
var cerCfg reconcilerConfigurator
484485
if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) {

helm/experimental.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ options:
1212
- PreflightPermissions
1313
- HelmChartSupport
1414
- BoxcutterRuntime
15+
- DeploymentConfig
1516
disabled:
1617
- WebhookProviderOpenshiftServiceCA
1718
# List of enabled experimental features for catalogd

internal/operator-controller/applier/provider.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type RegistryV1ManifestProvider struct {
3333
CertificateProvider render.CertificateProvider
3434
IsWebhookSupportEnabled bool
3535
IsSingleOwnNamespaceEnabled bool
36+
IsDeploymentConfigEnabled bool
3637
}
3738

3839
func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) {
@@ -70,7 +71,11 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens
7071
render.WithCertificateProvider(r.CertificateProvider),
7172
}
7273

73-
if r.IsSingleOwnNamespaceEnabled {
74+
// Always validate inline config when present so that disabled features produce
75+
// a clear error rather than being silently ignored. When IsSingleOwnNamespaceEnabled
76+
// is true we also call this with no config to validate required fields (e.g.
77+
// watchNamespace for OwnNamespace-only bundles).
78+
if r.IsSingleOwnNamespaceEnabled || ext.Spec.Config != nil {
7479
configOpts, err := r.extractBundleConfigOptions(&rv1, ext)
7580
if err != nil {
7681
return nil, err
@@ -88,6 +93,14 @@ func (r *RegistryV1ManifestProvider) extractBundleConfigOptions(rv1 *bundle.Regi
8893
return nil, fmt.Errorf("error getting configuration schema: %w", err)
8994
}
9095

96+
// When the DeploymentConfig feature gate is disabled, remove deploymentConfig from the
97+
// schema so that users get a clear "unknown field" error if they attempt to use it.
98+
if !r.IsDeploymentConfigEnabled {
99+
if props, ok := schema["properties"].(map[string]any); ok {
100+
delete(props, "deploymentConfig")
101+
}
102+
}
103+
91104
bundleConfigBytes := extensionConfigBytes(ext)
92105
bundleConfig, err := config.UnmarshalConfig(bundleConfigBytes, schema, ext.Spec.Namespace)
93106
if err != nil {
@@ -99,13 +112,15 @@ func (r *RegistryV1ManifestProvider) extractBundleConfigOptions(rv1 *bundle.Regi
99112
opts = append(opts, render.WithTargetNamespaces(*watchNS))
100113
}
101114

102-
// Extract and convert deploymentConfig if present
103-
if deploymentConfigMap := bundleConfig.GetDeploymentConfig(); deploymentConfigMap != nil {
104-
deploymentConfig, err := convertToDeploymentConfig(deploymentConfigMap)
105-
if err != nil {
106-
return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("invalid deploymentConfig: %w", err))
115+
// Extract and convert deploymentConfig if present and the feature gate is enabled.
116+
if r.IsDeploymentConfigEnabled {
117+
if deploymentConfigMap := bundleConfig.GetDeploymentConfig(); deploymentConfigMap != nil {
118+
deploymentConfig, err := convertToDeploymentConfig(deploymentConfigMap)
119+
if err != nil {
120+
return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("invalid deploymentConfig: %w", err))
121+
}
122+
opts = append(opts, render.WithDeploymentConfig(deploymentConfig))
107123
}
108-
opts = append(opts, render.WithDeploymentConfig(deploymentConfig))
109124
}
110125

111126
return opts, nil

internal/operator-controller/applier/provider_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
461461
},
462462
},
463463
IsSingleOwnNamespaceEnabled: true,
464+
IsDeploymentConfigEnabled: true,
464465
}
465466

466467
bundleFS := bundlefs.Builder().WithPackageName("test").
@@ -492,6 +493,7 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
492493
},
493494
},
494495
IsSingleOwnNamespaceEnabled: true,
496+
IsDeploymentConfigEnabled: true,
495497
}
496498

497499
bundleFS := bundlefs.Builder().WithPackageName("test").
@@ -524,6 +526,7 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
524526
},
525527
},
526528
IsSingleOwnNamespaceEnabled: true,
529+
IsDeploymentConfigEnabled: true,
527530
}
528531

529532
bundleFS := bundlefs.Builder().WithPackageName("test").
@@ -566,6 +569,7 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
566569
},
567570
},
568571
IsSingleOwnNamespaceEnabled: true,
572+
IsDeploymentConfigEnabled: true,
569573
}
570574

571575
bundleFS := bundlefs.Builder().WithPackageName("test").
@@ -602,6 +606,7 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
602606
},
603607
},
604608
IsSingleOwnNamespaceEnabled: true,
609+
IsDeploymentConfigEnabled: true,
605610
}
606611

607612
bundleFS := bundlefs.Builder().WithPackageName("test").
@@ -631,6 +636,7 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
631636
},
632637
},
633638
IsSingleOwnNamespaceEnabled: true,
639+
IsDeploymentConfigEnabled: true,
634640
}
635641

636642
bundleFS := bundlefs.Builder().WithPackageName("test").
@@ -654,6 +660,70 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
654660
require.Contains(t, err.Error(), "deploymentConfig.env")
655661
require.ErrorIs(t, err, reconcile.TerminalError(nil), "config validation errors should be terminal")
656662
})
663+
664+
t.Run("returns terminal error when deploymentConfig is used but feature gate is disabled", func(t *testing.T) {
665+
provider := applier.RegistryV1ManifestProvider{
666+
BundleRenderer: render.BundleRenderer{
667+
ResourceGenerators: []render.ResourceGenerator{
668+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
669+
return nil, nil
670+
},
671+
},
672+
},
673+
IsSingleOwnNamespaceEnabled: true,
674+
IsDeploymentConfigEnabled: false,
675+
}
676+
677+
bundleFS := bundlefs.Builder().WithPackageName("test").
678+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build()
679+
680+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
681+
Spec: ocv1.ClusterExtensionSpec{
682+
Namespace: "install-namespace",
683+
Config: &ocv1.ClusterExtensionConfig{
684+
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
685+
Inline: &apiextensionsv1.JSON{
686+
Raw: []byte(`{"deploymentConfig": {"env": [{"name": "TEST_ENV", "value": "test-value"}]}}`),
687+
},
688+
},
689+
},
690+
})
691+
require.Error(t, err)
692+
require.Contains(t, err.Error(), "unknown field \"deploymentConfig\"")
693+
require.ErrorIs(t, err, reconcile.TerminalError(nil), "feature gate disabled error should be terminal")
694+
})
695+
696+
t.Run("returns terminal error when deploymentConfig is used with SingleOwnNamespace disabled and DeploymentConfig gate disabled", func(t *testing.T) {
697+
provider := applier.RegistryV1ManifestProvider{
698+
BundleRenderer: render.BundleRenderer{
699+
ResourceGenerators: []render.ResourceGenerator{
700+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
701+
return nil, nil
702+
},
703+
},
704+
},
705+
IsSingleOwnNamespaceEnabled: false,
706+
IsDeploymentConfigEnabled: false,
707+
}
708+
709+
bundleFS := bundlefs.Builder().WithPackageName("test").
710+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build()
711+
712+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
713+
Spec: ocv1.ClusterExtensionSpec{
714+
Namespace: "install-namespace",
715+
Config: &ocv1.ClusterExtensionConfig{
716+
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
717+
Inline: &apiextensionsv1.JSON{
718+
Raw: []byte(`{"deploymentConfig": {"env": [{"name": "TEST_ENV", "value": "test-value"}]}}`),
719+
},
720+
},
721+
},
722+
})
723+
require.Error(t, err)
724+
require.Contains(t, err.Error(), "unknown field \"deploymentConfig\"")
725+
require.ErrorIs(t, err, reconcile.TerminalError(nil), "config should not be silently ignored when both feature gates are disabled")
726+
})
657727
}
658728

659729
func Test_RegistryV1HelmChartProvider_Integration(t *testing.T) {

internal/operator-controller/features/features.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA"
1919
HelmChartSupport featuregate.Feature = "HelmChartSupport"
2020
BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime"
21+
DeploymentConfig featuregate.Feature = "DeploymentConfig"
2122
)
2223

2324
var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
@@ -80,6 +81,14 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
8081
PreRelease: featuregate.Alpha,
8182
LockToDefault: false,
8283
},
84+
85+
// DeploymentConfig enables support for customizing operator deployments
86+
// via spec.config.inline.deploymentConfig in ClusterExtension resources.
87+
DeploymentConfig: {
88+
Default: false,
89+
PreRelease: featuregate.Alpha,
90+
LockToDefault: false,
91+
},
8392
}
8493

8594
var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()

manifests/experimental-e2e.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2454,6 +2454,7 @@ spec:
24542454
- --feature-gates=PreflightPermissions=true
24552455
- --feature-gates=HelmChartSupport=true
24562456
- --feature-gates=BoxcutterRuntime=true
2457+
- --feature-gates=DeploymentConfig=true
24572458
- --feature-gates=WebhookProviderOpenshiftServiceCA=false
24582459
- --tls-cert=/var/certs/tls.crt
24592460
- --tls-key=/var/certs/tls.key

manifests/experimental.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,6 +2360,7 @@ spec:
23602360
- --feature-gates=PreflightPermissions=true
23612361
- --feature-gates=HelmChartSupport=true
23622362
- --feature-gates=BoxcutterRuntime=true
2363+
- --feature-gates=DeploymentConfig=true
23632364
- --feature-gates=WebhookProviderOpenshiftServiceCA=false
23642365
- --tls-cert=/var/certs/tls.crt
23652366
- --tls-key=/var/certs/tls.key

test/e2e/features/install.feature

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,39 @@ Feature: Install ClusterExtension
500500
And ClusterExtension is available
501501
And ClusterExtensionRevision "${NAME}-1" has label "olm.operatorframework.io/owner-kind" with value "ClusterExtension"
502502
And ClusterExtensionRevision "${NAME}-1" has label "olm.operatorframework.io/owner-name" with value "${NAME}"
503+
504+
@DeploymentConfig
505+
Scenario: deploymentConfig nodeSelector is applied to the operator deployment
506+
When ClusterExtension is applied
507+
"""
508+
apiVersion: olm.operatorframework.io/v1
509+
kind: ClusterExtension
510+
metadata:
511+
name: ${NAME}
512+
spec:
513+
namespace: ${TEST_NAMESPACE}
514+
serviceAccount:
515+
name: olm-sa
516+
config:
517+
configType: Inline
518+
inline:
519+
deploymentConfig:
520+
nodeSelector:
521+
kubernetes.io/os: linux
522+
source:
523+
sourceType: Catalog
524+
catalog:
525+
packageName: test
526+
selector:
527+
matchLabels:
528+
"olm.operatorframework.io/metadata.name": test-catalog
529+
"""
530+
Then ClusterExtension is rolled out
531+
And resource "deployment/test-operator" matches
532+
"""
533+
spec:
534+
template:
535+
spec:
536+
nodeSelector:
537+
kubernetes.io/os: linux
538+
"""

test/e2e/steps/hooks.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ var (
7171
features.WebhookProviderOpenshiftServiceCA: false,
7272
features.HelmChartSupport: false,
7373
features.BoxcutterRuntime: false,
74+
features.DeploymentConfig: false,
7475
}
7576
logger logr.Logger
7677
)

0 commit comments

Comments
 (0)