Skip to content

Commit d3f11e9

Browse files
Add custom validator tutoral (operator-framework#5881)
* Add custom validator tutoral Covers the `--alpha-select-external` usage, writing a custom validator from scratch, and how to use validations in `operator-framework/api` at arbitrary versions. Signed-off-by: Austin Macdonald <[email protected]> * oopsies Signed-off-by: Austin Macdonald <[email protected]> * more PR review Signed-off-by: Austin Macdonald <[email protected]> * Update website/content/en/docs/advanced-topics/custom-bundle-validation.md Co-authored-by: Rashmi Gottipati <[email protected]> Signed-off-by: Austin Macdonald <[email protected]> * more links Signed-off-by: Austin Macdonald <[email protected]> Co-authored-by: Rashmi Gottipati <[email protected]>
1 parent 121fac5 commit d3f11e9

File tree

2 files changed

+317
-1
lines changed

2 files changed

+317
-1
lines changed

Diff for: .gitmodules

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[submodule "website/themes/docsy"]
22
path = website/themes/docsy
3-
url = https://github.com/asmacdo/docsy.git
3+
url = https://github.com/asmacdo/docsy.git
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
---
2+
title: Custom Bundle Validation
3+
weight: 80
4+
---
5+
6+
## Summary
7+
8+
Operator authors can now use "external" validators with the
9+
`operator-sdk bundle validate` command by using the
10+
`--alpha-select-external` flag. This feature enables Operator authors,
11+
users, and registry pipelines to use custom validators. These custom
12+
validators can be written in any language.
13+
14+
## Usage
15+
16+
External validators can be used by specifying a list of local filepaths to
17+
executables using colons as path separators:
18+
19+
```sh
20+
$ operator-sdk bundle validate \
21+
--alpha-select-external path/validator1:path/validator2
22+
```
23+
24+
## Writing a Custom Validator
25+
26+
For a validator to work with `operator-sdk bundle validate` each of the files must:
27+
1. Be executable with appropriate permissions
28+
1. Return JSON to STDOUT in the [`ManifestResult`][manifest_result] format.
29+
30+
### Custom Validator from Scratch
31+
32+
Using the `errors` package from [`operator-framework/api`][of-api], we
33+
can start by validating the correct number of arguments and marshaling a
34+
[`ManifestResult`][manifest_result] into STDOUT.
35+
36+
`myvalidator/main.go`
37+
38+
```go
39+
package main
40+
41+
import (
42+
"encoding/json"
43+
"fmt"
44+
"os"
45+
46+
"github.com/operator-framework/api/pkg/validation/errors"
47+
)
48+
49+
func main() {
50+
51+
// we expect a single argument which is the bundle root.
52+
// usage: validator-poc <bundle root>
53+
if len(os.Args) < 2 {
54+
fmt.Printf("usage: %s <bundle root>\n", os.Args[0])
55+
os.Exit(1)
56+
}
57+
58+
var validatorErrors []errors.Error
59+
var validatorWarnings []errors.Error
60+
result := errors.ManifestResult{
61+
Name: "Always Green Example",
62+
Errors: validatorErrors,
63+
Warnings: validatorWarnings,
64+
}
65+
prettyJSON, err := json.MarshalIndent(result, "", " ")
66+
if err != nil {
67+
fmt.Println("Invalid json")
68+
os.Exit(1)
69+
}
70+
fmt.Printf("%s\n", string(prettyJSON))
71+
}
72+
```
73+
74+
When executed on its own, this validator prints a JSON representation of
75+
[`ManifestResult`][manifest_result].
76+
77+
```sh
78+
go build -o myvalidator/main myvalidator/main.go && ./myvalidator/main
79+
80+
{
81+
"Name": "Always Green Example",
82+
"Errors": null,
83+
"Warnings": null
84+
}
85+
```
86+
87+
```sh
88+
$ go build -o myvalidator/main myvalidator/main.go
89+
$ operator-sdk bundle validate ./bundle --alpha-select-external ./myvalidator/main
90+
```
91+
```
92+
INFO[0000] All validation tests have completed successfully
93+
```
94+
95+
From here, custom validator authors can read in the bundle and make any
96+
assertions necessary.
97+
98+
Errors and Warnings are both implementations of the `error` interface
99+
and need `ErrorType`, `Level`, `Field`, `BadValue`, and `Detail`, which
100+
are all initialized by arbitrary strings. When using Golang, validator
101+
authors can use the [operator-framework/api][of-api] impementation of
102+
[errors and warnings][errors-pkg]
103+
104+
```go
105+
validatorErrors = []errors.Error{errors.Error{"someErrorType", "somelevel", "somefield", "somebadvalue", "somedetail"}}
106+
validatorWarnings = []errors.Error{errors.Error{"someWarningType", "somelevel", "somefield", "somebadvalue", "somedetail"}}
107+
```
108+
109+
We can now rebuild and run the validator, which now shows errors.
110+
111+
```sh
112+
$ go build -o myvalidator/main myvalidator/main.go
113+
$ operator-sdk bundle validate ./bundle --alpha-select-external ./myvalidator/main
114+
```
115+
```
116+
WARN[0000] somelevel: Field somefield, Value somebadvalue: somedetail
117+
ERRO[0000] somelevel: Field somefield, Value somebadvalue: somedetail
118+
```
119+
120+
### Composing Validators
121+
122+
For users wishing to use validators from
123+
[`operator-framework/api`][of-api] without being restricted to the
124+
version that is built into the `operator-sdk` binary, it is possible to
125+
create a `main.go` that makes use of the [`validation`
126+
package][of-validation] at an arbitrary version.
127+
128+
Currently, some of the code necessary requires copying code from
129+
internal packages, which may someday become a library.
130+
131+
`myvalidator/main.go`
132+
133+
```go
134+
package main
135+
136+
import (
137+
"encoding/json"
138+
"errors"
139+
"fmt"
140+
"os"
141+
"path/filepath"
142+
"strings"
143+
144+
apimanifests "github.com/operator-framework/api/pkg/manifests"
145+
apivalidation "github.com/operator-framework/api/pkg/validation"
146+
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
147+
log "github.com/sirupsen/logrus"
148+
149+
"github.com/spf13/afero"
150+
"sigs.k8s.io/yaml"
151+
)
152+
153+
func main() {
154+
155+
// we expect a single argument which is the bundle root.
156+
// usage: validator-poc <bundle root>
157+
if len(os.Args) < 2 {
158+
fmt.Printf("usage: %s <bundle root>\n", os.Args[0])
159+
os.Exit(1)
160+
}
161+
162+
// Read the bundle object and metadata from the passed in directory.
163+
bundle, _, err := getBundleDataFromDir(os.Args[1])
164+
if err != nil {
165+
fmt.Printf("problem getting bundle [%s] data, %v\n", os.Args[1], err)
166+
os.Exit(1)
167+
}
168+
169+
// pass the objects to the validator
170+
objs := bundle.ObjectsToValidate()
171+
for _, obj := range bundle.Objects {
172+
objs = append(objs, obj)
173+
}
174+
results := apivalidation.GoodPracticesValidator.Validate(objs...)
175+
176+
// take each of the ManifestResults and print to STDOUT
177+
for _, result := range results {
178+
prettyJSON, err := json.MarshalIndent(result, "", " ")
179+
if err != nil {
180+
// should output JSON so that the call knows how to parse it
181+
fmt.Printf("XXX ERROR: %v\n", err)
182+
}
183+
fmt.Printf("%s\n", string(prettyJSON))
184+
}
185+
}
186+
187+
// getBundleDataFromDir returns the bundle object and associated metadata from dir, if any.
188+
func getBundleDataFromDir(dir string) (*apimanifests.Bundle, string, error) {
189+
// Gather bundle metadata.
190+
metadata, _, err := FindBundleMetadata(dir)
191+
if err != nil {
192+
return nil, "", err
193+
}
194+
manifestsDirName, hasLabel := metadata.GetManifestsDir()
195+
if !hasLabel {
196+
manifestsDirName = registrybundle.ManifestsDir
197+
}
198+
manifestsDir := filepath.Join(dir, manifestsDirName)
199+
// Detect mediaType.
200+
mediaType, err := registrybundle.GetMediaType(manifestsDir)
201+
if err != nil {
202+
return nil, "", err
203+
}
204+
// Read the bundle.
205+
bundle, err := apimanifests.GetBundleFromDir(manifestsDir)
206+
if err != nil {
207+
return nil, "", err
208+
}
209+
return bundle, mediaType, nil
210+
}
211+
212+
// -------------------------------------------------------
213+
// Everything below this line was copied code from the internal Operator SDK
214+
// registry package operator-sdk/internal/registry/labels.go. If this is
215+
// generally useful please file an issue to move this to a reuable library.
216+
// to make this a library or other reusable code.
217+
// -------------------------------------------------------
218+
219+
type MetadataNotFoundError string
220+
221+
func (e MetadataNotFoundError) Error() string {
222+
return fmt.Sprintf("metadata not found in %s", string(e))
223+
}
224+
225+
// Labels is a set of key:value labels from an operator-registry object.
226+
type Labels map[string]string
227+
228+
// GetManifestsDir returns the manifests directory name in ls using
229+
// a predefined key, or false if it does not exist.
230+
func (ls Labels) GetManifestsDir() (string, bool) {
231+
value, hasKey := ls[registrybundle.ManifestsLabel]
232+
return filepath.Clean(value), hasKey
233+
}
234+
235+
// FindBundleMetadata walks bundleRoot searching for metadata (ex. annotations.yaml),
236+
// and returns metadata and its path if found. If one is not found, an error is returned.
237+
func FindBundleMetadata(bundleRoot string) (Labels, string, error) {
238+
return findBundleMetadata(afero.NewOsFs(), bundleRoot)
239+
}
240+
241+
func findBundleMetadata(fs afero.Fs, bundleRoot string) (Labels, string, error) {
242+
// Check the default path first, and return annotations if they were found or an error if that error
243+
// is not because the path does not exist (it exists or there was an unmarshalling error).
244+
annotationsPath := filepath.Join(bundleRoot, registrybundle.MetadataDir, registrybundle.AnnotationsFile)
245+
annotations, err := readAnnotations(fs, annotationsPath)
246+
if (err == nil && len(annotations) != 0) || (err != nil && !errors.Is(err, os.ErrNotExist)) {
247+
return annotations, annotationsPath, err
248+
}
249+
250+
// Annotations are not at the default path, so search recursively.
251+
annotations = make(Labels)
252+
annotationsPath = ""
253+
err = afero.Walk(fs, bundleRoot, func(path string, info os.FileInfo, err error) error {
254+
if err != nil {
255+
return err
256+
}
257+
// Skip directories and hidden files, or if annotations were already found.
258+
if len(annotations) != 0 || info.IsDir() || strings.HasPrefix(path, ".") {
259+
return nil
260+
}
261+
262+
annotationsPath = path
263+
// Ignore this error, since we only care if any annotations are returned.
264+
if annotations, err = readAnnotations(fs, path); err != nil {
265+
log.Debug(err)
266+
}
267+
return nil
268+
})
269+
if err != nil {
270+
return nil, "", err
271+
}
272+
273+
if len(annotations) == 0 {
274+
return nil, "", MetadataNotFoundError(bundleRoot)
275+
}
276+
277+
return annotations, annotationsPath, nil
278+
}
279+
280+
// readAnnotations reads annotations from file(s) in bundleRoot and returns them as Labels.
281+
func readAnnotations(fs afero.Fs, annotationsPath string) (Labels, error) {
282+
// The annotations file is well-defined.
283+
b, err := afero.ReadFile(fs, annotationsPath)
284+
if err != nil {
285+
return nil, err
286+
}
287+
288+
// Use the arbitrarily-labelled bundle representation of the annotations file
289+
// for forwards and backwards compatibility.
290+
annotations := registrybundle.AnnotationMetadata{
291+
Annotations: make(Labels),
292+
}
293+
if err = yaml.Unmarshal(b, &annotations); err != nil {
294+
return nil, fmt.Errorf("error unmarshalling potential bundle metadata %s: %v", annotationsPath, err)
295+
}
296+
297+
return annotations.Annotations, nil
298+
}
299+
```
300+
301+
The `main.go` is then built into a binary and used with `operator-sdk bundle
302+
validate`
303+
304+
```sh
305+
$ go build -o myvalidator/main myvalidator/main.go
306+
$ operator-sdk bundle validate ./bundle --alpha-select-external ./myvalidator/main
307+
```
308+
```
309+
WARN[0000] Warning: Value sandbox-op.v0.0.1: owned CRD "sandboxes.sandbox.example.come" has an empty description
310+
INFO[0000] All validation tests have completed successfully
311+
```
312+
[errors-pkg]: https://github.com/operator-framework/api/pkg/tree/master/validation/errors
313+
[manifest_result]: https://github.com/operator-framework/api/blob/master/pkg/validation/errors/error.go#L9-L16
314+
[of-api]: https://github.com/operator-framework/api
315+
[of-validation]: https://github.com/operator-framework/api/tree/master/pkg/validation

0 commit comments

Comments
 (0)