Skip to content

Commit 1d4703f

Browse files
committed
feat: Extract and pass deploymentConfig to bundle renderer
**Summary** Completes Phase 3 of the [Deployment Configuration RFC](https://docs.google.com/document/d/1bDo3W1asZqjJTgZy7BcVOGtKOEukp0vUi5CO1ac3vwc/edit?usp=sharing). Extracts `deploymentConfig` from validated ClusterExtension configuration and passes it to the bundle renderer. Builds on: - (PR2454)[#2454]: Config API and JSON schema validation - (PR2469)[#2469]: Renderer support for applying deployment customizations This completes the RFC - users can now customize operator deployments via ClusterExtension.spec.config.inline: ``` config: configType: Inline inline: watchNamespace: "some-namespace" deploymentConfig: env: - name: TEST_ENV value: test-value nodeSelector: kubernetes.io/os: linux ```
1 parent e41eb44 commit 1d4703f

2 files changed

Lines changed: 179 additions & 0 deletions

File tree

internal/operator-controller/applier/provider.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens
8484
if watchNS := bundleConfig.GetWatchNamespace(); watchNS != nil {
8585
opts = append(opts, render.WithTargetNamespaces(*watchNS))
8686
}
87+
88+
// Extract and convert deploymentConfig if present
89+
if deploymentConfigMap := bundleConfig.GetDeploymentConfig(); deploymentConfigMap != nil {
90+
deploymentConfig, err := convertToDeploymentConfig(deploymentConfigMap)
91+
if err != nil {
92+
return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("invalid deploymentConfig: %w", err))
93+
}
94+
opts = append(opts, render.WithDeploymentConfig(deploymentConfig))
95+
}
8796
}
8897
return r.BundleRenderer.Render(rv1, ext.Spec.Namespace, opts...)
8998
}
@@ -149,6 +158,29 @@ func extensionConfigBytes(ext *ocv1.ClusterExtension) []byte {
149158
return nil
150159
}
151160

161+
// convertToDeploymentConfig converts a map[string]any (from validated bundle config)
162+
// to a *config.DeploymentConfig struct that can be passed to the renderer.
163+
// Returns nil if the map is nil or empty.
164+
func convertToDeploymentConfig(deploymentConfigMap map[string]any) (*config.DeploymentConfig, error) {
165+
if deploymentConfigMap == nil || len(deploymentConfigMap) == 0 {
166+
return nil, nil
167+
}
168+
169+
// Marshal the map to JSON
170+
data, err := json.Marshal(deploymentConfigMap)
171+
if err != nil {
172+
return nil, fmt.Errorf("failed to marshal deploymentConfig: %w", err)
173+
}
174+
175+
// Unmarshal into the DeploymentConfig struct
176+
var deploymentConfig config.DeploymentConfig
177+
if err := json.Unmarshal(data, &deploymentConfig); err != nil {
178+
return nil, fmt.Errorf("failed to unmarshal deploymentConfig: %w", err)
179+
}
180+
181+
return &deploymentConfig, nil
182+
}
183+
152184
func getBundleAnnotations(bundleFS fs.FS) (map[string]string, error) {
153185
// The need to get the underlying bundle in order to extract its annotations
154186
// will go away once we have a bundle interface that can surface the annotations independently of the

internal/operator-controller/applier/provider_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,153 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) {
444444
})
445445
}
446446

447+
func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
448+
t.Run("passes deploymentConfig to renderer when provided in configuration", func(t *testing.T) {
449+
expectedEnvVars := []corev1.EnvVar{
450+
{Name: "TEST_ENV", Value: "test-value"},
451+
}
452+
provider := applier.RegistryV1ManifestProvider{
453+
BundleRenderer: render.BundleRenderer{
454+
ResourceGenerators: []render.ResourceGenerator{
455+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
456+
t.Log("ensure deploymentConfig is passed to renderer")
457+
require.NotNil(t, opts.DeploymentConfig)
458+
require.Equal(t, expectedEnvVars, opts.DeploymentConfig.Env)
459+
return nil, nil
460+
},
461+
},
462+
},
463+
IsSingleOwnNamespaceEnabled: true,
464+
}
465+
466+
bundleFS := bundlefs.Builder().WithPackageName("test").
467+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build()
468+
469+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
470+
Spec: ocv1.ClusterExtensionSpec{
471+
Namespace: "install-namespace",
472+
Config: &ocv1.ClusterExtensionConfig{
473+
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
474+
Inline: &apiextensionsv1.JSON{
475+
Raw: []byte(`{"deploymentConfig": {"env": [{"name": "TEST_ENV", "value": "test-value"}]}}`),
476+
},
477+
},
478+
},
479+
})
480+
require.NoError(t, err)
481+
})
482+
483+
t.Run("does not pass deploymentConfig to renderer when not provided in configuration", func(t *testing.T) {
484+
provider := applier.RegistryV1ManifestProvider{
485+
BundleRenderer: render.BundleRenderer{
486+
ResourceGenerators: []render.ResourceGenerator{
487+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
488+
t.Log("ensure deploymentConfig is nil when not provided")
489+
require.Nil(t, opts.DeploymentConfig)
490+
return nil, nil
491+
},
492+
},
493+
},
494+
IsSingleOwnNamespaceEnabled: true,
495+
}
496+
497+
bundleFS := bundlefs.Builder().WithPackageName("test").
498+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build()
499+
500+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
501+
Spec: ocv1.ClusterExtensionSpec{
502+
Namespace: "install-namespace",
503+
// No config provided
504+
},
505+
})
506+
require.NoError(t, err)
507+
})
508+
509+
t.Run("passes deploymentConfig with multiple fields to renderer", func(t *testing.T) {
510+
expectedNodeSelector := map[string]string{"kubernetes.io/os": "linux"}
511+
expectedTolerations := []corev1.Toleration{
512+
{Key: "key1", Operator: "Equal", Value: "value1", Effect: "NoSchedule"},
513+
}
514+
provider := applier.RegistryV1ManifestProvider{
515+
BundleRenderer: render.BundleRenderer{
516+
ResourceGenerators: []render.ResourceGenerator{
517+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
518+
t.Log("ensure all deploymentConfig fields are passed to renderer")
519+
require.NotNil(t, opts.DeploymentConfig)
520+
require.Equal(t, expectedNodeSelector, opts.DeploymentConfig.NodeSelector)
521+
require.Equal(t, expectedTolerations, opts.DeploymentConfig.Tolerations)
522+
return nil, nil
523+
},
524+
},
525+
},
526+
IsSingleOwnNamespaceEnabled: true,
527+
}
528+
529+
bundleFS := bundlefs.Builder().WithPackageName("test").
530+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces).Build()).Build()
531+
532+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
533+
Spec: ocv1.ClusterExtensionSpec{
534+
Namespace: "install-namespace",
535+
Config: &ocv1.ClusterExtensionConfig{
536+
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
537+
Inline: &apiextensionsv1.JSON{
538+
Raw: []byte(`{
539+
"deploymentConfig": {
540+
"nodeSelector": {"kubernetes.io/os": "linux"},
541+
"tolerations": [{"key": "key1", "operator": "Equal", "value": "value1", "effect": "NoSchedule"}]
542+
}
543+
}`),
544+
},
545+
},
546+
},
547+
})
548+
require.NoError(t, err)
549+
})
550+
551+
t.Run("passes both watchNamespace and deploymentConfig when both provided", func(t *testing.T) {
552+
expectedWatchNamespace := "some-namespace"
553+
expectedEnvVars := []corev1.EnvVar{
554+
{Name: "TEST_ENV", Value: "test-value"},
555+
}
556+
provider := applier.RegistryV1ManifestProvider{
557+
BundleRenderer: render.BundleRenderer{
558+
ResourceGenerators: []render.ResourceGenerator{
559+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
560+
t.Log("ensure both watchNamespace and deploymentConfig are passed to renderer")
561+
require.Equal(t, []string{expectedWatchNamespace}, opts.TargetNamespaces)
562+
require.NotNil(t, opts.DeploymentConfig)
563+
require.Equal(t, expectedEnvVars, opts.DeploymentConfig.Env)
564+
return nil, nil
565+
},
566+
},
567+
},
568+
IsSingleOwnNamespaceEnabled: true,
569+
}
570+
571+
bundleFS := bundlefs.Builder().WithPackageName("test").
572+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build()
573+
574+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
575+
Spec: ocv1.ClusterExtensionSpec{
576+
Namespace: "install-namespace",
577+
Config: &ocv1.ClusterExtensionConfig{
578+
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
579+
Inline: &apiextensionsv1.JSON{
580+
Raw: []byte(`{
581+
"watchNamespace": "some-namespace",
582+
"deploymentConfig": {
583+
"env": [{"name": "TEST_ENV", "value": "test-value"}]
584+
}
585+
}`),
586+
},
587+
},
588+
},
589+
})
590+
require.NoError(t, err)
591+
})
592+
}
593+
447594
func Test_RegistryV1HelmChartProvider_Integration(t *testing.T) {
448595
t.Run("surfaces bundle source errors", func(t *testing.T) {
449596
provider := applier.RegistryV1HelmChartProvider{

0 commit comments

Comments
 (0)