Skip to content

Commit 119d1d1

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

File tree

5 files changed

+276
-3
lines changed

5 files changed

+276
-3
lines changed

internal/operator-controller/rukpak/bundle/registryv1.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import (
77
"github.com/operator-framework/api/pkg/operators/v1alpha1"
88
)
99

10+
// InstallModeType represents the install mode for a bundle
11+
type InstallModeType string
12+
13+
// Install mode constants for registry+v1 bundles
14+
const (
15+
InstallModeAllNamespaces InstallModeType = "AllNamespaces"
16+
InstallModeSingleNamespace InstallModeType = "SingleNamespace"
17+
InstallModeOwnNamespace InstallModeType = "OwnNamespace"
18+
)
19+
1020
type RegistryV1 struct {
1121
PackageName string
1222
CSV v1alpha1.ClusterServiceVersion

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"helm.sh/helm/v3/pkg/chart"
99

10+
registryv1bundle "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
1011
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
1112
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
1213
)
@@ -18,6 +19,24 @@ type BundleToHelmChartConverter struct {
1819
}
1920

2021
func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, installNamespace string, watchNamespace string) (*chart.Chart, error) {
22+
bundleConfig := &render.BundleConfig{}
23+
if watchNamespace != "" && watchNamespace != installNamespace {
24+
bundleConfig.InstallMode = &render.InstallModeConfig{
25+
InstallMode: registryv1bundle.InstallModeSingleNamespace,
26+
SingleNamespaceInstallMode: &render.SingleNamespaceInstallModeConfig{
27+
WatchNamespace: watchNamespace,
28+
},
29+
}
30+
} else if watchNamespace == installNamespace {
31+
bundleConfig.InstallMode = &render.InstallModeConfig{
32+
InstallMode: registryv1bundle.InstallModeOwnNamespace,
33+
}
34+
}
35+
// If watchNamespace is empty, leave bundleConfig.InstallMode as nil (defaults to AllNamespaces)
36+
return r.ToHelmChartWithConfig(bundle, installNamespace, bundleConfig)
37+
}
38+
39+
func (r *BundleToHelmChartConverter) ToHelmChartWithConfig(bundle source.BundleSource, installNamespace string, config *render.BundleConfig) (*chart.Chart, error) {
2140
rv1, err := bundle.GetBundle()
2241
if err != nil {
2342
return nil, err
@@ -41,7 +60,7 @@ func (r *BundleToHelmChartConverter) ToHelmChart(bundle source.BundleSource, ins
4160

4261
objs, err := r.BundleRenderer.Render(
4362
rv1, installNamespace,
44-
render.WithTargetNamespaces(watchNamespace),
63+
render.WithBundleConfig(config),
4564
render.WithCertificateProvider(r.CertificateProvider),
4665
)
4766

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,50 @@ 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.NotNil(t, opts.BundleConfig.InstallMode)
204+
require.Equal(t, bundle.InstallModeSingleNamespace, opts.BundleConfig.InstallMode.InstallMode)
205+
require.NotNil(t, opts.BundleConfig.InstallMode.SingleNamespaceInstallMode)
206+
require.Equal(t, "test-watch-namespace", opts.BundleConfig.InstallMode.SingleNamespaceInstallMode.WatchNamespace)
207+
return []client.Object{&corev1.Service{}}, nil
208+
},
209+
},
210+
},
211+
}
212+
213+
b := source.FromBundle(
214+
bundle.RegistryV1{
215+
CSV: MakeCSV(
216+
WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace),
217+
WithAnnotations(map[string]string{"foo": "bar"}),
218+
),
219+
},
220+
)
221+
222+
config := &render.BundleConfig{
223+
InstallMode: &render.InstallModeConfig{
224+
InstallMode: bundle.InstallModeSingleNamespace,
225+
SingleNamespaceInstallMode: &render.SingleNamespaceInstallModeConfig{
226+
WatchNamespace: "test-watch-namespace",
227+
},
228+
},
229+
}
230+
231+
chart, err := converter.ToHelmChartWithConfig(b, "install-namespace", config)
232+
require.NoError(t, err)
233+
require.NotNil(t, chart)
234+
require.NotNil(t, chart.Metadata)
235+
236+
t.Log("Check Chart metadata contains CSV annotations")
237+
require.Equal(t, map[string]string{"foo": "bar"}, chart.Metadata.Annotations)
238+
239+
t.Log("Check Chart templates have the same number of resources generated by the renderer")
240+
require.Len(t, chart.Templates, 1)
241+
}

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

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,33 @@ 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+
// InstallMode contains install mode specific configuration
62+
InstallMode *InstallModeConfig
63+
}
64+
65+
// InstallModeConfig is a union type for install mode specific configuration
66+
type InstallModeConfig struct {
67+
// InstallMode specifies the install mode for the bundle
68+
InstallMode bundle.InstallModeType
69+
70+
// SingleNamespaceInstallMode is required when InstallMode == "SingleNamespace"
71+
SingleNamespaceInstallMode *SingleNamespaceInstallModeConfig
72+
}
73+
74+
// SingleNamespaceInstallModeConfig contains configuration for SingleNamespace install mode
75+
type SingleNamespaceInstallModeConfig struct {
76+
// WatchNamespace is the namespace to watch (required)
77+
WatchNamespace string
78+
}
79+
5980
type Options struct {
6081
InstallNamespace string
6182
TargetNamespaces []string
6283
UniqueNameGenerator UniqueNameGenerator
6384
CertificateProvider CertificateProvider
85+
BundleConfig *BundleConfig
6486
}
6587

6688
func (o *Options) apply(opts ...Option) *Options {
@@ -83,6 +105,14 @@ func (o *Options) validate(rv1 *bundle.RegistryV1) (*Options, []error) {
83105
if err := validateTargetNamespaces(rv1, o.InstallNamespace, o.TargetNamespaces); err != nil {
84106
errs = append(errs, fmt.Errorf("invalid target namespaces %v: %w", o.TargetNamespaces, err))
85107
}
108+
109+
// Validate bundle configuration
110+
if o.BundleConfig != nil {
111+
if configErrs := validateBundleConfig(o.BundleConfig); len(configErrs) > 0 {
112+
errs = append(errs, configErrs...)
113+
}
114+
}
115+
86116
return o, errs
87117
}
88118

@@ -106,6 +136,12 @@ func WithCertificateProvider(provider CertificateProvider) Option {
106136
}
107137
}
108138

139+
func WithBundleConfig(config *BundleConfig) Option {
140+
return func(o *Options) {
141+
o.BundleConfig = config
142+
}
143+
}
144+
109145
type BundleRenderer struct {
110146
BundleValidator BundleValidator
111147
ResourceGenerators []ResourceGenerator
@@ -118,13 +154,27 @@ func (r BundleRenderer) Render(rv1 bundle.RegistryV1, installNamespace string, o
118154
}
119155

120156
// generate bundle objects
121-
genOpts, errs := (&Options{
157+
genOpts := (&Options{
122158
// default options
123159
InstallNamespace: installNamespace,
124160
TargetNamespaces: []string{metav1.NamespaceAll},
125161
UniqueNameGenerator: DefaultUniqueNameGenerator,
126162
CertificateProvider: nil,
127-
}).apply(opts...).validate(&rv1)
163+
}).apply(opts...)
164+
165+
if genOpts.BundleConfig != nil && genOpts.BundleConfig.InstallMode != nil {
166+
switch genOpts.BundleConfig.InstallMode.InstallMode {
167+
case bundle.InstallModeSingleNamespace:
168+
if genOpts.BundleConfig.InstallMode.SingleNamespaceInstallMode != nil {
169+
genOpts.TargetNamespaces = []string{genOpts.BundleConfig.InstallMode.SingleNamespaceInstallMode.WatchNamespace}
170+
}
171+
case bundle.InstallModeOwnNamespace:
172+
genOpts.TargetNamespaces = []string{installNamespace}
173+
case bundle.InstallModeAllNamespaces:
174+
genOpts.TargetNamespaces = []string{metav1.NamespaceAll}
175+
}
176+
}
177+
genOpts, errs := genOpts.validate(&rv1)
128178

129179
if len(errs) > 0 {
130180
return nil, fmt.Errorf("invalid option(s): %w", errors.Join(errs...))
@@ -175,3 +225,34 @@ func validateTargetNamespaces(rv1 *bundle.RegistryV1, installNamespace string, t
175225
}
176226
return fmt.Errorf("supported install modes %v do not support target namespaces %v", sets.List[string](supportedInstallModes), targetNamespaces)
177227
}
228+
229+
// validateBundleConfig validates that the bundle configuration is internally consistent and complete
230+
func validateBundleConfig(config *BundleConfig) []error {
231+
var errs []error
232+
233+
if config.InstallMode == nil {
234+
return errs
235+
}
236+
237+
switch config.InstallMode.InstallMode {
238+
case bundle.InstallModeAllNamespaces:
239+
// AllNamespaces mode should not have SingleNamespace configuration
240+
if config.InstallMode.SingleNamespaceInstallMode != nil {
241+
errs = append(errs, fmt.Errorf("invalid parameter singleNamespaceInstallMode: must not be set when installMode is %q", bundle.InstallModeAllNamespaces))
242+
}
243+
case bundle.InstallModeSingleNamespace:
244+
// SingleNamespace mode requires SingleNamespace configuration
245+
if config.InstallMode.SingleNamespaceInstallMode == nil {
246+
errs = append(errs, fmt.Errorf("missing required parameter singleNamespaceInstallMode when installMode is %q", bundle.InstallModeSingleNamespace))
247+
} else if config.InstallMode.SingleNamespaceInstallMode.WatchNamespace == "" {
248+
errs = append(errs, fmt.Errorf("missing required parameter watchNamespace in singleNamespaceInstallMode"))
249+
}
250+
case bundle.InstallModeOwnNamespace:
251+
// OwnNamespace mode should not have SingleNamespace configuration
252+
if config.InstallMode.SingleNamespaceInstallMode != nil {
253+
errs = append(errs, fmt.Errorf("invalid parameter singleNamespaceInstallMode: must not be set when installMode is %q", bundle.InstallModeOwnNamespace))
254+
}
255+
}
256+
257+
return errs
258+
}

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

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,119 @@ 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+
config *render.BundleConfig
275+
expectedErr string
276+
}{
277+
{
278+
name: "No config provided - should pass",
279+
config: &render.BundleConfig{},
280+
expectedErr: "",
281+
},
282+
{
283+
name: "AllNamespaces mode - valid config",
284+
config: &render.BundleConfig{
285+
InstallMode: &render.InstallModeConfig{
286+
InstallMode: bundle.InstallModeAllNamespaces,
287+
},
288+
},
289+
expectedErr: "",
290+
},
291+
{
292+
name: "AllNamespaces mode - rejects SingleNamespace config",
293+
config: &render.BundleConfig{
294+
InstallMode: &render.InstallModeConfig{
295+
InstallMode: bundle.InstallModeAllNamespaces,
296+
SingleNamespaceInstallMode: &render.SingleNamespaceInstallModeConfig{
297+
WatchNamespace: "test-namespace",
298+
},
299+
},
300+
},
301+
expectedErr: "invalid parameter singleNamespaceInstallMode: must not be set when installMode is \"AllNamespaces\"",
302+
},
303+
{
304+
name: "SingleNamespace mode - valid config",
305+
config: &render.BundleConfig{
306+
InstallMode: &render.InstallModeConfig{
307+
InstallMode: bundle.InstallModeSingleNamespace,
308+
SingleNamespaceInstallMode: &render.SingleNamespaceInstallModeConfig{
309+
WatchNamespace: "test-namespace",
310+
},
311+
},
312+
},
313+
expectedErr: "",
314+
},
315+
{
316+
name: "SingleNamespace mode - missing config",
317+
config: &render.BundleConfig{
318+
InstallMode: &render.InstallModeConfig{
319+
InstallMode: bundle.InstallModeSingleNamespace,
320+
},
321+
},
322+
expectedErr: "missing required parameter singleNamespaceInstallMode when installMode is \"SingleNamespace\"",
323+
},
324+
{
325+
name: "SingleNamespace mode - empty watchNamespace",
326+
config: &render.BundleConfig{
327+
InstallMode: &render.InstallModeConfig{
328+
InstallMode: bundle.InstallModeSingleNamespace,
329+
SingleNamespaceInstallMode: &render.SingleNamespaceInstallModeConfig{
330+
WatchNamespace: "",
331+
},
332+
},
333+
},
334+
expectedErr: "missing required parameter watchNamespace in singleNamespaceInstallMode",
335+
},
336+
{
337+
name: "OwnNamespace mode - valid config",
338+
config: &render.BundleConfig{
339+
InstallMode: &render.InstallModeConfig{
340+
InstallMode: bundle.InstallModeOwnNamespace,
341+
},
342+
},
343+
expectedErr: "",
344+
},
345+
{
346+
name: "OwnNamespace mode - rejects SingleNamespace config",
347+
config: &render.BundleConfig{
348+
InstallMode: &render.InstallModeConfig{
349+
InstallMode: bundle.InstallModeOwnNamespace,
350+
SingleNamespaceInstallMode: &render.SingleNamespaceInstallModeConfig{
351+
WatchNamespace: "test-namespace",
352+
},
353+
},
354+
},
355+
expectedErr: "invalid parameter singleNamespaceInstallMode: must not be set when installMode is \"OwnNamespace\"",
356+
},
357+
{
358+
name: "Invalid install mode",
359+
config: &render.BundleConfig{
360+
InstallMode: &render.InstallModeConfig{
361+
InstallMode: "InvalidMode",
362+
},
363+
},
364+
expectedErr: "invalid value for parameter installMode: \"InvalidMode\", must be one of [AllNamespaces, SingleNamespace, OwnNamespace]",
365+
},
366+
} {
367+
t.Run(tc.name, func(t *testing.T) {
368+
renderer := render.BundleRenderer{}
369+
_, err := renderer.Render(
370+
bundle.RegistryV1{
371+
CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace, v1alpha1.InstallModeTypeOwnNamespace)),
372+
},
373+
"install-namespace",
374+
render.WithBundleConfig(tc.config),
375+
)
376+
377+
if tc.expectedErr == "" {
378+
require.NoError(t, err)
379+
} else {
380+
require.Error(t, err)
381+
require.Contains(t, err.Error(), tc.expectedErr)
382+
}
383+
})
384+
}
385+
}

0 commit comments

Comments
 (0)