Skip to content

Commit

Permalink
oci-image-tool: initial commit
Browse files Browse the repository at this point in the history
This adds the oci-image-tool with the following capabilities:
- validate manifest
- validate manifest list

Signed-off-by: Sergiusz Urbaniak <sur@coreos.com>
  • Loading branch information
Sergiusz Urbaniak authored and s-urbaniak committed May 19, 2016
1 parent ed95311 commit 4263cf5
Show file tree
Hide file tree
Showing 13 changed files with 940 additions and 140 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
oci-validate-examples
code-of-conduct.md
oci-image-tool
14 changes: 10 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ go:

sudo: false

before_script:
- export PATH=$HOME/gopath/bin:$PATH

before_install:
- go get github.com/vbatts/git-validation
- go get -d ./cmd/...
- go get github.com/alecthomas/gometalinter
- gometalinter --install --update
- go get -t -d ./...

install: true

script:
- $HOME/gopath/bin/git-validation -run DCO,short-subject -v -range ${TRAVIS_COMMIT_RANGE}
- make validate-examples

- git-validation -run DCO,short-subject -v -range ${TRAVIS_COMMIT_RANGE}
- make lint
- make test
- make oci-image-tool
17 changes: 15 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ fmt:
for i in *.json ; do jq --indent 2 -M . "$${i}" > xx && cat xx > "$${i}" && rm xx ; done

docs: output/docs.pdf output/docs.html
.PHONY: docs

output/docs.pdf: $(DOC_FILES) $(FIGURE_FILES)
@mkdir -p output/ && \
Expand Down Expand Up @@ -62,9 +61,23 @@ oci-validate-json: validate.go
oci-validate-examples: cmd/oci-validate-examples/main.go
go build ./cmd/oci-validate-examples

oci-image-tool:
go build ./cmd/oci-image-tool

lint:
for d in $(shell find . -type d -not -iwholename '*.git*'); do echo "$${d}" && ./lint "$${d}"; done

test:
go test -race ./...

media-types.png: media-types.dot

%.png: %.dot
dot -Tpng $^ > $@

.PHONY: validate-examples
.PHONY: \
validate-examples \
oci-image-tool \
lint \
docs \
test
24 changes: 24 additions & 0 deletions cmd/oci-image-tool/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"log"
"os"

"github.com/spf13/cobra"
)

func main() {
cmd := &cobra.Command{
Use: "oci-image-tool",
Short: "A tool for working with OCI images",
}

stdout := log.New(os.Stdout, "", 0)
stderr := log.New(os.Stderr, "", 0)

cmd.AddCommand(newValidateCmd(stdout, stderr))
if err := cmd.Execute(); err != nil {
stderr.Println(err)
os.Exit(1)
}
}
201 changes: 201 additions & 0 deletions cmd/oci-image-tool/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package main

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"

"github.com/opencontainers/image-spec/schema"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

// supported validation types
const (
typeImageLayout = "imageLayout"
typeImage = "image"
typeManifest = "manifest"
typeManifestList = "manifestList"
typeConfig = "config"
)

var validateTypes = []string{
typeImageLayout,
typeImage,
typeManifest,
typeManifestList,
typeConfig,
}

type validateCmd struct {
stdout *log.Logger
stderr *log.Logger
typ string // the type to validate, can be empty string
}

func newValidateCmd(stdout, stderr *log.Logger) *cobra.Command {
v := &validateCmd{
stdout: stdout,
stderr: stderr,
}

cmd := &cobra.Command{
Use: "validate FILE...",
Short: "Validate one or more image files",
Run: v.Run,
}

cmd.Flags().StringVar(
&v.typ, "type", "",
fmt.Sprintf(
`Type of the file to validate. If unset, oci-image-tool will try to auto-detect the type. One of "%s"`,
strings.Join(validateTypes, ","),
),
)

return cmd
}

func (v *validateCmd) Run(cmd *cobra.Command, args []string) {
if len(args) < 1 {
v.stderr.Printf("no files specified")
if err := cmd.Usage(); err != nil {
v.stderr.Println(err)
}
os.Exit(1)
}

var exitcode int
for _, arg := range args {
err := v.validatePath(arg)

if err == nil {
v.stdout.Printf("file %s: OK", arg)
continue
}

var errs []error
if verr, ok := errors.Cause(err).(schema.ValidationError); ok {
errs = verr.Errs
} else {
v.stderr.Printf("file %s: validation failed: %v", arg, err)
exitcode = 1
continue
}

for _, err := range errs {
v.stderr.Printf("file %s: validation failed: %v", arg, err)
}

exitcode = 1
}

os.Exit(exitcode)
}

func (v *validateCmd) validatePath(name string) error {
var err error
typ := v.typ

if typ == "" {
if typ, err = autodetect(name); err != nil {
return errors.Wrap(err, "unable to determine type")
}
}

f, err := os.Open(name)
if err != nil {
return errors.Wrap(err, "unable to open file")
}
defer f.Close()

switch typ {
case typeManifest:
if err := schema.MediaTypeManifest.Validate(f); err != nil {
return err
}

return nil
case typeManifestList:
if err := schema.MediaTypeManifestList.Validate(f); err != nil {
return err
}

return nil
}

return fmt.Errorf("type %q unimplemented", typ)
}

// autodetect detects the validation type for the given path
// or an error if the validation type could not be resolved.
func autodetect(path string) (string, error) {
fi, err := os.Stat(path)
if err != nil {
return "", errors.Wrapf(err, "unable to access path") // err from os.Stat includes path name
}

if fi.IsDir() {
return typeImageLayout, nil
}

f, err := os.Open(path)
if err != nil {
return "", errors.Wrap(err, "unable to open file") // os.Open includes the filename
}
defer f.Close()

buf, err := ioutil.ReadAll(io.LimitReader(f, 512)) // read some initial bytes to detect content
if err != nil {
return "", errors.Wrap(err, "unable to read")
}

mimeType := http.DetectContentType(buf)

switch mimeType {
case "application/x-gzip":
return typeImage, nil

case "application/octet-stream":
return typeImage, nil

case "text/plain; charset=utf-8":
// might be a JSON file, will be handled below

default:
return "", errors.New("unknown file type")
}

if _, err := f.Seek(0, os.SEEK_SET); err != nil {
return "", errors.Wrap(err, "unable to seek")
}

header := struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config interface{} `json:"config"`
}{}

if err := json.NewDecoder(f).Decode(&header); err != nil {
return "", errors.Wrap(err, "unable to parse JSON")
}

switch {
case header.MediaType == string(schema.MediaTypeManifest):
return typeManifest, nil

case header.MediaType == string(schema.MediaTypeManifestList):
return typeManifestList, nil

case header.MediaType == "" && header.SchemaVersion == 0 && header.Config != nil:
// config files don't have mediaType/schemaVersion header
return typeConfig, nil
}

return "", errors.New("unknown media type")
}
45 changes: 0 additions & 45 deletions cmd/oci-validate-json/main.go

This file was deleted.

22 changes: 22 additions & 0 deletions lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

if [ ! $(command -v gometalinter) ]
then
go get github.com/alecthomas/gometalinter
gometalinter --update --install
fi

gometalinter \
--exclude='error return value not checked.*(Close|Log|Print).*\(errcheck\)$' \
--exclude='.*_test\.go:.*error return value not checked.*\(errcheck\)$' \
--exclude='duplicate of.*_test.go.*\(dupl\)$' \
--exclude='schema/fs.go' \
--disable=aligncheck \
--disable=gotype \
--cyclo-over=20 \
--tests \
--deadline=10s "${1}"
2 changes: 2 additions & 0 deletions schema/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package schema defines the OCI image media types, schema definitions and validation functions.
package schema
Loading

0 comments on commit 4263cf5

Please sign in to comment.