Skip to content
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

Add DataDetector to support ACI and other layout format #49

Merged
merged 6 commits into from
Jan 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM golang:1.5
MAINTAINER Quentin Machu <quentin.machu@coreos.com>

RUN apt-get update && apt-get install -y bzr rpm && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN apt-get update && apt-get install -y bzr rpm xz && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN mkdir /db
VOLUME /db
Expand Down
4 changes: 2 additions & 2 deletions api/logic/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (

// POSTLayersParameters represents the expected parameters for POSTLayers.
type POSTLayersParameters struct {
ID, Path, ParentID string
ID, Path, ParentID, ImageFormat string
Copy link
Contributor

Choose a reason for hiding this comment

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

API doc about layer insertion should be updated as a new parameter appeared

}

// POSTLayers analyzes a layer and returns the engine version that has been used
Expand All @@ -43,7 +43,7 @@ func POSTLayers(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
}

// Process data.
if err := worker.Process(parameters.ID, parameters.ParentID, parameters.Path); err != nil {
if err := worker.Process(parameters.ID, parameters.ParentID, parameters.Path, parameters.ImageFormat); err != nil {
httputils.WriteHTTPError(w, 0, err)
return
}
Expand Down
1 change: 1 addition & 0 deletions cmd/clair/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

// Register components
_ "github.com/coreos/clair/updater/fetchers"
_ "github.com/coreos/clair/worker/detectors/data"
_ "github.com/coreos/clair/worker/detectors/os"
_ "github.com/coreos/clair/worker/detectors/packages"
)
Expand Down
2 changes: 1 addition & 1 deletion contrib/analyze-local-images/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func history(imageName string) ([]string, error) {
}

func analyzeLayer(endpoint, path, layerID, parentLayerID string) error {
payload := struct{ ID, Path, ParentID string }{ID: layerID, Path: path, ParentID: parentLayerID}
payload := struct{ ID, Path, ParentID, ImageFormat string }{ID: layerID, Path: path, ParentID: parentLayerID, ImageFormat: "Docker"}
jsonPayload, err := json.Marshal(payload)
if err != nil {
return err
Expand Down
3 changes: 2 additions & 1 deletion contrib/check-openvz-mirror-with-clair/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type AddLayoutRequestAPI struct {
ID string `json:"ID"`
Path string `json:"Path"`
ParantID string `json:"ParantID"`
ImageFormat string `json:"ImageFormat"`
}

type VulnerabilityItem struct {
Expand Down Expand Up @@ -223,7 +224,7 @@ func (clair ClairAPI) AddLayer(openvzMirror string, templateName string) error {
client = httpClient
}

jsonRequest, err := json.Marshal(AddLayoutRequestAPI{ID: templateName, Path: openvzMirror + "/" + templateName + ".tar.gz"})
jsonRequest, err := json.Marshal(AddLayoutRequestAPI{ID: templateName, Path: openvzMirror + "/" + templateName + ".tar.gz", ImageFormat: "Docker"})
if err != nil {
log.Println("Cannot convert to json request with error: ", err)
return err
Expand Down
9 changes: 5 additions & 4 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ It processes and inserts a new Layer in the database.
|------|-----|-------------|
|ID|String|Unique ID of the Layer|
|Path|String|Absolute path or HTTP link pointing to the Layer's tar file|
|ParentID|String|(Optionnal) Unique ID of the Layer's parent
|ParentID|String|(Optional) Unique ID of the Layer's parent|
|ImageFormat|String|Image format of the Layer ('Docker' or 'ACI')|

If the Layer has not parent, the ParentID field should be omitted or empty.

Expand Down Expand Up @@ -346,7 +347,7 @@ It returns the lists of vulnerabilities which affect a given Layer.
|Name|Type|Description|
|------|-----|-------------|
|ID|String|Unique ID of the Layer|
|minimumPriority|Priority|(Optionnal) The minimum priority of the returned vulnerabilities. Defaults to High|
|minimumPriority|Priority|(Optional) The minimum priority of the returned vulnerabilities. Defaults to High|

### Example

Expand Down Expand Up @@ -389,7 +390,7 @@ It returns the lists of vulnerabilities which are introduced and removed by the
|Name|Type|Description|
|------|-----|-------------|
|ID|String|Unique ID of the Layer|
|minimumPriority|Priority|(Optionnal) The minimum priority of the returned vulnerabilities|
|minimumPriority|Priority|(Optional) The minimum priority of the returned vulnerabilities|

### Example

Expand Down Expand Up @@ -436,7 +437,7 @@ Counterintuitively, this request is actually a POST to be able to pass a lot of
|Name|Type|Description|
|------|-----|-------------|
|LayersIDs|Array of strings|Unique IDs of Layers|
|minimumPriority|Priority|(Optionnal) The minimum priority of the returned vulnerabilities. Defaults to High|
|minimumPriority|Priority|(Optional) The minimum priority of the returned vulnerabilities. Defaults to High|

### Example

Expand Down
106 changes: 90 additions & 16 deletions utils/tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import (
"archive/tar"
"bufio"
"bytes"
"compress/bzip2"
"compress/gzip"
"errors"
"io"
"io/ioutil"
"os/exec"
"strings"
)

Expand All @@ -32,19 +34,75 @@ var (
// ErrExtractedFileTooBig occurs when a file to extract is too big.
ErrExtractedFileTooBig = errors.New("utils: could not extract one or more files from the archive: file too big")

gzipHeader = []byte{0x1f, 0x8b}
readLen = 6 // max bytes to sniff

gzipHeader = []byte{0x1f, 0x8b}
bzip2Header = []byte{0x42, 0x5a, 0x68}
xzHeader = []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}
)

// XzReader is an io.ReadCloser which decompresses xz compressed data.
type XzReader struct {
io.ReadCloser
cmd *exec.Cmd
closech chan error
}

// NewXzReader shells out to a command line xz executable (if
// available) to decompress the given io.Reader using the xz
// compression format and returns an *XzReader.
// It is the caller's responsibility to call Close on the XzReader when done.
func NewXzReader(r io.Reader) (*XzReader, error) {
rpipe, wpipe := io.Pipe()
ex, err := exec.LookPath("xz")
Copy link
Contributor

Choose a reason for hiding this comment

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

xz should be added as a dependency in the Dockerfile

if err != nil {
return nil, err
}
cmd := exec.Command(ex, "--decompress", "--stdout")

closech := make(chan error)

cmd.Stdin = r
cmd.Stdout = wpipe

go func() {
err := cmd.Run()
wpipe.CloseWithError(err)
closech <- err
}()

return &XzReader{rpipe, cmd, closech}, nil
}

func (r *XzReader) Close() error {
r.ReadCloser.Close()
r.cmd.Process.Kill()
return <-r.closech
}

// TarReadCloser embeds a *tar.Reader and the related io.Closer
// It is the caller's responsibility to call Close on TarReadCloser when
// done.
type TarReadCloser struct {
*tar.Reader
io.Closer
}

func (r *TarReadCloser) Close() error {
return r.Closer.Close()
}

// SelectivelyExtractArchive extracts the specified files and folders
// from targz data read from the given reader and store them in a map indexed by file paths
func SelectivelyExtractArchive(r io.Reader, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
func SelectivelyExtractArchive(r io.Reader, prefix string, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
data := make(map[string][]byte)

// Create a tar or tar/tar-gzip reader
// Create a tar or tar/tar-gzip/tar-bzip2/tar-xz reader
tr, err := getTarReader(r)
if err != nil {
return data, ErrCouldNotExtract
}
defer tr.Close()

// For each element in the archive
for {
Expand All @@ -59,6 +117,9 @@ func SelectivelyExtractArchive(r io.Reader, toExtract []string, maxFileSize int6
// Get element filename
filename := hdr.Name
filename = strings.TrimPrefix(filename, "./")
if prefix != "" {
filename = strings.TrimPrefix(filename, prefix)
}

// Determine if we should extract the element
toBeExtracted := false
Expand Down Expand Up @@ -86,22 +147,35 @@ func SelectivelyExtractArchive(r io.Reader, toExtract []string, maxFileSize int6
return data, nil
}

// getTarReader returns a tar.Reader associated with the specified io.Reader,
// optionally backed by a gzip.Reader if gzip compression is detected.
// getTarReader returns a TarReaderCloser associated with the specified io.Reader.
//
// Gzip detection is done by using the magic numbers defined in the RFC1952 :
// the first two bytes should be 0x1f and 0x8b..
func getTarReader(r io.Reader) (*tar.Reader, error) {
// Gzip/Bzip2/XZ detection is done by using the magic numbers:
// Gzip: the first two bytes should be 0x1f and 0x8b. Defined in the RFC1952.
// Bzip2: the first three bytes should be 0x42, 0x5a and 0x68. No RFC.
// XZ: the first three bytes should be 0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00. No RFC.
func getTarReader(r io.Reader) (*TarReadCloser, error) {
br := bufio.NewReader(r)
header, err := br.Peek(2)

if err == nil && bytes.Equal(header, gzipHeader) {
gr, err := gzip.NewReader(br)
if err != nil {
return nil, err
header, err := br.Peek(readLen)
if err == nil {
switch {
case bytes.HasPrefix(header, gzipHeader):
gr, err := gzip.NewReader(br)
if err != nil {
return nil, err
}
return &TarReadCloser{tar.NewReader(gr), gr}, nil
case bytes.HasPrefix(header, bzip2Header):
bzip2r := ioutil.NopCloser(bzip2.NewReader(br))
return &TarReadCloser{tar.NewReader(bzip2r), bzip2r}, nil
case bytes.HasPrefix(header, xzHeader):
xzr, err := NewXzReader(br)
if err != nil {
return nil, err
}
return &TarReadCloser{tar.NewReader(xzr), xzr}, nil
}
return tar.NewReader(gr), nil
}

return tar.NewReader(br), nil
dr := ioutil.NopCloser(br)
return &TarReadCloser{tar.NewReader(dr), dr}, nil
}
Binary file added utils/testdata/utils_test.tar.bz2
Binary file not shown.
Binary file added utils/testdata/utils_test.tar.xz
Binary file not shown.
12 changes: 6 additions & 6 deletions utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ func TestTar(t *testing.T) {
var err error
var data map[string][]byte
_, filepath, _, _ := runtime.Caller(0)

for _, filename := range []string{"/testdata/utils_test.tar.gz", "/testdata/utils_test.tar"} {
testArchivePath := path.Join(path.Dir(filepath)) + filename
testDataDir := "/testdata"
for _, filename := range []string{"utils_test.tar.gz", "utils_test.tar.bz2", "utils_test.tar.xz", "utils_test.tar"} {
testArchivePath := path.Join(path.Dir(filepath), testDataDir, filename)

// Extract non compressed data
data, err = SelectivelyExtractArchive(bytes.NewReader([]byte("that string does not represent a tar or tar-gzip file")), []string{}, 0)
data, err = SelectivelyExtractArchive(bytes.NewReader([]byte("that string does not represent a tar or tar-gzip file")), "", []string{}, 0)
assert.Error(t, err, "Extracting non compressed data should return an error")

// Extract an archive
f, _ := os.Open(testArchivePath)
defer f.Close()
data, err = SelectivelyExtractArchive(f, []string{"test/"}, 0)
data, err = SelectivelyExtractArchive(f, "", []string{"test/"}, 0)
assert.Nil(t, err)

if c, n := data["test/test.txt"]; !n {
Expand All @@ -86,7 +86,7 @@ func TestTar(t *testing.T) {
// File size limit
f, _ = os.Open(testArchivePath)
defer f.Close()
data, err = SelectivelyExtractArchive(f, []string{"test"}, 50)
data, err = SelectivelyExtractArchive(f, "", []string{"test"}, 50)
assert.Equal(t, ErrExtractedFileTooBig, err)
}
}
Expand Down
91 changes: 91 additions & 0 deletions worker/detectors/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2015 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package detectors exposes functions to register and use container
// information extractors.
package detectors

import (
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"

cerrors "github.com/coreos/clair/utils/errors"
)

// The DataDetector interface defines a way to detect the required data from input path
type DataDetector interface {
//Support check if the input path and format are supported by the underling detector
Supported(path string, format string) bool
// Detect detects the required data from input path
Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (data map[string][]byte, err error)
}

var (
dataDetectorsLock sync.Mutex
dataDetectors = make(map[string]DataDetector)
)

// RegisterDataDetector provides a way to dynamically register an implementation of a
// DataDetector.
//
// If RegisterDataDetector is called twice with the same name if DataDetector is nil,
// or if the name is blank, it panics.
func RegisterDataDetector(name string, f DataDetector) {
if name == "" {
panic("Could not register a DataDetector with an empty name")
}
if f == nil {
panic("Could not register a nil DataDetector")
}

dataDetectorsLock.Lock()
defer dataDetectorsLock.Unlock()

if _, alreadyExists := dataDetectors[name]; alreadyExists {
panic(fmt.Sprintf("Detector '%s' is already registered", name))
}
dataDetectors[name] = f
}

// DetectData finds the Data of the layer by using every registered DataDetector
func DetectData(path string, format string, toExtract []string, maxFileSize int64) (data map[string][]byte, err error) {
var layerReader io.ReadCloser
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
r, err := http.Get(path)
if err != nil {
return nil, cerrors.ErrCouldNotDownload
}
layerReader = r.Body
} else {
layerReader, err = os.Open(path)
if err != nil {
return nil, cerrors.ErrNotFound
}
}
defer layerReader.Close()

for _, detector := range dataDetectors {
if detector.Supported(path, format) {
if data, err = detector.Detect(layerReader, toExtract, maxFileSize); err == nil {
return data, nil
}
}
}

return nil, nil
}
Loading