diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..36c126c2c --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ + +default: help + +help: + @echo "Usage: make " + @echo + @echo " * 'fmt' - format the json with indentation" + @echo " * 'validate' - build the validation tool" + +fmt: + for i in *.json ; do jq --indent 2 -M . "$${i}" > xx && cat xx > "$${i}" && rm xx ; done + +.PHONY: validate-examples +validate-examples: oci-validate-examples + ./oci-validate-examples < manifest.md + +oci-validate-json: validate.go + go build ./cmd/oci-validate-json + +oci-validate-examples: cmd/oci-validate-examples/main.go + go build ./cmd/oci-validate-examples + diff --git a/cmd/oci-validate-examples/main.go b/cmd/oci-validate-examples/main.go new file mode 100644 index 000000000..5a2d7b768 --- /dev/null +++ b/cmd/oci-validate-examples/main.go @@ -0,0 +1,208 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/russross/blackfriday" + "github.com/xeipuuv/gojsonschema" +) + +var ( + verbose bool + skipped bool + schemaPath string +) + +func init() { + flag.BoolVar(&verbose, "verbose", false, "display examples no matter what") + flag.BoolVar(&skipped, "skipped", false, "show skipped examples") + flag.StringVar(&schemaPath, "schema", "./schema", "specify location of schema directory") +} + +func main() { + flag.Parse() + + examples, err := extractExamples(os.Stdin) + if err != nil { + log.Fatalln(err) + } + var fail bool + for _, example := range examples { + if example.Err != nil { + printFields("error", example.Mediatype, example.Title, example.Err) + fail = true + continue + } + + schema, err := schemaByMediatype(schemaPath, example.Mediatype) + if err != nil { + if err == errSchemaNotFound { + if skipped { + printFields("skip", example.Mediatype, example.Title) + + if verbose { + fmt.Println(example.Body, "---") + } + } + continue + } + } + + // BUG(stevvooe): Recursive validation is not working. Need to + // investigate. Will use this code as information for bug. + document := gojsonschema.NewStringLoader(example.Body) + result, err := gojsonschema.Validate(schema, document) + + if err != nil { + printFields("error", example.Mediatype, example.Title, err) + fmt.Println(example.Body, "---") + fail = true + continue + } + + if !result.Valid() { + // TOOD(stevvooe): This is nearly useless without file, line no. + printFields("invalid", example.Mediatype, example.Title) + for _, desc := range result.Errors() { + printFields("reason", example.Mediatype, example.Title, desc) + } + fmt.Println(example.Body, "---") + fail = true + continue + } + + printFields("ok", example.Mediatype, example.Title) + if verbose { + fmt.Println(example.Body, "---") + } + } + + if fail { + os.Exit(1) + } +} + +var ( + specsByMediaType = map[string]string{ + "application/vnd.oci.image.manifest.v1+json": "image-manifest-schema.json", + "application/vnd.oci.image.manifest.list.v1+json": "manifest-list-schema.json", + } + + errSchemaNotFound = errors.New("schema: not found") + errFormatInvalid = errors.New("format: invalid") +) + +func schemaByMediatype(root, mediatype string) (gojsonschema.JSONLoader, error) { + name, ok := specsByMediaType[mediatype] + if !ok { + return nil, errSchemaNotFound + } + + if !filepath.IsAbs(root) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + root = filepath.Join(wd, root) + } + + // lookup path + path := filepath.Join(root, name) + return gojsonschema.NewReferenceLoader("file://" + path), nil +} + +// renderer allows one to incercept fenced blocks in markdown documents. +type renderer struct { + blackfriday.Renderer + fn func(text []byte, lang string) +} + +func (r *renderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { + r.fn(text, lang) + r.Renderer.BlockCode(out, text, lang) +} + +type example struct { + Lang string // gets raw "lang" field + Title string + Mediatype string + Body string + Err error + + // TODO(stevvooe): Figure out how to keep track of revision, file, line so + // that we can trace back verification output. +} + +// parseExample treats the field as a syntax,attribute tuple separated by a comma. +// Attributes are encoded as a url values. +// +// An example of this is `json,title=Foo%20Bar&mediatype=application/json. We +// get that the "lang" is json, the title is "Foo Bar" and the mediatype is +// "application/json". +// +// This preserves syntax highlighting and lets us tag examples with further +// metadata. +func parseExample(lang, body string) (e example) { + e.Lang = lang + e.Body = body + + parts := strings.SplitN(lang, ",", 2) + if len(parts) < 2 { + e.Err = errFormatInvalid + return + } + + m, err := url.ParseQuery(parts[1]) + if err != nil { + e.Err = err + return + } + + e.Mediatype = m.Get("mediatype") + e.Title = m.Get("title") + return +} + +func extractExamples(rd io.Reader) ([]example, error) { + p, err := ioutil.ReadAll(rd) + if err != nil { + return nil, err + } + + var examples []example + renderer := &renderer{ + Renderer: blackfriday.HtmlRenderer(0, "test test", ""), + fn: func(text []byte, lang string) { + examples = append(examples, parseExample(lang, string(text))) + }, + } + + // just pass over the markdown and ignore the rendered result. We just want + // the side-effect of calling back for each code block. + // TODO(stevvooe): Consider just parsing these with a scanner. It will be + // faster and we can retain file, line no. + blackfriday.MarkdownOptions(p, renderer, blackfriday.Options{ + Extensions: blackfriday.EXTENSION_FENCED_CODE, + }) + + return examples, nil +} + +// printFields prints each value tab separated. +func printFields(vs ...interface{}) { + var ss []string + for _, f := range vs { + ss = append(ss, fmt.Sprint(f)) + } + fmt.Println(strings.Join(ss, "\t")) +} diff --git a/schema/validate.go b/cmd/oci-validate-json/main.go similarity index 100% rename from schema/validate.go rename to cmd/oci-validate-json/main.go diff --git a/manifest.md b/manifest.md index 0a1a0284b..04f9094e5 100644 --- a/manifest.md +++ b/manifest.md @@ -84,7 +84,7 @@ A client will distinguish a manifest list from an image manifest based on the Co ## Example Manifest List *Example showing a simple manifest list pointing to image manifests for two platforms:* -```json +```json,title=Manifest%20List&mediatype=application/vnd.oci.image.manifest.list.v1%2Bjson { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.list.v1+json", @@ -177,7 +177,7 @@ The image manifest provides a configuration and a set of layers for a container ## Example Image Manifest *Example showing an image manifest:* -```json +```json,title=Manifest&mediatype=application/vnd.oci.image.manifest.v1%2Bjson { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", diff --git a/schema/Makefile b/schema/Makefile deleted file mode 100644 index ae940718e..000000000 --- a/schema/Makefile +++ /dev/null @@ -1,15 +0,0 @@ - -default: help - -help: - @echo "Usage: make " - @echo - @echo " * 'fmt' - format the json with indentation" - @echo " * 'validate' - build the validation tool" - -fmt: - for i in *.json ; do jq --indent 2 -M . "$${i}" > xx && cat xx > "$${i}" && rm xx ; done - -validate: validate.go - go build ./validate.go -