Skip to content

Commit a4520ff

Browse files
committedApr 9, 2021
Improve the declcfg and model validation
1. Missing `defaultChannel` doesn’t get caught and error out as an empty channel issue which is incorrect. 2. Missing `package` property will result out of bound error 3. Bundles without channel is allowed which will lead to orphaned bundles 4. Bundle without a bundle image is valid when it shouldn’t be. Bundle must either have bundle image or bundle data (objects). Signed-off-by: Vu Dinh <[email protected]>
1 parent e50cfdd commit a4520ff

File tree

8 files changed

+119
-21
lines changed

8 files changed

+119
-21
lines changed
 

‎cmd/opm/root/cmd.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func NewCmd() *cobra.Command {
2525
},
2626
}
2727

28-
cmd.AddCommand(registry.NewOpmRegistryCmd(), alpha.NewCmd(), serve.NewCmd(), newAddCmd(), validate.NewConfigValidateCmd())
28+
cmd.AddCommand(registry.NewOpmRegistryCmd(), alpha.NewCmd(), serve.NewCmd(), newAddCmd(), validate.NewCmd())
2929
index.AddCommand(cmd)
3030
version.AddCommand(cmd)
3131

‎cmd/opm/validate/validate.go

+9-10
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@ import (
1010
"github.com/operator-framework/operator-registry/pkg/lib/config"
1111
)
1212

13-
func NewConfigValidateCmd() *cobra.Command {
13+
func NewCmd() *cobra.Command {
1414
validate := &cobra.Command{
1515
Use: "validate <directory>",
1616
Short: "Validate the declarative index config",
1717
Long: "Validate the declarative config JSON file(s) in a given directory",
18-
Args: func(cmd *cobra.Command, args []string) error {
19-
return cobra.ExactArgs(1)(cmd, args)
20-
},
21-
RunE: configValidate,
18+
Args: cobra.ExactArgs(1),
19+
RunE: configValidate,
2220
}
2321

2422
validate.Flags().BoolP("debug", "d", false, "enable debug log output")
@@ -31,16 +29,17 @@ func configValidate(cmd *cobra.Command, args []string) error {
3129
return err
3230
}
3331

32+
directory := args[0]
33+
if _, err := os.Stat(directory); os.IsNotExist(err) {
34+
return err
35+
}
36+
3437
logger := logrus.WithField("cmd", "validate")
3538
if debug {
3639
logger.Logger.SetLevel(logrus.DebugLevel)
3740
}
3841

39-
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
40-
logger.Error(err.Error())
41-
}
42-
43-
err = config.ValidateConfig(args[0])
42+
err = config.ValidateConfig(directory)
4443
if err != nil {
4544
logger.Error(err.Error())
4645
return fmt.Errorf("failed to validate config: %s", err)

‎internal/declcfg/declcfg_to_model.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,18 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
4141
return nil, fmt.Errorf("parse properties for bundle %q: %v", b.Name, err)
4242
}
4343

44+
if len(props.Packages) == 0 {
45+
return nil, fmt.Errorf("missing package property for bundle %q", b.Name)
46+
}
47+
4448
if b.Package != props.Packages[0].PackageName {
4549
return nil, fmt.Errorf("package %q does not match %q property %q", b.Package, property.TypePackage, props.Packages[0].PackageName)
4650
}
4751

52+
if len(props.Channels) == 0 {
53+
return nil, fmt.Errorf("bundle %q is missing channel information", b.Name)
54+
}
55+
4856
for _, bundleChannel := range props.Channels {
4957
pkgChannel, ok := mpkg.Channels[bundleChannel.Name]
5058
if !ok {
@@ -75,7 +83,7 @@ func ConvertToModel(cfg DeclarativeConfig) (model.Model, error) {
7583

7684
for _, mpkg := range mpkgs {
7785
defaultChannelName := defaultChannels[mpkg.Name]
78-
if mpkg.DefaultChannel == nil {
86+
if defaultChannelName != "" && mpkg.DefaultChannel == nil {
7987
dch := &model.Channel{
8088
Package: mpkg,
8189
Name: defaultChannelName,

‎internal/declcfg/declcfg_to_model_test.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ func TestConvertToModel(t *testing.T) {
2828
assertion: require.Error,
2929
cfg: DeclarativeConfig{
3030
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
31-
Bundles: []Bundle{newTestBundle("bar", "0.1.0")},
31+
Bundles: []Bundle{newTestBundle("bar", "0.1.0", withChannel("alpha", ""))},
3232
},
3333
},
3434
{
35-
name: "Error/FailedModelValidation",
35+
name: "Error/BundleMissingChannel",
3636
assertion: require.Error,
3737
cfg: DeclarativeConfig{
3838
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
@@ -47,6 +47,38 @@ func TestConvertToModel(t *testing.T) {
4747
Bundles: []Bundle{newTestBundle("foo", "0.1.0", withChannel("alpha", "1"), withChannel("alpha", "2"))},
4848
},
4949
},
50+
{
51+
name: "Error/BundleMissingDefaultChannel",
52+
assertion: require.Error,
53+
cfg: DeclarativeConfig{
54+
Packages: []Package{newTestPackage("foo", "", svgSmallCircle)},
55+
Bundles: []Bundle{newTestBundle("foo", "0.1.0", withChannel("alpha", ""))},
56+
},
57+
},
58+
{
59+
name: "Error/BundleMissingImageAndData",
60+
assertion: require.Error,
61+
cfg: DeclarativeConfig{
62+
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
63+
Bundles: []Bundle{newTestBundle("foo", "0.1.0", withChannel("alpha", ""), withNoBundleImage(), withNoBundleData())},
64+
},
65+
},
66+
{
67+
name: "NoError/BundleMissingProperties",
68+
assertion: require.Error,
69+
cfg: DeclarativeConfig{
70+
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
71+
Bundles: []Bundle{newTestBundle("foo", "0.1.0", withChannel("alpha", ""), withNoProperties())},
72+
},
73+
},
74+
{
75+
name: "NoError/BundleWithDataButMissingImage",
76+
assertion: require.NoError,
77+
cfg: DeclarativeConfig{
78+
Packages: []Package{newTestPackage("foo", "alpha", svgSmallCircle)},
79+
Bundles: []Bundle{newTestBundle("foo", "0.1.0", withChannel("alpha", ""), withNoBundleImage())},
80+
},
81+
},
5082
{
5183
name: "Success/ValidModel",
5284
assertion: require.NoError,

‎internal/declcfg/helpers_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,25 @@ func withSkips(name string) func(*Bundle) {
7979
}
8080
}
8181

82+
func withNoProperties() func(*Bundle) {
83+
return func(b *Bundle) {
84+
b.Properties = []property.Property{}
85+
}
86+
}
87+
88+
func withNoBundleImage() func(*Bundle) {
89+
return func(b *Bundle) {
90+
b.Image = ""
91+
}
92+
}
93+
94+
func withNoBundleData() func(*Bundle) {
95+
return func(b *Bundle) {
96+
b.Objects = []string{}
97+
b.CsvJSON = ""
98+
}
99+
}
100+
82101
func newTestBundle(packageName, version string, opts ...bundleOpt) Bundle {
83102
csvJson := fmt.Sprintf(`{"kind": "ClusterServiceVersion", "apiVersion": "operators.coreos.com/v1alpha1", "metadata":{"name":%q}}`, testBundleName(packageName, version))
84103
b := Bundle{

‎internal/model/model.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ func (m *Package) Validate() error {
5454
result = multierror.Append(result, fmt.Errorf("invalid icon: %v", err))
5555
}
5656

57-
if len(m.Channels) == 0 {
58-
result = multierror.Append(result, fmt.Errorf("package must contain at least one channel"))
59-
}
60-
6157
if m.DefaultChannel == nil {
6258
result = multierror.Append(result, fmt.Errorf("default channel must be set"))
6359
}
6460

61+
if len(m.Channels) == 0 {
62+
result = multierror.Append(result, fmt.Errorf("package must contain at least one channel"))
63+
}
64+
6565
foundDefault := false
6666
for name, ch := range m.Channels {
6767
if name != ch.Name {
@@ -260,6 +260,10 @@ func (b *Bundle) Validate() error {
260260
result = multierror.Append(result, fmt.Errorf("must be exactly one property with type %q", property.TypePackage))
261261
}
262262

263+
if b.Image == "" && len(b.Objects) == 0 {
264+
result = multierror.Append(result, errors.New("bundle image must be set"))
265+
}
266+
263267
return result.ErrorOrNil()
264268
}
265269

‎internal/model/model_test.go

+34-1
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ func TestValidators(t *testing.T) {
362362
Package: pkg,
363363
Channel: ch,
364364
Name: "anakin.v0.1.0",
365-
Image: "",
365+
Image: "registry.io/image",
366366
Replaces: "anakin.v0.0.1",
367367
Skips: []string{"anakin.v0.0.2"},
368368
Properties: []property.Property{
@@ -375,6 +375,39 @@ func TestValidators(t *testing.T) {
375375
},
376376
assertion: require.NoError,
377377
},
378+
{
379+
name: "Bundle/Success/NoBundleImage/HaveBundleData",
380+
v: &Bundle{
381+
Package: pkg,
382+
Channel: ch,
383+
Name: "anakin.v0.1.0",
384+
Image: "",
385+
Properties: []property.Property{
386+
property.MustBuildPackage("anakin", "0.1.0"),
387+
property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
388+
property.MustBuildChannel("light", "anakin.v0.0.1"),
389+
property.MustBuildBundleObjectRef("path/to/data"),
390+
},
391+
Objects: []string{"testdata"},
392+
CsvJSON: "CSVjson",
393+
},
394+
assertion: require.NoError,
395+
},
396+
{
397+
name: "Bundle/Error/NoBundleImage/NoBundleData",
398+
v: &Bundle{
399+
Package: pkg,
400+
Channel: ch,
401+
Name: "anakin.v0.1.0",
402+
Image: "",
403+
Properties: []property.Property{
404+
property.MustBuildPackage("anakin", "0.1.0"),
405+
property.MustBuildGVK("skywalker.me", "v1alpha1", "PodRacer"),
406+
property.MustBuildChannel("light", "anakin.v0.0.1"),
407+
},
408+
},
409+
assertion: require.Error,
410+
},
378411
{
379412
name: "Bundle/Error/NoName",
380413
v: &Bundle{},

‎pkg/lib/config/validate.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import (
1212
// Outputs:
1313
// error: a wrapped error that contains a list of error strings
1414
func ValidateConfig(directory string) error {
15-
// Load config files
15+
// Load config files and convert them to declcfg objects
1616
cfg, err := declcfg.LoadDir(directory)
1717
if err != nil {
1818
return err
1919
}
20-
// Validate the config using model validation
20+
// Validate the config using model validation:
21+
// This will convert declcfg objects to intermediate model objects that are
22+
// also used for serve and add commands. The conversion process will run
23+
// validation for the model objects and ensure they are valid.
2124
_, err = declcfg.ConvertToModel(*cfg)
2225
if err != nil {
2326
return err

0 commit comments

Comments
 (0)
Please sign in to comment.