Skip to content

Commit 7b919f4

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

File tree

4 files changed

+237
-3
lines changed

4 files changed

+237
-3
lines changed

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

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

2020
func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) {
21+
bundleConfig := &render.BundleConfig{}
22+
if watchNamespace != "" {
23+
bundleConfig.WatchNamespace = watchNamespace
24+
}
25+
return r.ToHelmChartWithConfig(bundle, installNamespace, bundleConfig)
26+
}
27+
28+
func (r *BundleToHelmChartConverter) ToHelmChartWithConfig(bundle source.BundleSource, installNamespace string, config *render.BundleConfig) (*chart.Chart, error) {
2129
rv1, err := bundle.GetBundle()
2230
if err != nil {
2331
return nil, err
@@ -41,7 +49,7 @@ func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, ins
4149

4250
objs, err := r.BundleRenderer.Render(
4351
rv1, installNamespace,
44-
render.WithTargetNamespaces(watchNamespace),
52+
render.WithBundleConfig(config),
4553
render.WithCertificateProvider(r.CertificateProvider),
4654
)
4755

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,42 @@ 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+
}

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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,111 @@ func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) {
267267
require.NoError(t, val.Validate(nil))
268268
require.Equal(t, "hi", actual)
269269
}
270+
271+
func Test_BundleRenderer_ValidatesBundleConfig(t *testing.T) {
272+
for _, tc := range []struct {
273+
name string
274+
installModes []v1alpha1.InstallModeType
275+
config *render.BundleConfig
276+
expectedErr string
277+
}{
278+
{
279+
name: "AllNamespaces only - rejects watchNamespace",
280+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces},
281+
config: &render.BundleConfig{
282+
WatchNamespace: "test-namespace",
283+
},
284+
expectedErr: "watchNamespace configuration parameter is not supported for bundles that only support AllNamespaces install mode",
285+
},
286+
{
287+
name: "AllNamespaces only - accepts empty watchNamespace",
288+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces},
289+
config: &render.BundleConfig{
290+
WatchNamespace: "",
291+
},
292+
expectedErr: "",
293+
},
294+
{
295+
name: "AllNamespaces and SingleNamespace - accepts any watchNamespace",
296+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace},
297+
config: &render.BundleConfig{
298+
WatchNamespace: "test-namespace",
299+
},
300+
expectedErr: "",
301+
},
302+
{
303+
name: "AllNamespaces and OwnNamespace - accepts matching watchNamespace",
304+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace},
305+
config: &render.BundleConfig{
306+
WatchNamespace: "install-namespace",
307+
},
308+
expectedErr: "",
309+
},
310+
{
311+
name: "AllNamespaces and OwnNamespace - rejects non-matching watchNamespace",
312+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace},
313+
config: &render.BundleConfig{
314+
WatchNamespace: "different-namespace",
315+
},
316+
expectedErr: "watchNamespace must equal install namespace (install-namespace) when set for bundles supporting AllNamespaces and OwnNamespace install modes",
317+
},
318+
{
319+
name: "SingleNamespace only - requires watchNamespace",
320+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
321+
config: &render.BundleConfig{
322+
WatchNamespace: "",
323+
},
324+
expectedErr: "watchNamespace configuration parameter is required for bundles that only support SingleNamespace install mode",
325+
},
326+
{
327+
name: "SingleNamespace only - accepts watchNamespace",
328+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
329+
config: &render.BundleConfig{
330+
WatchNamespace: "test-namespace",
331+
},
332+
expectedErr: "",
333+
},
334+
{
335+
name: "SingleNamespace and OwnNamespace - requires watchNamespace",
336+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace},
337+
config: &render.BundleConfig{
338+
WatchNamespace: "",
339+
},
340+
expectedErr: "watchNamespace configuration parameter is required for bundles supporting SingleNamespace and OwnNamespace install modes",
341+
},
342+
{
343+
name: "OwnNamespace only - rejects watchNamespace",
344+
installModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace},
345+
config: &render.BundleConfig{
346+
WatchNamespace: "test-namespace",
347+
},
348+
expectedErr: "watchNamespace configuration parameter is not supported for bundles that only support OwnNamespace install mode",
349+
},
350+
{
351+
name: "No install modes - invalid bundle",
352+
installModes: []v1alpha1.InstallModeType{},
353+
config: &render.BundleConfig{
354+
WatchNamespace: "test-namespace",
355+
},
356+
expectedErr: "invalid bundle: no supported install modes",
357+
},
358+
} {
359+
t.Run(tc.name, func(t *testing.T) {
360+
renderer := render.BundleRenderer{}
361+
_, err := renderer.Render(
362+
bundle.RegistryV1{
363+
CSV: MakeCSV(WithInstallModeSupportFor(tc.installModes...)),
364+
},
365+
"install-namespace",
366+
render.WithBundleConfig(tc.config),
367+
)
368+
369+
if tc.expectedErr == "" {
370+
require.NoError(t, err)
371+
} else {
372+
require.Error(t, err)
373+
require.Contains(t, err.Error(), tc.expectedErr)
374+
}
375+
})
376+
}
377+
}

0 commit comments

Comments
 (0)