Skip to content

Commit

Permalink
bundle validate: implement bundle content validation to extract warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
estroz committed May 22, 2020
1 parent 35d5162 commit 0b4b9be
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 56 deletions.
150 changes: 97 additions & 53 deletions cmd/operator-sdk/bundle/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import (
"os"
"path/filepath"

"github.com/operator-framework/operator-sdk/internal/flags"

"github.com/operator-framework/operator-registry/pkg/lib/bundle"
apimanifests "github.com/operator-framework/api/pkg/manifests"
apierrors "github.com/operator-framework/api/pkg/validation/errors"
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"

"github.com/operator-framework/operator-sdk/internal/flags"
internalregistry "github.com/operator-framework/operator-sdk/internal/registry"
)

type bundleValidateCmd struct {
Expand All @@ -42,8 +45,9 @@ func newValidateCmd() *cobra.Command {
Short: "Validate an operator bundle image",
Long: `The 'operator-sdk bundle validate' command can validate both content and
format of an operator bundle image or an operator bundles directory on-disk
containing operator metadata and manifests. This command will exit with a non-zero
exit code if any validation tests fail.
containing operator metadata and manifests. This command will exit with an
exit code of 1 if any validation errors arise, and 0 if only warnings arise or
all validators pass.
More information on operator bundle images and the manifests/metadata format:
https://github.com/openshift/enhancements/blob/master/enhancements/olm/operator-bundle.md
Expand Down Expand Up @@ -72,20 +76,43 @@ To build and validate an image:
`,
RunE: func(cmd *cobra.Command, args []string) (err error) {
if err = c.validate(args); err != nil {
return fmt.Errorf("error validating args: %v", err)
if viper.GetBool(flags.VerboseOpt) {
log.SetLevel(log.DebugLevel)
}

if err := c.validate(args); err != nil {
return fmt.Errorf("invalid command args: %v", err)
}

// If the argument isn't a directory, assume it's an image.
if isExist(args[0]) {
if c.directory, err = relWd(args[0]); err != nil {
log.Fatal(err)
}
} else {
c.imageTag = args[0]
c.directory, err = ioutil.TempDir("", "bundle-")
if err != nil {
return err
}
defer func() {
if err = os.RemoveAll(c.directory); err != nil {
log.Errorf("Error removing temp bundle dir: %v", err)
}
}()

log.Info("Unpacking image layers")

if err := c.unpackImageIntoDir(args[0], c.directory); err != nil {
log.Fatalf("Error unpacking image %s: %v", args[0], err)
}
}

if err = c.run(); err != nil {
log.Fatal(err)
}

log.Info("All validation tests have completed successfully")

return nil
},
}
Expand All @@ -102,63 +129,80 @@ func (c bundleValidateCmd) validate(args []string) error {
return nil
}

// TODO: add a "permissive" flag to toggle whether warnings also cause a non-zero
// exit code to be returned (true by default).
func (c *bundleValidateCmd) addToFlagSet(fs *pflag.FlagSet) {
fs.StringVarP(&c.imageBuilder, "image-builder", "b", "docker",
"Tool to extract container images. One of: [docker, podman]")
"Tool to extract bundle image data. Only used when validating a bundle image. "+
"One of: [docker, podman]")
}

func (c bundleValidateCmd) run() (err error) {
// Set directory, either supplied directly or a temp dir used to unpack
// the image.
dir := c.directory
if c.imageTag != "" {
dir, err = ioutil.TempDir("", "bundle-")
if err != nil {
return err
}
defer func() {
if err = os.RemoveAll(dir); err != nil {
log.Errorf("Error removing temp bundle dir: %v", err)
}
}()
}
if dir, err = filepath.Abs(dir); err != nil {
return err
}
func (c bundleValidateCmd) run() error {

logger := log.WithFields(log.Fields{
"bundle-dir": c.directory,
"container-tool": c.imageBuilder,
})
val := registrybundle.NewImageValidator(c.imageBuilder, logger)

// Set up logger.
fields := log.Fields{"bundle-dir": dir}
if c.imageTag != "" {
fields["container-tool"] = c.imageBuilder
// Validate bundle format.
if err := val.ValidateBundleFormat(c.directory); err != nil {
return fmt.Errorf("invalid bundle format: %v", err)
}
logger := log.WithFields(fields)
if viper.GetBool(flags.VerboseOpt) {
log.SetLevel(log.DebugLevel)

// Validate bundle content.
// TODO(estroz): instead of using hard-coded 'manifests', look up bundle
// dir name in metadata labels.
manifestsDir := filepath.Join(c.directory, registrybundle.ManifestsDir)
if results, err := validateBundleContent(logger, manifestsDir); err != nil {
return fmt.Errorf("error validating %s: %v", manifestsDir, err)
} else if checkResults(results) {
return errors.New("invalid bundle content")
}

val := bundle.NewImageValidator(c.imageBuilder, logger)
return nil
}

// Pull image if a tag was passed.
if c.imageTag != "" {
logger.Info("Unpacked image layers")
err = val.PullBundleImage(c.imageTag, dir)
if err != nil {
logger.Fatalf("Error to unpacking image: %v", err)
}
}
// unpackImageIntoDir writes files in image layers found in image imageTag to dir.
func (c bundleValidateCmd) unpackImageIntoDir(imageTag, dir string) error {
logger := log.WithFields(log.Fields{
"bundle-dir": dir,
"container-tool": c.imageBuilder,
})
val := registrybundle.NewImageValidator(c.imageBuilder, logger)

// Validate bundle format.
if err = val.ValidateBundleFormat(dir); err != nil {
logger.Fatalf("Bundle format validation failed: %v", err)
}
return val.PullBundleImage(imageTag, dir)
}

// Validate bundle content.
manifestsDir := filepath.Join(dir, bundle.ManifestsDir)
if err = val.ValidateBundleContent(manifestsDir); err != nil {
logger.Fatalf("Bundle content validation failed: %v", err)
// validateBundleContent validates a bundle in manifestsDir.
func validateBundleContent(logger *log.Entry, manifestsDir string) ([]apierrors.ManifestResult, error) {
// Detect mediaType.
mediaType, err := registrybundle.GetMediaType(manifestsDir)
if err != nil {
return nil, err
}
// Read the bundle.
bundle, err := apimanifests.GetBundleFromDir(manifestsDir)
if err != nil {
return nil, err
}

logger.Info("All validation tests have completed successfully")
return internalregistry.ValidateBundleContent(logger, bundle, mediaType), nil
}

return nil
// checkResults logs warnings and errors in results, and returns true if at
// least one error was encountered.
func checkResults(results []apierrors.ManifestResult) (hasErrors bool) {
for _, r := range results {
for _, w := range r.Warnings {
log.Warnf("%s validation: [%s] %s", r.Name, w.Type, w.Detail)
}
for _, e := range r.Errors {
log.Errorf("%s validation: [%s] %s", r.Name, e.Type, e.Detail)
}
if r.HasError() {
hasErrors = true
}
}
return hasErrors
}
129 changes: 129 additions & 0 deletions internal/registry/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2020 The Operator-SDK Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package registry

import (
"fmt"

apimanifests "github.com/operator-framework/api/pkg/manifests"
apivalidation "github.com/operator-framework/api/pkg/validation"
apierrors "github.com/operator-framework/api/pkg/validation/errors"
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
log "github.com/sirupsen/logrus"
k8svalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)

// ValidateBundleContent confirms that the CSV and CRD files inside the bundle
// directory are valid and can be installed in a cluster. Other GVK types are
// also validated to confirm if they are "kubectl-able" to a cluster meaning
// if they can be applied to a cluster using `kubectl` provided users have all
// necessary permissions and configurations.
func ValidateBundleContent(logger *log.Entry, bundle *apimanifests.Bundle,
mediaType string) []apierrors.ManifestResult {

// Use errs to collect bundle-level validation errors.
errs := apierrors.ManifestResult{
Name: bundle.Name,
}

logger.Debug("Validating bundle contents")

// helm+vX media types are not supported by this validation function.
switch mediaType {
case registrybundle.HelmType:
return []apierrors.ManifestResult{errs}
}

for _, u := range bundle.Objects {
// CSVs and CRDs will be validated separately.
gvk := u.GetObjectKind().GroupVersionKind()
if gvk.Kind == "ClusterServiceVersion" || gvk.Kind == "CustomResourceDefinition" {
continue
}

logger.Debugf("Validating %s %q", gvk, u.GetName())

// Verify if the object kind is supported for registry+v1 format.
supported, _ := registrybundle.IsSupported(gvk.Kind)
if mediaType == registrybundle.RegistryV1Type && !supported {
errs.Add(apierrors.ErrInvalidBundle(fmt.Sprintf("unsupported media type %s for bundle object", mediaType), gvk))
continue
}

if err := validateObject(metav1.Object(u)); err != nil {
errs.Add(apierrors.ErrFailedValidation(err.Error(), u.GetName()))
}
}

// Validate bundle itself.
results := apivalidation.BundleValidator.Validate(bundle)

// All bundles must have a CSV currently.
if bundle.CSV != nil {
results = append(results, apivalidation.ClusterServiceVersionValidator.Validate(bundle.CSV)...)
} else {
errs.Add(apierrors.ErrInvalidBundle("no ClusterServiceVersion in bundle", bundle.Name))
}

// Validate all CRD versions in the bundle together.
var crds []interface{}
for _, crd := range bundle.V1beta1CRDs {
crds = append(crds, crd)
}
for _, crd := range bundle.V1CRDs {
crds = append(crds, crd)
}
if len(crds) != 0 {
results = append(results, apivalidation.CustomResourceDefinitionValidator.Validate(crds...)...)
}

// Add all other results/errors to the bundle validation results.
results = appendResult(results, errs)

return results
}

// validateObject validates an arbitrary metav1.Object's metadata.
func validateObject(obj metav1.Object) error {
f := func(string, bool) []string { return nil }
errs := k8svalidation.ValidateObjectMetaAccessor(obj, false, f, field.NewPath("metadata"))
if len(errs) > 0 {
return fmt.Errorf("error validating object: %s. %v", errs.ToAggregate(), obj)
}
return nil
}

// appendResult attempts to find a result in results that matches r.Name, and
// if found appends errors and warnings to that result. Otherwise r is added
// to the end of results.
func appendResult(results []apierrors.ManifestResult, r apierrors.ManifestResult) []apierrors.ManifestResult {
resultIdx := -1
for i, result := range results {
if result.Name == r.Name {
resultIdx = i
break
}
}
if resultIdx < 0 {
results = append(results, r)
} else {
results[resultIdx].Add(r.Errors...)
results[resultIdx].Add(r.Warnings...)
}

return results
}
7 changes: 4 additions & 3 deletions website/content/en/docs/cli/operator-sdk_bundle_validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Validate an operator bundle image

The 'operator-sdk bundle validate' command can validate both content and
format of an operator bundle image or an operator bundles directory on-disk
containing operator metadata and manifests. This command will exit with a non-zero
exit code if any validation tests fail.
containing operator metadata and manifests. This command will exit with an
exit code of 1 if any validation errors arise, and 0 if only warnings arise or
all validators pass.

More information on operator bundle images and the manifests/metadata format:
https://github.com/openshift/enhancements/blob/master/enhancements/olm/operator-bundle.md
Expand Down Expand Up @@ -52,7 +53,7 @@ To build and validate an image:

```
-h, --help help for validate
-b, --image-builder string Tool to extract container images. One of: [docker, podman] (default "docker")
-b, --image-builder string Tool to extract bundle image data. Only used when validating a bundle image. One of: [docker, podman] (default "docker")
```

### SEE ALSO
Expand Down

0 comments on commit 0b4b9be

Please sign in to comment.