Skip to content

validation library #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Nov 25, 2019
Merged

Conversation

estroz
Copy link
Member

@estroz estroz commented Sep 24, 2019

Moved from dweepgogia/new-manifest-verification.

Implementation of the library described in #2.

Notes:

  • See my comment about api repo structure, which is not followed here but could/should be.
  • Quite a bit more validation logic needs to be ported from operator-courier.

/cc @gallettilance @shawn-hurley @ecordell

dweepgogia and others added 18 commits August 26, 2019 16:20
- Checking CSV yaml for data type mismatch against OLM's CSV type
- Adding Command Line Tool for the library
- To avoid "usage" and "flags" getting printed every time there's an error
- For reporting warnings and errors in case of optional and mandatory fields missing, respectively.
- To enable users to extract more information from an error/warning.
validation logic, refactoring around interface

cmd/verify.go: Verify() name change
- To implement a generic custom error type which is also compatible with upstream.
- To have all the csv checks in one file (csv_validator.go).
- To avoid redundancy while using the cli (operator-verify manifest)
  instead of (operator-verify verify).
- To verify the operator manifest.
- To compare example annotations to the provided APIs list
  and report appropriate errors.
- To check for the manifest format and validate the manifest
  and individual operator files.
- To support validating the CRDs present in the manifest.
- To support validating package yaml present in the manifest.
- To perform validation across manifest files.
@openshift-ci-robot openshift-ci-robot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Sep 24, 2019
Copy link
Member

@awgreene awgreene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A number of nits - but the logic seems good.

"os"

manifests "github.com/operator-framework/api/cmd/operator-verify/manifests"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this newline necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but I prefer to separate intra-repo from extra-repo imports. I can merge them all if you prefer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In OLM we use goimports which actually does something similar, just with the position of the intra/extra imports flipped from what you have here. Do we want to make it the org wide formatter?

"os"

"github.com/operator-framework/api/pkg/manifests"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this newline necessary?

seen := map[string]struct{}{}
for i, c := range pkg.Channels {
if c.Name == "" {
errs = append(errs, errors.ErrInvalidPackageManifest(fmt.Sprintf("channel %d name is empty", i), pkg.PackageName))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Sprintf("channel %d name is empty", i), pkg.PackageName) isn't i just a integer for each channel?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, here I assume there's no other identifying information for a channel so channel number is the only piece of information available to identify which channel is invalid.

Copy link
Member

@njhale njhale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this! It's looking good so far. I was only able to get half way through, but I figured it was a good place to stop and get some questions out.

@openshift-ci-robot openshift-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Sep 26, 2019
go.mod Outdated
github.com/ghodss/yaml v1.0.0
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/operator-framework/operator-lifecycle-manager v0.0.0-20190725173916-b56e63a643cc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid the OLM dependency? Eventually all the types could live in this repo anyway, and it would help to avoid circular dependency issues when OLM inevitably imports validation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have to duplicate the ClusterServiceVersion and related types here to remove this dependency. Are you ok with this?

We may have to do the same thing for operator-registry (registry.Bundle and some parsing types).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the initial idea. Ideally, as soon as we moved something to this repo, we'd switch over in OLM. That whole plan has been put on hold since it makes releasing API changes more difficult; e.g. we would need to release API changes ahead of the controller logic that handles them.

@ecordell, I think we need to make a decision ASAP to either move validation to its own repo or move the OLM APIs here.

func (l *manifestsLoad) populate() error {
dl := sqlite.NewSQLLoaderForDirectory(l, l.dir)
if err := dl.Populate(); err != nil {
return errors.Wrapf(err, "error getting bundles from manifests dir %q", l.dir)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In OLM we actually do the opposite -- but we could totally review that convention if you want @awgreene

}

// AddOperatorBundle adds the package manifest to l.
func (l *manifestsLoad) AddPackageChannels(pkg registry.PackageManifest) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type needs to implement this exact method signature in order to implement the registry.Load interface, which includes the error in case an implementation needs to report that the operation failed in some way.

@njhale
Copy link
Member

njhale commented Oct 28, 2019

@kevinrizza @ecordell, did we decide if we're abandoning this repo? If so, we should make plans to move this PR into its own validation repo or OLM proper.

(hopefully this question doesn't rekindle the entire conversation...)

@njhale njhale mentioned this pull request Oct 28, 2019
@kevinrizza
Copy link
Member

@kevinrizza @ecordell, did we decide if we're abandoning this repo? If so, we should make plans to move this PR into its own validation repo or OLM proper.

(hopefully this question doesn't rekindle the entire conversation...)

There is an active effort to create an API version for the entire operator-framework. I think once that conversation gets further along we may end up wanting to merge this (either in the current state or otherwise) so for now maybe let's just leave it open and reevaluate later?

@njhale
Copy link
Member

njhale commented Oct 28, 2019

There is an active effort to create an API version for the entire operator-framework. I think once that conversation gets further along we may end up wanting to merge this (either in the current state or otherwise) so for now maybe let's just leave it open and reevaluate later?

Sure, we can leave this here for now -- my comment was spurred by the decision to place net-new OLM APIs directly in its repository (instead of here).

@estroz
Copy link
Member Author

estroz commented Oct 30, 2019

@njhale @awgreene comments addressed, PTAL

Copy link
Member

@njhale njhale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for your patience. I'm going to be in transit soon, so I decided to post the first half of the review in advance. It's looking solid so far, mostly just nits and questions

go.mod Outdated
github.com/ghodss/yaml v1.0.0
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/operator-framework/operator-lifecycle-manager v0.0.0-20190725173916-b56e63a643cc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the initial idea. Ideally, as soon as we moved something to this repo, we'd switch over in OLM. That whole plan has been put on hold since it makes releasing API changes more difficult; e.g. we would need to release API changes ahead of the controller logic that handles them.

@ecordell, I think we need to make a decision ASAP to either move validation to its own repo or move the OLM APIs here.

return l.pkg
}

func (l manifests) GetBundles() (bundles []*registry.Bundle) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was probably for convenience since the method receivers are of type *registry.Bundle. This lets us avoid needing to get the pointer for each element every time we want to call a method.

"k8s.io/apimachinery/pkg/util/yaml"
)

type CSVValidator struct{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If you make a function type that implements Validator, then you won't need this empty struct. A good example is in the go standard library; see https://golang.org/src/net/http/server.go?s=61509:61556#L1993

Copy link
Member Author

@estroz estroz Nov 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So something like

type csvv1alpha1Validator func(*v1alpha1.ClusterServiceVersion) errors.ManifestResult

func (f csvv1alpha1Validator) Validate(objs ...interface{}) (results []errors.ManifestResult) {
	for _, obj := range objs {
		switch v := obj.(type) {
		case *v1alpha1.ClusterServiceVersion:
			results = append(results, f(v))
		}
	}
	return results
}

Then export functions that actually validate the CSV object:

var Validatev1alpha1CSV csvv1alpha1Validator = func(*v1alpha1.ClusterServiceVersion) errors.ManifestResult {
    ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost. What I'm proposing just allows functions to implement the Validator interface:

type ValidatorFunc Validate(...interface{}) []errors.ManifestResult

func (v ValidatorFunc) Validate(objs ...interface{}) (results []errors.ManifestResult) {
    return v(objs...)
}

var CSVValidator Validator = ValidatorFunc(validateCSVs)

func validateCSVs(objs ...interface{}) (results []errors.ManifestResult) {
    for _, obj := range objs {
		switch v := obj.(type) {
		case *v1alpha1.ClusterServiceVersion:
			results = append(results, validateCSV(v))
		case *registry.ClusterServiceVersion:
			results = append(results, validateCSVRegistry(v))
		}
	}
	return results
}


// Omitted field tags will contain ",omitempty", and ignored tags will
// match "-" exactly, respectively.
isOptionalField := strings.Contains(tag, ",omitempty") || tag == "-"
Copy link
Member

@njhale njhale Nov 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow-up: If we could somehow get the OpenAPI spec for CRDs not included in the bundle, could we use a pre-existing schema validator to handle checking for missing required fields?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on what you mean by "OpenAPI spec for CRDs not included in the bundle" a bit? What manifest kind are we checking for missing fields in the context of your question?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One example would be the OpenAPI spec for a Deployment. We would validate the manifest against the OpenAPI schema, which would tell us when fields are missing (due to required directives, etc...). Does that make more sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that does. I will implement this in a follow-up.

@estroz estroz force-pushed the validator-interface branch from f23e367 to 689a491 Compare November 7, 2019 00:27
Copy link
Member

@gallettilance gallettilance left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!!
Just a few questions (could be a follow up PR). Also we still need to resolve where the OLM APIs live


## `pkg/validation`: Operator Manifest Validation

`pkg/validation` exposes a convenient set of interfaces to validate Kubernetes object manifests, primarily for use in an Operator project.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of adding some examples? This could be in a separate PR/folder

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking examples of implementing validators / using the apis

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might make a nice follow-up.

Short: "Operator manifest validation tool",
Long: `operator-verify is a CLI tool that calls functions in pkg/validation.`,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think of adding an option to output the resulting errors/warnings to a log file - I think this was a use case that Ralph or Scott had with operator courier

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this can be a follow-up.

}

// AddBundlePackageChannels is a no-op to implement the registry.Load interface.
func (l *manifestsLoad) AddBundlePackageChannels(manifest registry.PackageManifest, bundle registry.Bundle) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: As we change operator-registry we'll need to be aware of the dependency here on the implementation of its Load interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general we're going to have to do something about operator-registry types. If we want this to be a dependency of operator-registry, we're either going to have to copy over types or copy over parse code and use internal types. WDYT? We can probably clean this up in a follow-up PR.

Copy link
Member

@njhale njhale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the rest of my review. Mostly follow-ups and nits:

"github.com/operator-framework/operator-registry/pkg/registry"
)

type ManifestsValidator struct{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PackageUpdateGraphValidator?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this is a fairer name given this validator's current logic. However I'd rather keep the name general in case the constituent manifest types of a manifests directory are changed, or other, non-update graph validation logic is added in the future. How about VersionedManifestSetValidator or ManifestsDirValidator?

Alternatively we could add future logic as described above in another validator and reserve this one for validating the update graph.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could add future logic as described above in another validator and reserve this one for validating the update graph.

This is more where my head is at, since I view validators as composable units that form larger validators (if Validators implemented Validator). I can also see where it can be preferable to make larger units to avoid duplication of work or handle interdependencies. If that's the case, then either of the names you've suggested are fine with me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ehh because this code is only validating the update graph right now, we can name this PackageUpdateGraphValidator and add another validator later. These names are subject to change since they're internal types.

@estroz estroz force-pushed the validator-interface branch from 4a3cc94 to 02fc1ea Compare November 9, 2019 02:46
errors.ManifestResult.

pkg/validation/internal: default validator implementations

pkg/validation/errors: result and error types for validators

pkg/internal: bundle parser

pkg/manifests: GetManifestsDir() parses and validates a directory
of Operator manifests

*: update import paths to operator-framework/api

go.*: revendor

README.md: concise description of pkg/validation
@estroz estroz force-pushed the validator-interface branch from 02fc1ea to 439c769 Compare November 9, 2019 03:01
Copy link
Member

@njhale njhale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's super close. I just don't want to merge it now only to move it to another repo immediately after. A big chunk of our team is out, so I'll confer with @shawn-hurley and @dmesser soon to settle things.

"k8s.io/apimachinery/pkg/util/yaml"
)

type CSVValidator struct{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost. What I'm proposing just allows functions to implement the Validator interface:

type ValidatorFunc Validate(...interface{}) []errors.ManifestResult

func (v ValidatorFunc) Validate(objs ...interface{}) (results []errors.ManifestResult) {
    return v(objs...)
}

var CSVValidator Validator = ValidatorFunc(validateCSVs)

func validateCSVs(objs ...interface{}) (results []errors.ManifestResult) {
    for _, obj := range objs {
		switch v := obj.(type) {
		case *v1alpha1.ClusterServiceVersion:
			results = append(results, validateCSV(v))
		case *registry.ClusterServiceVersion:
			results = append(results, validateCSVRegistry(v))
		}
	}
	return results
}

"github.com/operator-framework/operator-registry/pkg/registry"
)

type ManifestsValidator struct{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could add future logic as described above in another validator and reserve this one for validating the update graph.

This is more where my head is at, since I view validators as composable units that form larger validators (if Validators implemented Validator). I can also see where it can be preferable to make larger units to avoid duplication of work or handle interdependencies. If that's the case, then either of the names you've suggested are fine with me.

…pend non-default Validators to the Validator; ValidatorFunc implements Validator
@estroz estroz changed the title [WIP] validation library validation library Nov 15, 2019
@openshift-ci-robot openshift-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Nov 15, 2019
@estroz estroz requested review from njhale and awgreene November 15, 2019 18:39
@njhale
Copy link
Member

njhale commented Nov 25, 2019

/hold cancel
/lgtm

@openshift-ci-robot openshift-ci-robot added lgtm Indicates that a PR is ready to be merged. and removed do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. labels Nov 25, 2019
@njhale njhale merged commit 94d8034 into operator-framework:master Nov 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lgtm Indicates that a PR is ready to be merged. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants