Skip to content

Commit 7874041

Browse files
perdasilvaPer Goncalves da Silva
andauthored
✨ Update .spec.config unmarshalling to accept yaml and json inputs (#2266)
* Improve .spec.config unmarshallig Signed-off-by: Per Goncalves da Silva <[email protected]> * Update .spec.config api doc to ellucidate validation Signed-off-by: Per Goncalves da Silva <[email protected]> --------- Signed-off-by: Per Goncalves da Silva <[email protected]> Co-authored-by: Per Goncalves da Silva <[email protected]>
1 parent 6b88f77 commit 7874041

File tree

7 files changed

+142
-20
lines changed

7 files changed

+142
-20
lines changed

api/v1/clusterextension_types.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,13 @@ type ClusterExtensionSpec struct {
9898
// +optional
9999
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`
100100

101-
// config contains optional configuration values applied during rendering of the
102-
// ClusterExtension's manifests. Values can be specified inline.
101+
// config is an optional field used to specify bundle specific configuration
102+
// used to configure the bundle. Configuration is bundle specific and a bundle may provide
103+
// a configuration schema. When not specified, the default configuration of the resolved bundle will be used.
103104
//
104-
// config is optional. When not specified, the default configuration of the resolved bundle will be used.
105+
// config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
106+
// a configuration schema the final manifests will be derived on a best-effort basis. More information on how
107+
// to configure the bundle should be found in its end-user documentation.
105108
//
106109
// <opcon:experimental>
107110
// +optional
@@ -174,6 +177,8 @@ type ClusterExtensionConfig struct {
174177
// ClusterExtension.
175178
//
176179
// inline must be set if configType is 'Inline'.
180+
// inline accepts arbitrary JSON/YAML objects.
181+
// inline is validation at runtime against the schema provided by the bundle if a schema is provided.
177182
//
178183
// +kubebuilder:validation:Type=object
179184
// +optional

docs/api-reference/olmv1-api-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ _Appears in:_
254254
| Field | Description | Default | Validation |
255255
| --- | --- | --- | --- |
256256
| `configType` _[ClusterExtensionConfigType](#clusterextensionconfigtype)_ | configType is a required reference to the type of configuration source.<br /><br />Allowed values are "Inline"<br /><br />When this field is set to "Inline", the cluster extension configuration is defined inline within the<br />ClusterExtension resource. | | Enum: [Inline] <br />Required: \{\} <br /> |
257-
| `inline` _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#json-v1-apiextensions-k8s-io)_ | inline contains JSON or YAML values specified directly in the<br />ClusterExtension.<br /><br />inline must be set if configType is 'Inline'. | | Type: object <br /> |
257+
| `inline` _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.31/#json-v1-apiextensions-k8s-io)_ | inline contains JSON or YAML values specified directly in the<br />ClusterExtension.<br /><br />inline must be set if configType is 'Inline'.<br />inline accepts arbitrary JSON/YAML objects.<br />inline is validation at runtime against the schema provided by the bundle if a schema is provided. | | Type: object <br /> |
258258

259259

260260
#### ClusterExtensionConfigType
@@ -343,7 +343,7 @@ _Appears in:_
343343
| `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount is a reference to a ServiceAccount used to perform all interactions<br />with the cluster that are required to manage the extension.<br />The ServiceAccount must be configured with the necessary permissions to perform these interactions.<br />The ServiceAccount must exist in the namespace referenced in the spec.<br />serviceAccount is required. | | Required: \{\} <br /> |
344344
| `source` _[SourceConfig](#sourceconfig)_ | source is a required field which selects the installation source of content<br />for this ClusterExtension. Selection is performed by setting the sourceType.<br /><br />Catalog is currently the only implemented sourceType, and setting the<br />sourcetype to "Catalog" requires the catalog field to also be defined.<br /><br />Below is a minimal example of a source definition (in yaml):<br /><br />source:<br /> sourceType: Catalog<br /> catalog:<br /> packageName: example-package | | Required: \{\} <br /> |
345345
| `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is an optional field used to configure the installation options<br />for the ClusterExtension such as the pre-flight check configuration. | | |
346-
| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config contains optional configuration values applied during rendering of the<br />ClusterExtension's manifests. Values can be specified inline.<br /><br />config is optional. When not specified, the default configuration of the resolved bundle will be used.<br /><br /><opcon:experimental> | | |
346+
| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is an optional field used to specify bundle specific configuration<br />used to configure the bundle. Configuration is bundle specific and a bundle may provide<br />a configuration schema. When not specified, the default configuration of the resolved bundle will be used.<br /><br />config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide<br />a configuration schema the final manifests will be derived on a best-effort basis. More information on how<br />to configure the bundle should be found in its end-user documentation.<br /><br /><opcon:experimental> | | |
347347

348348

349349
#### ClusterExtensionStatus

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ spec:
5959
properties:
6060
config:
6161
description: |-
62-
config contains optional configuration values applied during rendering of the
63-
ClusterExtension's manifests. Values can be specified inline.
62+
config is an optional field used to specify bundle specific configuration
63+
used to configure the bundle. Configuration is bundle specific and a bundle may provide
64+
a configuration schema. When not specified, the default configuration of the resolved bundle will be used.
6465
65-
config is optional. When not specified, the default configuration of the resolved bundle will be used.
66+
config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
67+
a configuration schema the final manifests will be derived on a best-effort basis. More information on how
68+
to configure the bundle should be found in its end-user documentation.
6669
properties:
6770
configType:
6871
description: |-
@@ -81,6 +84,8 @@ spec:
8184
ClusterExtension.
8285
8386
inline must be set if configType is 'Inline'.
87+
inline accepts arbitrary JSON/YAML objects.
88+
inline is validation at runtime against the schema provided by the bundle if a schema is provided.
8489
type: object
8590
x-kubernetes-preserve-unknown-fields: true
8691
required:

internal/operator-controller/applier/provider.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package applier
33
import (
44
"crypto/sha256"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io/fs"
9+
"strings"
810

911
"helm.sh/helm/v3/pkg/chart"
1012
"k8s.io/apimachinery/pkg/util/sets"
1113
"k8s.io/apimachinery/pkg/util/validation"
1214
"sigs.k8s.io/controller-runtime/pkg/client"
15+
"sigs.k8s.io/yaml"
1316

1417
"github.com/operator-framework/api/pkg/operators/v1alpha1"
1518

@@ -32,6 +35,9 @@ type RegistryV1ManifestProvider struct {
3235
IsWebhookSupportEnabled bool
3336
IsSingleOwnNamespaceEnabled bool
3437
}
38+
type registryV1Config struct {
39+
WatchNamespace string `json:"watchNamespace"`
40+
}
3541

3642
func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtension) ([]client.Object, error) {
3743
rv1, err := source.FromFS(bundleFS).GetBundle()
@@ -70,7 +76,7 @@ func (r *RegistryV1ManifestProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExtens
7076

7177
watchNamespace, err := r.getWatchNamespace(ext)
7278
if err != nil {
73-
return nil, err
79+
return nil, fmt.Errorf("invalid bundle configuration: %w", err)
7480
}
7581

7682
if watchNamespace != "" {
@@ -89,11 +95,12 @@ func (r *RegistryV1ManifestProvider) getWatchNamespace(ext *ocv1.ClusterExtensio
8995

9096
var watchNamespace string
9197
if ext.Spec.Config != nil && ext.Spec.Config.Inline != nil {
92-
cfg := struct {
93-
WatchNamespace string `json:"watchNamespace"`
94-
}{}
95-
if err := json.Unmarshal(ext.Spec.Config.Inline.Raw, &cfg); err != nil {
96-
return "", fmt.Errorf("invalid bundle configuration: %w", err)
98+
cfg := &registryV1Config{}
99+
// Using k8s.io/yaml package as that is able to handle both json and yaml
100+
// In most cases, at this point we should have a valid JSON/YAML object in the byte slice and failures will
101+
// be related to object structure (e.g. additional fields).
102+
if err := yaml.UnmarshalStrict(ext.Spec.Config.Inline.Raw, cfg); err != nil {
103+
return "", fmt.Errorf("error unmarshalling registry+v1 configuration: %w", formatUnmarshallError(err))
97104
}
98105
watchNamespace = cfg.WatchNamespace
99106
} else {
@@ -153,3 +160,27 @@ func (r *RegistryV1HelmChartProvider) Get(bundleFS fs.FS, ext *ocv1.ClusterExten
153160

154161
return chrt, nil
155162
}
163+
164+
func formatUnmarshallError(err error) error {
165+
var unmarshalErr *json.UnmarshalTypeError
166+
if errors.As(err, &unmarshalErr) {
167+
if unmarshalErr.Field == "" {
168+
return errors.New("input is not a valid JSON object")
169+
} else {
170+
return fmt.Errorf("invalid value type for field %q: expected %q but got %q", unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value)
171+
}
172+
}
173+
174+
// unwrap error until the core and process it
175+
for {
176+
unwrapped := errors.Unwrap(err)
177+
if unwrapped == nil {
178+
// usually the errors present in the form json: <message> or yaml: <message>
179+
// we want to extract <message> if we can
180+
errMessageComponents := strings.Split(err.Error(), ":")
181+
coreErrMessage := strings.TrimSpace(errMessageComponents[len(errMessageComponents)-1])
182+
return errors.New(coreErrMessage)
183+
}
184+
err = unwrapped
185+
}
186+
}

internal/operator-controller/applier/provider_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,77 @@ func Test_RegistryV1ManifestProvider_WebhookSupport(t *testing.T) {
188188
})
189189
}
190190

191+
func Test_RegistryV1ManifestProvider_ConfigUnmarshalling(t *testing.T) {
192+
for _, tc := range []struct {
193+
name string
194+
configBytes []byte
195+
expectedErrMessage string
196+
}{
197+
{
198+
name: "accepts json config",
199+
configBytes: []byte(`{"watchNamespace": "some-namespace"}`),
200+
},
201+
{
202+
name: "accepts yaml config",
203+
configBytes: []byte(`watchNamespace: some-namespace`),
204+
},
205+
{
206+
name: "rejects invalid json",
207+
configBytes: []byte(`{"hello`),
208+
expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: found unexpected end of stream`,
209+
},
210+
{
211+
name: "rejects valid json that isn't of object type",
212+
configBytes: []byte(`true`),
213+
expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: input is not a valid JSON object`,
214+
},
215+
{
216+
name: "rejects additional fields",
217+
configBytes: []byte(`somekey: somevalue`),
218+
expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: unknown field "somekey"`,
219+
},
220+
{
221+
name: "rejects valid json but invalid registry+v1",
222+
configBytes: []byte(`{"watchNamespace": {"hello": "there"}}`),
223+
expectedErrMessage: `invalid bundle configuration: error unmarshalling registry+v1 configuration: invalid value type for field "watchNamespace": expected "string" but got "object"`,
224+
},
225+
} {
226+
t.Run(tc.name, func(t *testing.T) {
227+
provider := applier.RegistryV1ManifestProvider{
228+
BundleRenderer: render.BundleRenderer{
229+
ResourceGenerators: []render.ResourceGenerator{
230+
func(rv1 *bundle.RegistryV1, opts render.Options) ([]client.Object, error) {
231+
return nil, nil
232+
},
233+
},
234+
},
235+
IsSingleOwnNamespaceEnabled: true,
236+
}
237+
238+
bundleFS := bundlefs.Builder().WithPackageName("test").
239+
WithCSV(clusterserviceversion.Builder().WithInstallModeSupportFor(v1alpha1.InstallModeTypeSingleNamespace).Build()).Build()
240+
241+
_, err := provider.Get(bundleFS, &ocv1.ClusterExtension{
242+
Spec: ocv1.ClusterExtensionSpec{
243+
Namespace: "install-namespace",
244+
Config: &ocv1.ClusterExtensionConfig{
245+
ConfigType: ocv1.ClusterExtensionConfigTypeInline,
246+
Inline: &apiextensionsv1.JSON{
247+
Raw: tc.configBytes,
248+
},
249+
},
250+
},
251+
})
252+
if tc.expectedErrMessage != "" {
253+
require.Error(t, err)
254+
require.Contains(t, err.Error(), tc.expectedErrMessage)
255+
} else {
256+
require.NoError(t, err)
257+
}
258+
})
259+
}
260+
}
261+
191262
func Test_RegistryV1ManifestProvider_SingleOwnNamespaceSupport(t *testing.T) {
192263
t.Run("rejects bundles without AllNamespaces install mode when Single/OwnNamespace install mode support is disabled", func(t *testing.T) {
193264
provider := applier.RegistryV1ManifestProvider{

manifests/experimental-e2e.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -864,10 +864,13 @@ spec:
864864
properties:
865865
config:
866866
description: |-
867-
config contains optional configuration values applied during rendering of the
868-
ClusterExtension's manifests. Values can be specified inline.
867+
config is an optional field used to specify bundle specific configuration
868+
used to configure the bundle. Configuration is bundle specific and a bundle may provide
869+
a configuration schema. When not specified, the default configuration of the resolved bundle will be used.
869870
870-
config is optional. When not specified, the default configuration of the resolved bundle will be used.
871+
config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
872+
a configuration schema the final manifests will be derived on a best-effort basis. More information on how
873+
to configure the bundle should be found in its end-user documentation.
871874
properties:
872875
configType:
873876
description: |-
@@ -886,6 +889,8 @@ spec:
886889
ClusterExtension.
887890
888891
inline must be set if configType is 'Inline'.
892+
inline accepts arbitrary JSON/YAML objects.
893+
inline is validation at runtime against the schema provided by the bundle if a schema is provided.
889894
type: object
890895
x-kubernetes-preserve-unknown-fields: true
891896
required:

manifests/experimental.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -829,10 +829,13 @@ spec:
829829
properties:
830830
config:
831831
description: |-
832-
config contains optional configuration values applied during rendering of the
833-
ClusterExtension's manifests. Values can be specified inline.
832+
config is an optional field used to specify bundle specific configuration
833+
used to configure the bundle. Configuration is bundle specific and a bundle may provide
834+
a configuration schema. When not specified, the default configuration of the resolved bundle will be used.
834835
835-
config is optional. When not specified, the default configuration of the resolved bundle will be used.
836+
config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
837+
a configuration schema the final manifests will be derived on a best-effort basis. More information on how
838+
to configure the bundle should be found in its end-user documentation.
836839
properties:
837840
configType:
838841
description: |-
@@ -851,6 +854,8 @@ spec:
851854
ClusterExtension.
852855
853856
inline must be set if configType is 'Inline'.
857+
inline accepts arbitrary JSON/YAML objects.
858+
inline is validation at runtime against the schema provided by the bundle if a schema is provided.
854859
type: object
855860
x-kubernetes-preserve-unknown-fields: true
856861
required:

0 commit comments

Comments
 (0)