Skip to content

Commit 1b885c5

Browse files
authored
Merge pull request #618 from dinhxuanvu/declcfg-validate
feat(cmd): Add validate subcommand to opm cmd to validate config files
2 parents bfe5ff3 + a4520ff commit 1b885c5

File tree

8 files changed

+184
-9
lines changed

8 files changed

+184
-9
lines changed

cmd/opm/root/cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/operator-framework/operator-registry/cmd/opm/index"
99
"github.com/operator-framework/operator-registry/cmd/opm/registry"
1010
"github.com/operator-framework/operator-registry/cmd/opm/serve"
11+
"github.com/operator-framework/operator-registry/cmd/opm/validate"
1112
"github.com/operator-framework/operator-registry/cmd/opm/version"
1213
)
1314

@@ -24,7 +25,7 @@ func NewCmd() *cobra.Command {
2425
},
2526
}
2627

27-
cmd.AddCommand(registry.NewOpmRegistryCmd(), alpha.NewCmd(), serve.NewCmd(), newAddCmd())
28+
cmd.AddCommand(registry.NewOpmRegistryCmd(), alpha.NewCmd(), serve.NewCmd(), newAddCmd(), validate.NewCmd())
2829
index.AddCommand(cmd)
2930
version.AddCommand(cmd)
3031

cmd/opm/validate/validate.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package validate
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/sirupsen/logrus"
8+
"github.com/spf13/cobra"
9+
10+
"github.com/operator-framework/operator-registry/pkg/lib/config"
11+
)
12+
13+
func NewCmd() *cobra.Command {
14+
validate := &cobra.Command{
15+
Use: "validate <directory>",
16+
Short: "Validate the declarative index config",
17+
Long: "Validate the declarative config JSON file(s) in a given directory",
18+
Args: cobra.ExactArgs(1),
19+
RunE: configValidate,
20+
}
21+
22+
validate.Flags().BoolP("debug", "d", false, "enable debug log output")
23+
return validate
24+
}
25+
26+
func configValidate(cmd *cobra.Command, args []string) error {
27+
debug, err := cmd.Flags().GetBool("debug")
28+
if err != nil {
29+
return err
30+
}
31+
32+
directory := args[0]
33+
if _, err := os.Stat(directory); os.IsNotExist(err) {
34+
return err
35+
}
36+
37+
logger := logrus.WithField("cmd", "validate")
38+
if debug {
39+
logger.Logger.SetLevel(logrus.DebugLevel)
40+
}
41+
42+
err = config.ValidateConfig(directory)
43+
if err != nil {
44+
logger.Error(err.Error())
45+
return fmt.Errorf("failed to validate config: %s", err)
46+
}
47+
48+
return nil
49+
}

internal/declcfg/declcfg_to_model.go

Lines changed: 9 additions & 1 deletion
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

Lines changed: 34 additions & 2 deletions
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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 8 additions & 4 deletions
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

Lines changed: 34 additions & 1 deletion
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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package config
2+
3+
import (
4+
"github.com/operator-framework/operator-registry/internal/declcfg"
5+
)
6+
7+
// ValidateConfig takes a directory containing the declarative config file(s)
8+
// 1. Validate if declarative config file(s) are valid based on specified schema
9+
// 2. Validate the `replaces` chains of the upgrade graph
10+
// Inputs:
11+
// directory: the directory where declarative config file(s) exist
12+
// Outputs:
13+
// error: a wrapped error that contains a list of error strings
14+
func ValidateConfig(directory string) error {
15+
// Load config files and convert them to declcfg objects
16+
cfg, err := declcfg.LoadDir(directory)
17+
if err != nil {
18+
return err
19+
}
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.
24+
_, err = declcfg.ConvertToModel(*cfg)
25+
if err != nil {
26+
return err
27+
}
28+
return nil
29+
}

0 commit comments

Comments
 (0)