Skip to content

Commit d4af9f8

Browse files
committed
(rukpak) extend bundle renderer to accept config opts
Introduce BundleConfig that contains InstallConfig and DeploymentConfig.
1 parent dff07d5 commit d4af9f8

File tree

4 files changed

+300
-3
lines changed

4 files changed

+300
-3
lines changed

internal/operator-controller/rukpak/convert/helm.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ type BundleToHelmChartConverter struct {
1818
}
1919

2020
func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) {
21+
// Create bundle config from watchNamespace for backward compatibility
22+
bundleConfig := &render.BundleConfig{}
23+
if watchNamespace != "" {
24+
bundleConfig.WatchNamespace = watchNamespace
25+
}
26+
return r.ToHelmChartWithConfig(bundle, installNamespace, bundleConfig)
27+
}
28+
29+
func (r *BundleToHelmChartConverter) ToHelmChartWithConfig(bundle source.BundleSource, installNamespace string, config *render.BundleConfig) (*chart.Chart, error) {
2130
rv1, err := bundle.GetBundle()
2231
if err != nil {
2332
return nil, err
@@ -41,7 +50,7 @@ func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, ins
4150

4251
objs, err := r.BundleRenderer.Render(
4352
rv1, installNamespace,
44-
render.WithTargetNamespaces(watchNamespace),
53+
render.WithBundleConfig(config),
4554
render.WithCertificateProvider(r.CertificateProvider),
4655
)
4756

internal/operator-controller/rukpak/convert/helm_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,67 @@ func Test_BundleToHelmChartConverter_ToHelmChart_Success(t *testing.T) {
192192
t.Log("Check Chart templates have the same number of resources generated by the renderer")
193193
require.Len(t, chart.Templates, 1)
194194
}
195+
196+
func Test_BundleToHelmChartConverter_ToHelmChartWithConfig_Success(t *testing.T) {
197+
converter := convert.BundleToHelmChartConverter{
198+
BundleRenderer: render.BundleRenderer{
199+
ResourceGenerators: []render.ResourceGenerator{
200+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
201+
// Verify that bundle config is passed correctly
202+
require.NotNil(t, opts.BundleConfig)
203+
require.Equal(t, "test-watch-namespace", opts.BundleConfig.WatchNamespace)
204+
return []client.Object{&corev1.Service{}}, nil
205+
},
206+
},
207+
},
208+
}
209+
210+
b := source.FromBundle(
211+
bundle.RegistryV1{
212+
CSV: MakeCSV(
213+
WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace),
214+
WithAnnotations(map[string]string{"foo": "bar"}),
215+
),
216+
},
217+
)
218+
219+
config := &render.BundleConfig{
220+
WatchNamespace: "test-watch-namespace",
221+
}
222+
223+
chart, err := converter.ToHelmChartWithConfig(b, "install-namespace", config)
224+
require.NoError(t, err)
225+
require.NotNil(t, chart)
226+
require.NotNil(t, chart.Metadata)
227+
228+
t.Log("Check Chart metadata contains CSV annotations")
229+
require.Equal(t, map[string]string{"foo": "bar"}, chart.Metadata.Annotations)
230+
231+
t.Log("Check Chart templates have the same number of resources generated by the renderer")
232+
require.Len(t, chart.Templates, 1)
233+
}
234+
235+
func Test_BundleToHelmChartConverter_ToHelmChart_BackwardCompatibility(t *testing.T) {
236+
converter := convert.BundleToHelmChartConverter{
237+
BundleRenderer: render.BundleRenderer{
238+
ResourceGenerators: []render.ResourceGenerator{
239+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
240+
// Verify that watchNamespace is converted to bundle config
241+
require.NotNil(t, opts.BundleConfig)
242+
require.Equal(t, "test-watch-namespace", opts.BundleConfig.WatchNamespace)
243+
return []client.Object{&corev1.Service{}}, nil
244+
},
245+
},
246+
},
247+
}
248+
249+
b := source.FromBundle(
250+
bundle.RegistryV1{
251+
CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace)),
252+
},
253+
)
254+
255+
chart, err := converter.ToHelmChart(b, "install-namespace", "test-watch-namespace")
256+
require.NoError(t, err)
257+
require.NotNil(t, chart)
258+
}

internal/operator-controller/rukpak/render/render.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,18 @@ func (r ResourceGenerators) ResourceGenerator() ResourceGenerator {
5656

5757
type UniqueNameGenerator func(string, interface{}) (string, error)
5858

59+
// BundleConfig represents configuration options for bundle rendering
60+
type BundleConfig struct {
61+
// WatchNamespace specifies the namespace to watch for Single/OwnNamespace install modes
62+
WatchNamespace string `json:"watchNamespace,omitempty"`
63+
}
64+
5965
type Options struct {
6066
InstallNamespace string
6167
TargetNamespaces []string
6268
UniqueNameGenerator UniqueNameGenerator
6369
CertificateProvider CertificateProvider
70+
BundleConfig *BundleConfig
6471
}
6572

6673
func (o *Options) apply(opts ...Option) *Options {
@@ -83,6 +90,14 @@ func (o *Options) validate(rv1 *bundle.RegistryV1) (*Options, []error) {
8390
if err := validateTargetNamespaces(rv1, o.InstallNamespace, o.TargetNamespaces); err != nil {
8491
errs = append(errs, fmt.Errorf("invalid target namespaces %v: %w", o.TargetNamespaces, err))
8592
}
93+
94+
// Validate bundle configuration
95+
if o.BundleConfig != nil {
96+
if configErrs := validateBundleConfig(rv1, o.InstallNamespace, o.BundleConfig); len(configErrs) > 0 {
97+
errs = append(errs, configErrs...)
98+
}
99+
}
100+
86101
return o, errs
87102
}
88103

@@ -106,6 +121,12 @@ func WithCertificateProvider(provider CertificateProvider) Option {
106121
}
107122
}
108123

124+
func WithBundleConfig(config *BundleConfig) Option {
125+
return func(o *Options) {
126+
o.BundleConfig = config
127+
}
128+
}
129+
109130
type BundleRenderer struct {
110131
BundleValidator BundleValidator
111132
ResourceGenerators []ResourceGenerator
@@ -118,13 +139,19 @@ func (r BundleRenderer) Render(rv1 bundle.RegistryV1, installNamespace string, o
118139
}
119140

120141
// generate bundle objects
121-
genOpts, errs := (&Options{
142+
genOpts := (&Options{
122143
// default options
123144
InstallNamespace: installNamespace,
124145
TargetNamespaces: []string{metav1.NamespaceAll},
125146
UniqueNameGenerator: DefaultUniqueNameGenerator,
126147
CertificateProvider: nil,
127-
}).apply(opts...).validate(&rv1)
148+
}).apply(opts...)
149+
150+
// If bundle config is provided and contains watchNamespace, derive target namespaces from it
151+
if genOpts.BundleConfig != nil && genOpts.BundleConfig.WatchNamespace != "" {
152+
genOpts.TargetNamespaces = []string{genOpts.BundleConfig.WatchNamespace}
153+
}
154+
genOpts, errs := genOpts.validate(&rv1)
128155

129156
if len(errs) > 0 {
130157
return nil, fmt.Errorf("invalid option(s): %w", errors.Join(errs...))
@@ -175,3 +202,55 @@ func validateTargetNamespaces(rv1 *bundle.RegistryV1, installNamespace string, t
175202
}
176203
return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[string](supportedInstallModes), targetNamespaces)
177204
}
205+
206+
// validateBundleConfig validates the bundle configuration against the bundle's supported install modes
207+
func validateBundleConfig(rv1 *bundle.RegistryV1, installNamespace string, config *BundleConfig) []error {
208+
var errs []error
209+
210+
supportedInstallModes := sets.New[string]()
211+
for _, im := range rv1.CSV.Spec.InstallModes {
212+
if im.Supported {
213+
supportedInstallModes.Insert(string(im.Type))
214+
}
215+
}
216+
217+
allSupported := supportedInstallModes.Has(string(v1alpha1.InstallModeTypeAllNamespaces))
218+
singleSupported := supportedInstallModes.Has(string(v1alpha1.InstallModeTypeSingleNamespace))
219+
ownSupported := supportedInstallModes.Has(string(v1alpha1.InstallModeTypeOwnNamespace))
220+
221+
switch {
222+
case allSupported && !singleSupported && !ownSupported:
223+
// All, no Single, no Own: watchNamespace should be unknown/non-existent
224+
if config.WatchNamespace != "" {
225+
errs = append(errs, fmt.Errorf("watchNamespace configuration parameter is not supported for bundles that only support AllNamespaces install mode"))
226+
}
227+
case allSupported && singleSupported && !ownSupported:
228+
// All, Single, no Own: watchNamespace is optional
229+
// No validation needed - any value is acceptable
230+
case allSupported && !singleSupported && ownSupported:
231+
// All, no Single, Own: watchNamespace is optional (must == install namespace when set)
232+
if config.WatchNamespace != "" && config.WatchNamespace != installNamespace {
233+
errs = append(errs, fmt.Errorf("watchNamespace must equal install namespace (%s) when set for bundles supporting AllNamespaces and OwnNamespace install modes", installNamespace))
234+
}
235+
case !allSupported && singleSupported && !ownSupported:
236+
// no All, Single, no Own: watchNamespace is required
237+
if config.WatchNamespace == "" {
238+
errs = append(errs, fmt.Errorf("watchNamespace configuration parameter is required for bundles that only support SingleNamespace install mode"))
239+
}
240+
case !allSupported && singleSupported && ownSupported:
241+
// no All, Single, Own: watchNamespace is required
242+
if config.WatchNamespace == "" {
243+
errs = append(errs, fmt.Errorf("watchNamespace configuration parameter is required for bundles supporting SingleNamespace and OwnNamespace install modes"))
244+
}
245+
case !allSupported && !singleSupported && ownSupported:
246+
// no All, no Single, Own: watchNamespace should be unknown/non-existent
247+
if config.WatchNamespace != "" {
248+
errs = append(errs, fmt.Errorf("watchNamespace configuration parameter is not supported for bundles that only support OwnNamespace install mode"))
249+
}
250+
case !allSupported && !singleSupported && !ownSupported:
251+
// no All, no Single, no Own: invalid bundle
252+
errs = append(errs, fmt.Errorf("invalid bundle: no supported install modes"))
253+
}
254+
255+
return errs
256+
}

internal/operator-controller/rukpak/render/render_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,148 @@ func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) {
267267
require.NoError(t, val.Validate(nil))
268268
require.Equal(t, "hi", actual)
269269
}
270+
271+
func Test_WithBundleConfig(t *testing.T) {
272+
config := &render.BundleConfig{
273+
WatchNamespace: "test-namespace",
274+
}
275+
opts := &render.Options{}
276+
render.WithBundleConfig(config)(opts)
277+
require.Equal(t, config, opts.BundleConfig)
278+
}
279+
280+
func Test_BundleRenderer_ValidatesBundleConfig(t *testing.T) {
281+
for _, tc := range []struct {
282+
name string
283+
installModes []v1alpha1.InstallModeType
284+
config *render.BundleConfig
285+
expectedErr string
286+
}{
287+
{
288+
name: "AllNamespaces only - rejects watchNamespace",
289+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces},
290+
config: &render.BundleConfig{
291+
WatchNamespace: "test-namespace",
292+
},
293+
expectedErr: "watchNamespace configuration parameter is not supported for bundles that only support AllNamespaces install mode",
294+
},
295+
{
296+
name: "AllNamespaces only - accepts empty watchNamespace",
297+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces},
298+
config: &render.BundleConfig{
299+
WatchNamespace: "",
300+
},
301+
expectedErr: "",
302+
},
303+
{
304+
name: "AllNamespaces and SingleNamespace - accepts any watchNamespace",
305+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace},
306+
config: &render.BundleConfig{
307+
WatchNamespace: "test-namespace",
308+
},
309+
expectedErr: "",
310+
},
311+
{
312+
name: "AllNamespaces and OwnNamespace - accepts matching watchNamespace",
313+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace},
314+
config: &render.BundleConfig{
315+
WatchNamespace: "install-namespace",
316+
},
317+
expectedErr: "",
318+
},
319+
{
320+
name: "AllNamespaces and OwnNamespace - rejects non-matching watchNamespace",
321+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace},
322+
config: &render.BundleConfig{
323+
WatchNamespace: "different-namespace",
324+
},
325+
expectedErr: "watchNamespace must equal install namespace (install-namespace) when set for bundles supporting AllNamespaces and OwnNamespace install modes",
326+
},
327+
{
328+
name: "SingleNamespace only - requires watchNamespace",
329+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
330+
config: &render.BundleConfig{
331+
WatchNamespace: "",
332+
},
333+
expectedErr: "watchNamespace configuration parameter is required for bundles that only support SingleNamespace install mode",
334+
},
335+
{
336+
name: "SingleNamespace only - accepts watchNamespace",
337+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
338+
config: &render.BundleConfig{
339+
WatchNamespace: "test-namespace",
340+
},
341+
expectedErr: "",
342+
},
343+
{
344+
name: "SingleNamespace and OwnNamespace - requires watchNamespace",
345+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace},
346+
config: &render.BundleConfig{
347+
WatchNamespace: "",
348+
},
349+
expectedErr: "watchNamespace configuration parameter is required for bundles supporting SingleNamespace and OwnNamespace install modes",
350+
},
351+
{
352+
name: "OwnNamespace only - rejects watchNamespace",
353+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace},
354+
config: &render.BundleConfig{
355+
WatchNamespace: "test-namespace",
356+
},
357+
expectedErr: "watchNamespace configuration parameter is not supported for bundles that only support OwnNamespace install mode",
358+
},
359+
{
360+
name: "No install modes - invalid bundle",
361+
installModes: []v1alpha1.InstallModeType{},
362+
config: &render.BundleConfig{
363+
WatchNamespace: "test-namespace",
364+
},
365+
expectedErr: "invalid bundle: no supported install modes",
366+
},
367+
} {
368+
t.Run(tc.name, func(t *testing.T) {
369+
renderer := render.BundleRenderer{}
370+
_, err := renderer.Render(
371+
bundle.RegistryV1{
372+
CSV: MakeCSV(WithInstallModeSupportFor(tc.installModes...)),
373+
},
374+
"install-namespace",
375+
render.WithBundleConfig(tc.config),
376+
)
377+
378+
if tc.expectedErr == "" {
379+
require.NoError(t, err)
380+
} else {
381+
require.Error(t, err)
382+
require.Contains(t, err.Error(), tc.expectedErr)
383+
}
384+
})
385+
}
386+
}
387+
388+
func Test_BundleRenderer_DerivesTargetNamespacesFromConfig(t *testing.T) {
389+
expectedWatchNamespace := "test-watch-namespace"
390+
391+
renderer := render.BundleRenderer{
392+
ResourceGenerators: []render.ResourceGenerator{
393+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
394+
// Verify that target namespaces are derived from bundle config
395+
require.Equal(t, []string{expectedWatchNamespace}, opts.TargetNamespaces)
396+
return nil, nil
397+
},
398+
},
399+
}
400+
401+
config := &render.BundleConfig{
402+
WatchNamespace: expectedWatchNamespace,
403+
}
404+
405+
_, err := renderer.Render(
406+
bundle.RegistryV1{
407+
CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace)),
408+
},
409+
"install-namespace",
410+
render.WithBundleConfig(config),
411+
)
412+
413+
require.NoError(t, err)
414+
}

0 commit comments

Comments
 (0)