Skip to content

Commit 6b5d048

Browse files
committed
Merge pull request #65 from s-urbaniak/validation
oci-image-tool: initial cut
2 parents ed95311 + 4263cf5 commit 6b5d048

13 files changed

Lines changed: 940 additions & 140 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
oci-validate-examples
22
code-of-conduct.md
3+
oci-image-tool

.travis.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ go:
44

55
sudo: false
66

7+
before_script:
8+
- export PATH=$HOME/gopath/bin:$PATH
9+
710
before_install:
811
- go get github.com/vbatts/git-validation
9-
- go get -d ./cmd/...
12+
- go get github.com/alecthomas/gometalinter
13+
- gometalinter --install --update
14+
- go get -t -d ./...
1015

1116
install: true
1217

1318
script:
14-
- $HOME/gopath/bin/git-validation -run DCO,short-subject -v -range ${TRAVIS_COMMIT_RANGE}
15-
- make validate-examples
16-
19+
- git-validation -run DCO,short-subject -v -range ${TRAVIS_COMMIT_RANGE}
20+
- make lint
21+
- make test
22+
- make oci-image-tool

Makefile

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ fmt:
2424
for i in *.json ; do jq --indent 2 -M . "$${i}" > xx && cat xx > "$${i}" && rm xx ; done
2525

2626
docs: output/docs.pdf output/docs.html
27-
.PHONY: docs
2827

2928
output/docs.pdf: $(DOC_FILES) $(FIGURE_FILES)
3029
@mkdir -p output/ && \
@@ -62,9 +61,23 @@ oci-validate-json: validate.go
6261
oci-validate-examples: cmd/oci-validate-examples/main.go
6362
go build ./cmd/oci-validate-examples
6463

64+
oci-image-tool:
65+
go build ./cmd/oci-image-tool
66+
67+
lint:
68+
for d in $(shell find . -type d -not -iwholename '*.git*'); do echo "$${d}" && ./lint "$${d}"; done
69+
70+
test:
71+
go test -race ./...
72+
6573
media-types.png: media-types.dot
6674

6775
%.png: %.dot
6876
dot -Tpng $^ > $@
6977

70-
.PHONY: validate-examples
78+
.PHONY: \
79+
validate-examples \
80+
oci-image-tool \
81+
lint \
82+
docs \
83+
test

cmd/oci-image-tool/main.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func main() {
11+
cmd := &cobra.Command{
12+
Use: "oci-image-tool",
13+
Short: "A tool for working with OCI images",
14+
}
15+
16+
stdout := log.New(os.Stdout, "", 0)
17+
stderr := log.New(os.Stderr, "", 0)
18+
19+
cmd.AddCommand(newValidateCmd(stdout, stderr))
20+
if err := cmd.Execute(); err != nil {
21+
stderr.Println(err)
22+
os.Exit(1)
23+
}
24+
}

cmd/oci-image-tool/validate.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"io/ioutil"
8+
"log"
9+
"net/http"
10+
"os"
11+
"strings"
12+
13+
"github.com/opencontainers/image-spec/schema"
14+
"github.com/pkg/errors"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
// supported validation types
19+
const (
20+
typeImageLayout = "imageLayout"
21+
typeImage = "image"
22+
typeManifest = "manifest"
23+
typeManifestList = "manifestList"
24+
typeConfig = "config"
25+
)
26+
27+
var validateTypes = []string{
28+
typeImageLayout,
29+
typeImage,
30+
typeManifest,
31+
typeManifestList,
32+
typeConfig,
33+
}
34+
35+
type validateCmd struct {
36+
stdout *log.Logger
37+
stderr *log.Logger
38+
typ string // the type to validate, can be empty string
39+
}
40+
41+
func newValidateCmd(stdout, stderr *log.Logger) *cobra.Command {
42+
v := &validateCmd{
43+
stdout: stdout,
44+
stderr: stderr,
45+
}
46+
47+
cmd := &cobra.Command{
48+
Use: "validate FILE...",
49+
Short: "Validate one or more image files",
50+
Run: v.Run,
51+
}
52+
53+
cmd.Flags().StringVar(
54+
&v.typ, "type", "",
55+
fmt.Sprintf(
56+
`Type of the file to validate. If unset, oci-image-tool will try to auto-detect the type. One of "%s"`,
57+
strings.Join(validateTypes, ","),
58+
),
59+
)
60+
61+
return cmd
62+
}
63+
64+
func (v *validateCmd) Run(cmd *cobra.Command, args []string) {
65+
if len(args) < 1 {
66+
v.stderr.Printf("no files specified")
67+
if err := cmd.Usage(); err != nil {
68+
v.stderr.Println(err)
69+
}
70+
os.Exit(1)
71+
}
72+
73+
var exitcode int
74+
for _, arg := range args {
75+
err := v.validatePath(arg)
76+
77+
if err == nil {
78+
v.stdout.Printf("file %s: OK", arg)
79+
continue
80+
}
81+
82+
var errs []error
83+
if verr, ok := errors.Cause(err).(schema.ValidationError); ok {
84+
errs = verr.Errs
85+
} else {
86+
v.stderr.Printf("file %s: validation failed: %v", arg, err)
87+
exitcode = 1
88+
continue
89+
}
90+
91+
for _, err := range errs {
92+
v.stderr.Printf("file %s: validation failed: %v", arg, err)
93+
}
94+
95+
exitcode = 1
96+
}
97+
98+
os.Exit(exitcode)
99+
}
100+
101+
func (v *validateCmd) validatePath(name string) error {
102+
var err error
103+
typ := v.typ
104+
105+
if typ == "" {
106+
if typ, err = autodetect(name); err != nil {
107+
return errors.Wrap(err, "unable to determine type")
108+
}
109+
}
110+
111+
f, err := os.Open(name)
112+
if err != nil {
113+
return errors.Wrap(err, "unable to open file")
114+
}
115+
defer f.Close()
116+
117+
switch typ {
118+
case typeManifest:
119+
if err := schema.MediaTypeManifest.Validate(f); err != nil {
120+
return err
121+
}
122+
123+
return nil
124+
case typeManifestList:
125+
if err := schema.MediaTypeManifestList.Validate(f); err != nil {
126+
return err
127+
}
128+
129+
return nil
130+
}
131+
132+
return fmt.Errorf("type %q unimplemented", typ)
133+
}
134+
135+
// autodetect detects the validation type for the given path
136+
// or an error if the validation type could not be resolved.
137+
func autodetect(path string) (string, error) {
138+
fi, err := os.Stat(path)
139+
if err != nil {
140+
return "", errors.Wrapf(err, "unable to access path") // err from os.Stat includes path name
141+
}
142+
143+
if fi.IsDir() {
144+
return typeImageLayout, nil
145+
}
146+
147+
f, err := os.Open(path)
148+
if err != nil {
149+
return "", errors.Wrap(err, "unable to open file") // os.Open includes the filename
150+
}
151+
defer f.Close()
152+
153+
buf, err := ioutil.ReadAll(io.LimitReader(f, 512)) // read some initial bytes to detect content
154+
if err != nil {
155+
return "", errors.Wrap(err, "unable to read")
156+
}
157+
158+
mimeType := http.DetectContentType(buf)
159+
160+
switch mimeType {
161+
case "application/x-gzip":
162+
return typeImage, nil
163+
164+
case "application/octet-stream":
165+
return typeImage, nil
166+
167+
case "text/plain; charset=utf-8":
168+
// might be a JSON file, will be handled below
169+
170+
default:
171+
return "", errors.New("unknown file type")
172+
}
173+
174+
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
175+
return "", errors.Wrap(err, "unable to seek")
176+
}
177+
178+
header := struct {
179+
SchemaVersion int `json:"schemaVersion"`
180+
MediaType string `json:"mediaType"`
181+
Config interface{} `json:"config"`
182+
}{}
183+
184+
if err := json.NewDecoder(f).Decode(&header); err != nil {
185+
return "", errors.Wrap(err, "unable to parse JSON")
186+
}
187+
188+
switch {
189+
case header.MediaType == string(schema.MediaTypeManifest):
190+
return typeManifest, nil
191+
192+
case header.MediaType == string(schema.MediaTypeManifestList):
193+
return typeManifestList, nil
194+
195+
case header.MediaType == "" && header.SchemaVersion == 0 && header.Config != nil:
196+
// config files don't have mediaType/schemaVersion header
197+
return typeConfig, nil
198+
}
199+
200+
return "", errors.New("unknown media type")
201+
}

cmd/oci-validate-json/main.go

Lines changed: 0 additions & 45 deletions
This file was deleted.

lint

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
3+
set -o errexit
4+
set -o nounset
5+
set -o pipefail
6+
7+
if [ ! $(command -v gometalinter) ]
8+
then
9+
go get github.com/alecthomas/gometalinter
10+
gometalinter --update --install
11+
fi
12+
13+
gometalinter \
14+
--exclude='error return value not checked.*(Close|Log|Print).*\(errcheck\)$' \
15+
--exclude='.*_test\.go:.*error return value not checked.*\(errcheck\)$' \
16+
--exclude='duplicate of.*_test.go.*\(dupl\)$' \
17+
--exclude='schema/fs.go' \
18+
--disable=aligncheck \
19+
--disable=gotype \
20+
--cyclo-over=20 \
21+
--tests \
22+
--deadline=10s "${1}"

schema/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package schema defines the OCI image media types, schema definitions and validation functions.
2+
package schema

0 commit comments

Comments
 (0)