Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/operator-controller/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const (
// FormatSingleNamespaceInstallMode defines the format check to ensure that
// the watchNamespace must differ from install namespace
FormatSingleNamespaceInstallMode = "singleNamespaceInstallMode"
// FormatAllNamespacesOnlyInstallMode defines the format check to reject
// watchNamespace when only AllNamespaces mode is supported (registry+v1 specific)
FormatAllNamespacesOnlyInstallMode = "allNamespacesOnlyInstallMode"
)

// SchemaProvider lets each package format type describe what configuration it accepts.
Expand Down Expand Up @@ -192,6 +195,14 @@ func validateConfigWithSchema(configBytes []byte, schema map[string]any, install
return nil
},
})
compiler.RegisterFormat(&jsonschema.Format{
Name: FormatAllNamespacesOnlyInstallMode,
Validate: func(value interface{}) error {
// Always reject - this format is used when AllNamespaces is the only supported mode
// and watchNamespace configuration doesn't make sense
return fmt.Errorf("watchNamespace configuration is not supported when the content only supports AllNamespaces install mode")
},
})

if err := compiler.AddResource(configSchemaID, schema); err != nil {
return fmt.Errorf("failed to load schema: %w", err)
Expand Down
16 changes: 2 additions & 14 deletions internal/operator-controller/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,10 @@ func Test_UnmarshalConfig(t *testing.T) {
expectedErrMessage: `got object, want string`,
},
{
name: "rejects with unknown field when install modes {AllNamespaces}",
name: "rejects with descriptive message when install modes {AllNamespaces}",
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces},
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
expectedErrMessage: `unknown field "watchNamespace"`,
},
{
name: "rejects with unknown field when install modes {MultiNamespace}",
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace},
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
expectedErrMessage: `unknown field "watchNamespace"`,
},
{
name: "reject with unknown field when install modes {AllNamespaces, MultiNamespace}",
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace},
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
expectedErrMessage: `unknown field "watchNamespace"`,
expectedErrMessage: `watchNamespace configuration is not supported when the content only supports AllNamespaces install mode`,
},
{
name: "reject with required field when install modes {OwnNamespace} and watchNamespace is null",
Expand Down
34 changes: 31 additions & 3 deletions internal/operator-controller/rukpak/bundle/registryv1.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (rv1 *RegistryV1) GetConfigSchema() (map[string]any, error) {
// buildBundleConfigSchema creates validation rules based on what the operator supports.
//
// Examples of how install modes affect validation:
// - AllNamespaces only: user can't set watchNamespace (operator watches everything)
// - AllNamespaces only: watchNamespace is explicitly rejected with helpful error
// - OwnNamespace only: user must set watchNamespace to the install namespace
// - SingleNamespace only: user must set watchNamespace to a different namespace
// - AllNamespaces + OwnNamespace: user can optionally set watchNamespace
Expand All @@ -48,8 +48,12 @@ func buildBundleConfigSchema(installModes sets.Set[v1alpha1.InstallMode]) (map[s
properties := map[string]any{}
var required []any

// Add watchNamespace property if the bundle supports it
if isWatchNamespaceConfigurable(installModes) {
// Special case: if ONLY AllNamespaces is supported, explicitly reject watchNamespace
// with a helpful error message (instead of generic "unknown field")
if isAllNamespacesOnly(installModes) {
properties["watchNamespace"] = buildRejectedWatchNamespaceProperty()
} else if isWatchNamespaceConfigurable(installModes) {
// Add watchNamespace property if the bundle supports it
watchNSProperty, isRequired := buildWatchNamespaceProperty(installModes)
properties["watchNamespace"] = watchNSProperty
if isRequired {
Expand Down Expand Up @@ -151,3 +155,27 @@ func isWatchNamespaceConfigRequired(installModes sets.Set[v1alpha1.InstallMode])
return isWatchNamespaceConfigurable(installModes) &&
!installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true})
}

// isAllNamespacesOnly checks if only AllNamespaces install mode is supported.
//
// Returns true when:
// - Only AllNamespaces is supported (no OwnNamespace, no SingleNamespace)
//
// Returns false when:
// - OwnNamespace or SingleNamespace is also supported
func isAllNamespacesOnly(installModes sets.Set[v1alpha1.InstallMode]) bool {
hasAllNamespaces := installModes.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true})
hasConfigurable := isWatchNamespaceConfigurable(installModes)

return hasAllNamespaces && !hasConfigurable
}

// buildRejectedWatchNamespaceProperty creates a schema property that always rejects
// watchNamespace with a descriptive error message for AllNamespaces-only operators.
func buildRejectedWatchNamespaceProperty() map[string]any {
return map[string]any{
"type": "string",
"format": config.FormatAllNamespacesOnlyInstallMode,
"description": "This field is not supported for this operator's install mode configuration",
}
}
Loading