Skip to content

Commit 09d32fb

Browse files
joelanfordclaude
andauthored
🌱 Return *DeploymentConfig directly from GetDeploymentConfig() (#2598)
Change GetDeploymentConfig() to return (*DeploymentConfig, error) instead of map[string]any, eliminating the intermediate convertToDeploymentConfig() function in provider.go. The caller was immediately converting the map to a DeploymentConfig anyway, so this simplifies the API and removes unnecessary indirection. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 253feae commit 09d32fb

File tree

4 files changed

+51
-86
lines changed

4 files changed

+51
-86
lines changed

internal/operator-controller/applier/provider.go

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,13 @@ func (r *RegistryV1ManifestProvider) extractBundleConfigOptions(rv1 *bundle.Regi
112112
opts = append(opts, render.WithTargetNamespaces(*watchNS))
113113
}
114114

115-
// Extract and convert deploymentConfig if present and the feature gate is enabled.
115+
// Extract deploymentConfig if present and the feature gate is enabled.
116116
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-
}
117+
deploymentConfig, err := bundleConfig.GetDeploymentConfig()
118+
if err != nil {
119+
return nil, errorutil.NewTerminalError(ocv1.ReasonInvalidConfiguration, fmt.Errorf("invalid deploymentConfig: %w", err))
120+
}
121+
if deploymentConfig != nil {
122122
opts = append(opts, render.WithDeploymentConfig(deploymentConfig))
123123
}
124124
}
@@ -187,29 +187,6 @@ func extensionConfigBytes(ext *ocv1.ClusterExtension) []byte {
187187
return nil
188188
}
189189

190-
// convertToDeploymentConfig converts a map[string]any (from validated bundle config)
191-
// to a *config.DeploymentConfig struct that can be passed to the renderer.
192-
// Returns nil if the map is empty.
193-
func convertToDeploymentConfig(deploymentConfigMap map[string]any) (*config.DeploymentConfig, error) {
194-
if len(deploymentConfigMap) == 0 {
195-
return nil, nil
196-
}
197-
198-
// Marshal the map to JSON
199-
data, err := json.Marshal(deploymentConfigMap)
200-
if err != nil {
201-
return nil, fmt.Errorf("failed to marshal deploymentConfig: %w", err)
202-
}
203-
204-
// Unmarshal into the DeploymentConfig struct
205-
var deploymentConfig config.DeploymentConfig
206-
if err := json.Unmarshal(data, &deploymentConfig); err != nil {
207-
return nil, fmt.Errorf("failed to unmarshal deploymentConfig: %w", err)
208-
}
209-
210-
return &deploymentConfig, nil
211-
}
212-
213190
func getBundleAnnotations(bundleFS fs.FS) (map[string]string, error) {
214191
// The need to get the underlying bundle in order to extract its annotations
215192
// 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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
ocv1 "github.com/operator-framework/operator-controller/api/v1"
1919
"github.com/operator-framework/operator-controller/internal/operator-controller/applier"
20+
"github.com/operator-framework/operator-controller/internal/operator-controller/config"
2021
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
2122
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
2223
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1"
@@ -599,8 +600,8 @@ func Test_RegistryV1ManifestProvider_DeploymentConfig(t *testing.T) {
599600
BundleRenderer: render.BundleRenderer{
600601
ResourceGenerators: []render.ResourceGenerator{
601602
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
602-
t.Log("ensure deploymentConfig is nil for empty config object")
603-
require.Nil(t, opts.DeploymentConfig)
603+
t.Log("ensure deploymentConfig is empty for empty config object")
604+
require.Equal(t, &config.DeploymentConfig{}, opts.DeploymentConfig)
604605
return nil, nil
605606
},
606607
},

internal/operator-controller/config/config.go

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -106,44 +106,29 @@ func (c *Config) GetWatchNamespace() *string {
106106
}
107107

108108
// GetDeploymentConfig returns the deploymentConfig value if present in the configuration.
109-
// Returns nil if deploymentConfig is not set or is explicitly set to null.
110-
// The returned value is a generic map[string]any that can be marshaled to JSON
111-
// for validation or conversion to specific types (like v1alpha1.SubscriptionConfig).
112-
//
113-
// Returns a defensive deep copy so callers can't mutate the internal Config state.
114-
func (c *Config) GetDeploymentConfig() map[string]any {
109+
// Returns (nil, nil) if deploymentConfig is not set or is explicitly set to null.
110+
// Returns a non-nil error if the value cannot be marshaled or unmarshaled into a DeploymentConfig.
111+
func (c *Config) GetDeploymentConfig() (*DeploymentConfig, error) {
115112
if c == nil || *c == nil {
116-
return nil
113+
return nil, nil
117114
}
118115
val, exists := (*c)["deploymentConfig"]
119116
if !exists {
120-
return nil
117+
return nil, nil
121118
}
122119
// User set deploymentConfig: null - treat as "not configured"
123120
if val == nil {
124-
return nil
125-
}
126-
// Schema validation ensures this is an object (map)
127-
dcMap, ok := val.(map[string]any)
128-
if !ok {
129-
return nil
121+
return nil, nil
130122
}
131-
132-
// Return a defensive deep copy so callers can't mutate the internal Config state.
133-
// We use JSON marshal/unmarshal because the data is already JSON-compatible and
134-
// this handles nested structures correctly.
135-
data, err := json.Marshal(dcMap)
123+
data, err := json.Marshal(val)
136124
if err != nil {
137-
// This should never happen since the map came from validated JSON/YAML,
138-
// but return nil as a safe fallback
139-
return nil
125+
return nil, fmt.Errorf("failed to marshal deploymentConfig: %w", err)
140126
}
141-
var copied map[string]any
142-
if err := json.Unmarshal(data, &copied); err != nil {
143-
// This should never happen for valid JSON
144-
return nil
127+
var dc DeploymentConfig
128+
if err := json.Unmarshal(data, &dc); err != nil {
129+
return nil, fmt.Errorf("failed to unmarshal deploymentConfig: %w", err)
145130
}
146-
return copied
131+
return &dc, nil
147132
}
148133

149134
// UnmarshalConfig takes user configuration, validates it, and creates a Config object.

internal/operator-controller/config/config_test.go

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/require"
8+
corev1 "k8s.io/api/core/v1"
9+
"k8s.io/apimachinery/pkg/api/resource"
810
"k8s.io/utils/ptr"
911

1012
"github.com/operator-framework/api/pkg/operators/v1alpha1"
@@ -596,7 +598,7 @@ func Test_GetDeploymentConfig(t *testing.T) {
596598
tests := []struct {
597599
name string
598600
rawConfig []byte
599-
expectedDeploymentConfig map[string]any
601+
expectedDeploymentConfig *config.DeploymentConfig
600602
expectedDeploymentConfigNil bool
601603
}{
602604
{
@@ -628,13 +630,13 @@ func Test_GetDeploymentConfig(t *testing.T) {
628630
}
629631
}
630632
}`),
631-
expectedDeploymentConfig: map[string]any{
632-
"nodeSelector": map[string]any{
633+
expectedDeploymentConfig: &config.DeploymentConfig{
634+
NodeSelector: map[string]string{
633635
"kubernetes.io/os": "linux",
634636
},
635-
"resources": map[string]any{
636-
"requests": map[string]any{
637-
"memory": "128Mi",
637+
Resources: &corev1.ResourceRequirements{
638+
Requests: corev1.ResourceList{
639+
corev1.ResourceMemory: resource.MustParse("128Mi"),
638640
},
639641
},
640642
},
@@ -650,7 +652,8 @@ func Test_GetDeploymentConfig(t *testing.T) {
650652
cfg, err := config.UnmarshalConfig(tt.rawConfig, schema, "")
651653
require.NoError(t, err)
652654

653-
result := cfg.GetDeploymentConfig()
655+
result, err := cfg.GetDeploymentConfig()
656+
require.NoError(t, err)
654657
if tt.expectedDeploymentConfigNil {
655658
require.Nil(t, result)
656659
} else {
@@ -663,12 +666,13 @@ func Test_GetDeploymentConfig(t *testing.T) {
663666
// Test nil config separately
664667
t.Run("nil config returns nil", func(t *testing.T) {
665668
var cfg *config.Config
666-
result := cfg.GetDeploymentConfig()
669+
result, err := cfg.GetDeploymentConfig()
670+
require.NoError(t, err)
667671
require.Nil(t, result)
668672
})
669673

670-
// Test that returned map is a defensive copy (mutations don't affect original)
671-
t.Run("returned map is defensive copy - mutations don't affect original", func(t *testing.T) {
674+
// Test that returned struct is a separate instance (mutations don't affect original)
675+
t.Run("returned struct is independent copy - mutations don't affect original", func(t *testing.T) {
672676
rawConfig := []byte(`{
673677
"deploymentConfig": {
674678
"nodeSelector": {
@@ -684,31 +688,29 @@ func Test_GetDeploymentConfig(t *testing.T) {
684688
require.NoError(t, err)
685689

686690
// Get the deploymentConfig
687-
result1 := cfg.GetDeploymentConfig()
691+
result1, err := cfg.GetDeploymentConfig()
692+
require.NoError(t, err)
688693
require.NotNil(t, result1)
689694

690-
// Mutate the returned map
691-
result1["nodeSelector"] = map[string]any{
692-
"mutated": "value",
693-
}
694-
result1["newField"] = "added"
695+
// Mutate the returned struct
696+
result1.NodeSelector["mutated"] = "value"
695697

696698
// Get deploymentConfig again - should be unaffected by mutations
697-
result2 := cfg.GetDeploymentConfig()
699+
result2, err := cfg.GetDeploymentConfig()
700+
require.NoError(t, err)
698701
require.NotNil(t, result2)
699702

700703
// Original values should be intact
701-
require.Equal(t, map[string]any{
702-
"nodeSelector": map[string]any{
703-
"kubernetes.io/os": "linux",
704-
},
705-
}, result2)
706-
707-
// New field should not exist
708-
_, exists := result2["newField"]
709-
require.False(t, exists)
704+
require.Equal(t, map[string]string{
705+
"kubernetes.io/os": "linux",
706+
}, result2.NodeSelector)
707+
})
710708

711-
// result1 should have the mutations
712-
require.Equal(t, "added", result1["newField"])
709+
// Test that invalid deploymentConfig type returns an error
710+
t.Run("invalid deploymentConfig type returns error", func(t *testing.T) {
711+
cfg := config.Config{"deploymentConfig": "not-an-object"}
712+
result, err := cfg.GetDeploymentConfig()
713+
require.Error(t, err)
714+
require.Nil(t, result)
713715
})
714716
}

0 commit comments

Comments
 (0)