Skip to content

Commit 5b79578

Browse files
author
Per Goncalves da Silva
committed
Improve .spec.config unmarshallig
Signed-off-by: Per Goncalves da Silva <[email protected]>
1 parent 292c0db commit 5b79578

File tree

2 files changed

+108
-6
lines changed

2 files changed

+108
-6
lines changed

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{

0 commit comments

Comments
 (0)