Skip to content

Commit d164dee

Browse files
author
Per G. da Silva
committed
Add registry+v1 bundle config unmarshal function
Signed-off-by: Per G. da Silva <[email protected]>
1 parent f125d8b commit d164dee

File tree

2 files changed

+318
-0
lines changed

2 files changed

+318
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package bundle
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
9+
"k8s.io/apimachinery/pkg/util/sets"
10+
"k8s.io/apimachinery/pkg/util/validation"
11+
"sigs.k8s.io/yaml"
12+
13+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
14+
)
15+
16+
type Config struct {
17+
WatchNamespace string `json:"watchNamespace"`
18+
}
19+
20+
// UnmarshallConfig returns a deserialized and validated *bundle.Config based on bytes and validated
21+
// against rv1 and the desired install namespaces. It will error if:
22+
// - rv is nil
23+
// - bytes is not a valid YAML/JSON object
24+
// - bytes is a valid YAML/JSON object but does not follow the registry+v1 schema
25+
// if bytes is nil a nil bundle.Config is returned
26+
func UnmarshallConfig(bytes []byte, rv1 *RegistryV1, installNamespace string) (*Config, error) {
27+
if bytes == nil {
28+
return nil, nil
29+
}
30+
if rv1 == nil {
31+
return nil, errors.New("bundle is nil")
32+
}
33+
34+
bundleConfig := &Config{}
35+
if err := yaml.UnmarshalStrict(bytes, bundleConfig); err != nil {
36+
return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", formatUnmarshallError(err))
37+
}
38+
39+
if err := validateConfig(bundleConfig, rv1, installNamespace); err != nil {
40+
return nil, fmt.Errorf("error unmarshalling registry+v1 configuration: %w", err)
41+
}
42+
43+
return bundleConfig, nil
44+
}
45+
46+
func validateConfig(config *Config, rv1 *RegistryV1, installNamespace string) error {
47+
// no config, no problem
48+
if config == nil {
49+
return nil
50+
}
51+
52+
// collect bundle install modes
53+
installModeSet := sets.New(rv1.CSV.Spec.InstallModes...)
54+
55+
// only accept a non-empty value for watchNamespace if the bundle configuration accepts the watchNamespace config
56+
if config.WatchNamespace != "" && !hasWatchNamespaceAsConfig(installModeSet) {
57+
return errors.New(`unknown field "watchNamespace"`)
58+
}
59+
60+
// validate input format
61+
if errs := validation.IsDNS1123Subdomain(config.WatchNamespace); len(errs) > 0 {
62+
return fmt.Errorf("invalid 'watchNamespace' %q: namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character", config.WatchNamespace)
63+
}
64+
65+
// only accept install namespace if OwnNamespace install mode is supported
66+
if config.WatchNamespace == installNamespace &&
67+
!installModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}) {
68+
return fmt.Errorf("invalid 'watchNamespace' %q: must not be install namespace (%s)", config.WatchNamespace, installNamespace)
69+
}
70+
71+
if config.WatchNamespace != installNamespace &&
72+
!installModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) {
73+
return fmt.Errorf("invalid 'watchNamespace' %q: must be install namespace (%s)", config.WatchNamespace, installNamespace)
74+
}
75+
76+
return nil
77+
}
78+
79+
func hasWatchNamespaceAsConfig(bundleInstallModeSet sets.Set[v1alpha1.InstallMode]) bool {
80+
return bundleInstallModeSet.Has(v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}) ||
81+
bundleInstallModeSet.HasAll(
82+
v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true},
83+
v1alpha1.InstallMode{Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true})
84+
}
85+
86+
func formatUnmarshallError(err error) error {
87+
var unmarshalErr *json.UnmarshalTypeError
88+
if errors.As(err, &unmarshalErr) {
89+
if unmarshalErr.Field == "" {
90+
return errors.New("input is not a valid JSON object")
91+
} else {
92+
return fmt.Errorf("invalid value type for field %q: expected %q but got %q", unmarshalErr.Field, unmarshalErr.Type.String(), unmarshalErr.Value)
93+
}
94+
}
95+
96+
// unwrap error until the core and process it
97+
for {
98+
unwrapped := errors.Unwrap(err)
99+
if unwrapped == nil {
100+
// usually the errors present in the form json: <message> or yaml: <message>
101+
// we want to extract <message> if we can
102+
errMessageComponents := strings.Split(err.Error(), ":")
103+
coreErrMessage := strings.TrimSpace(errMessageComponents[len(errMessageComponents)-1])
104+
return errors.New(coreErrMessage)
105+
}
106+
err = unwrapped
107+
}
108+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package bundle_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
8+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
9+
10+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
11+
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing/clusterserviceversion"
12+
)
13+
14+
func Test_UnmarshallConfig(t *testing.T) {
15+
for _, tc := range []struct {
16+
name string
17+
rawConfig []byte
18+
supportedInstallModes []v1alpha1.InstallModeType
19+
installNamespace string
20+
expectedErrMessage string
21+
expectedConfig *bundle.Config
22+
}{
23+
{
24+
name: "accepts nil raw config",
25+
rawConfig: nil,
26+
expectedConfig: nil,
27+
},
28+
{
29+
name: "rejects nil rv1",
30+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
31+
expectedErrMessage: `bundle is nil`,
32+
},
33+
{
34+
name: "accepts json config",
35+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
36+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
37+
expectedConfig: &bundle.Config{
38+
WatchNamespace: "some-namespace",
39+
},
40+
},
41+
{
42+
name: "accepts yaml config",
43+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
44+
rawConfig: []byte(`watchNamespace: some-namespace`),
45+
expectedConfig: &bundle.Config{
46+
WatchNamespace: "some-namespace",
47+
},
48+
},
49+
{
50+
name: "rejects invalid json",
51+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
52+
rawConfig: []byte(`{"hello`),
53+
expectedErrMessage: `error unmarshalling registry+v1 configuration: found unexpected end of stream`,
54+
},
55+
{
56+
name: "rejects valid json that isn't of object type",
57+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
58+
rawConfig: []byte(`true`),
59+
expectedErrMessage: `error unmarshalling registry+v1 configuration: input is not a valid JSON object`,
60+
},
61+
{
62+
name: "rejects additional fields",
63+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
64+
rawConfig: []byte(`somekey: somevalue`),
65+
expectedErrMessage: `error unmarshalling registry+v1 configuration: unknown field "somekey"`,
66+
},
67+
{
68+
name: "rejects valid json but invalid registry+v1",
69+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
70+
rawConfig: []byte(`{"watchNamespace": {"hello": "there"}}`),
71+
expectedErrMessage: `error unmarshalling registry+v1 configuration: invalid value type for field "watchNamespace": expected "string" but got "object"`,
72+
},
73+
{
74+
name: "rejects bad namespace format",
75+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
76+
rawConfig: []byte(`{"watchNamespace": "bad-Namespace-"}`),
77+
expectedErrMessage: "invalid 'watchNamespace' \"bad-Namespace-\": namespace must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character",
78+
},
79+
{
80+
name: "rejects with unknown field when install modes {AllNamespaces}",
81+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces},
82+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
83+
expectedErrMessage: "unknown field \"watchNamespace\"",
84+
},
85+
{
86+
name: "rejects with unknown field when install modes {MultiNamespace}",
87+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace},
88+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
89+
expectedErrMessage: "unknown field \"watchNamespace\"",
90+
},
91+
{
92+
name: "reject with unknown field when install modes {AllNamespaces, MultiNamespace}",
93+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeMultiNamespace},
94+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
95+
expectedErrMessage: "unknown field \"watchNamespace\"",
96+
},
97+
{
98+
name: "reject with unknown field when install modes {OwnNamespace}",
99+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace},
100+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
101+
expectedErrMessage: "unknown field \"watchNamespace\"",
102+
},
103+
{
104+
name: "reject with unknown field when install modes {MultiNamespace, OwnNamespace}",
105+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeOwnNamespace},
106+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
107+
expectedErrMessage: "unknown field \"watchNamespace\"",
108+
},
109+
{
110+
name: "accepts when install modes {SingleNamespace} and watchNamespace != install namespace",
111+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
112+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
113+
expectedConfig: &bundle.Config{
114+
WatchNamespace: "some-namespace",
115+
},
116+
},
117+
{
118+
name: "accepts when install modes {AllNamespaces, SingleNamespace} and watchNamespace != install namespace",
119+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace},
120+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
121+
expectedConfig: &bundle.Config{
122+
WatchNamespace: "some-namespace",
123+
},
124+
},
125+
{
126+
name: "accepts when install modes {MultiNamespace, SingleNamespace} and watchNamespace != install namespace",
127+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeSingleNamespace},
128+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
129+
expectedConfig: &bundle.Config{
130+
WatchNamespace: "some-namespace",
131+
},
132+
},
133+
{
134+
name: "accepts when install modes {OwnNamespace, SingleNamespace} and watchNamespace != install namespace",
135+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace},
136+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
137+
installNamespace: "not-namespace",
138+
expectedConfig: &bundle.Config{
139+
WatchNamespace: "some-namespace",
140+
},
141+
},
142+
{
143+
name: "rejects when install modes {SingleNamespace} and watchNamespace == install namespace",
144+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeSingleNamespace},
145+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
146+
installNamespace: "some-namespace",
147+
expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)",
148+
},
149+
{
150+
name: "rejects when install modes {AllNamespaces, SingleNamespace} and watchNamespace == install namespace",
151+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeSingleNamespace},
152+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
153+
installNamespace: "some-namespace",
154+
expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)",
155+
},
156+
{
157+
name: "rejects when install modes {MultiNamespace, SingleNamespace} and watchNamespace == install namespace",
158+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeMultiNamespace, v1alpha1.InstallModeTypeSingleNamespace},
159+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
160+
installNamespace: "some-namespace",
161+
expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must not be install namespace (some-namespace)",
162+
},
163+
{
164+
name: "accepts when install modes {AllNamespaces, OwnNamespace} and watchNamespace == install namespace",
165+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace},
166+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
167+
installNamespace: "some-namespace",
168+
expectedConfig: &bundle.Config{
169+
WatchNamespace: "some-namespace",
170+
},
171+
},
172+
{
173+
name: "accepts when install modes {OwnNamespace, SingleNamespace} and watchNamespace == install namespace",
174+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeOwnNamespace, v1alpha1.InstallModeTypeSingleNamespace},
175+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
176+
installNamespace: "some-namespace",
177+
expectedConfig: &bundle.Config{
178+
WatchNamespace: "some-namespace",
179+
},
180+
},
181+
{
182+
name: "rejects when install modes {AllNamespaces, OwnNamespace} and watchNamespace != install namespace",
183+
supportedInstallModes: []v1alpha1.InstallModeType{v1alpha1.InstallModeTypeAllNamespaces, v1alpha1.InstallModeTypeOwnNamespace},
184+
rawConfig: []byte(`{"watchNamespace": "some-namespace"}`),
185+
installNamespace: "not-some-namespace",
186+
expectedErrMessage: "invalid 'watchNamespace' \"some-namespace\": must be install namespace (not-some-namespace)",
187+
},
188+
} {
189+
t.Run(tc.name, func(t *testing.T) {
190+
var rv1 *bundle.RegistryV1
191+
if tc.supportedInstallModes != nil {
192+
rv1 = &bundle.RegistryV1{
193+
CSV: clusterserviceversion.Builder().
194+
WithName("test-operator").
195+
WithInstallModeSupportFor(tc.supportedInstallModes...).
196+
Build(),
197+
}
198+
}
199+
200+
config, err := bundle.UnmarshallConfig(tc.rawConfig, rv1, tc.installNamespace)
201+
require.Equal(t, tc.expectedConfig, config)
202+
if tc.expectedErrMessage != "" {
203+
require.Error(t, err)
204+
require.Contains(t, err.Error(), tc.expectedErrMessage)
205+
} else {
206+
require.NoError(t, err)
207+
}
208+
})
209+
}
210+
}

0 commit comments

Comments
 (0)