Skip to content

Commit 15b4904

Browse files
(chore): Add JSONSchema validation for bundle configuration (#2316)
ClusterExtension configuration is now validated using JSONSchema. Configuration errors (typos, missing required fields, wrong types) are caught immediately with clear error messages instead of failing during installation. Assisted-by: Cursor
1 parent bf7e39f commit 15b4904

File tree

10 files changed

+1275
-452
lines changed

10 files changed

+1275
-452
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/operator-framework/operator-registry v1.61.0
2424
github.com/prometheus/client_golang v1.23.2
2525
github.com/prometheus/common v0.67.2
26+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
2627
github.com/spf13/cobra v1.10.1
2728
github.com/spf13/pflag v1.0.10
2829
github.com/stretchr/testify v1.11.1
@@ -192,7 +193,6 @@ require (
192193
github.com/rivo/uniseg v0.4.7 // indirect
193194
github.com/rubenv/sql-migrate v1.8.0 // indirect
194195
github.com/russross/blackfriday/v2 v2.1.0 // indirect
195-
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
196196
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
197197
github.com/shopspring/decimal v1.4.0 // indirect
198198
github.com/sigstore/fulcio v1.7.1 // indirect

internal/operator-controller/applier/provider.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1414

1515
ocv1 "github.com/operator-framework/operator-controller/api/v1"
16-
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
16+
"github.com/operator-framework/operator-controller/internal/operator-controller/config"
1717
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
1818
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
1919
)
@@ -69,19 +69,19 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens
6969
}
7070

7171
if r.IsSingleOwnNamespaceEnabled {
72-
bundleConfigBytes := extensionConfigBytes(ext)
73-
// treat no config as empty to properly validate the configuration
74-
// e.g. ensure that validation catches missing required fields
75-
if bundleConfigBytes == nil {
76-
bundleConfigBytes = []byte(`{}`)
72+
schema, err := rv1.GetConfigSchema()
73+
if err != nil {
74+
return nil, fmt.Errorf("error getting configuration schema: %w", err)
7775
}
78-
bundleConfig, err := bundle.UnmarshalConfig(bundleConfigBytes, rv1, ext.Spec.Namespace)
76+
77+
bundleConfigBytes := extensionConfigBytes(ext)
78+
bundleConfig, err := config.UnmarshalConfig(bundleConfigBytes, schema, ext.Spec.Namespace)
7979
if err != nil {
80-
return nil, fmt.Errorf("invalid bundle configuration: %w", err)
80+
return nil, fmt.Errorf("invalid ClusterExtension configuration: %w", err)
8181
}
8282

83-
if bundleConfig != nil && bundleConfig.WatchNamespace != nil {
84-
opts = append(opts, render.WithTargetNamespaces(*bundleConfig.WatchNamespace))
83+
if watchNS := bundleConfig.GetWatchNamespace(); watchNS != nil {
84+
opts = append(opts, render.WithTargetNamespaces(*watchNS))
8585
}
8686
}
8787

internal/operator-controller/applier/provider_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func Test_RegistryV1ManifestProvider_Integration(t *testing.T) {
9797

9898
_, err := provider.Get(bundleFS, ext)
9999
require.Error(t, err)
100-
require.Contains(t, err.Error(), "invalid bundle configuration")
100+
require.Contains(t, err.Error(), "invalid ClusterExtension configuration")
101101
})
102102

103103
t.Run("returns rendered manifests", func(t *testing.T) {
@@ -326,7 +326,7 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) {
326326
},
327327
})
328328
require.Error(t, err)
329-
require.Contains(t, err.Error(), "required field \"watchNamespace\" is missing")
329+
require.Contains(t, err.Error(), `required field "watchNamespace" is missing`)
330330
})
331331

332332
t.Run("accepts bundles with {OwnNamespace} install modes when the appropriate configuration is given", func(t *testing.T) {
@@ -371,7 +371,7 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) {
371371
},
372372
})
373373
require.Error(t, err)
374-
require.Contains(t, err.Error(), "required field \"watchNamespace\" is missing")
374+
require.Contains(t, err.Error(), `required field "watchNamespace" is missing`)
375375
})
376376

377377
t.Run("rejects bundles with {OwnNamespace} install modes when watchNamespace is not install namespace", func(t *testing.T) {
@@ -392,7 +392,9 @@ func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) {
392392
},
393393
})
394394
require.Error(t, err)
395-
require.Contains(t, err.Error(), "invalid 'watchNamespace' \"not-install-namespace\": must be install namespace (install-namespace)")
395+
require.Contains(t, err.Error(), "invalid ClusterExtension configuration:")
396+
require.Contains(t, err.Error(), "watchNamespace must be")
397+
require.Contains(t, err.Error(), "install-namespace")
396398
})
397399

398400
t.Run("rejects bundles without AllNamespaces, SingleNamespace, or OwnNamespace install mode support when Single/OwnNamespace install mode support is enabled", func(t *testing.T) {

0 commit comments

Comments
 (0)