ext: namespace detector -> featurens

This commit is contained in:
Jimmy Zelinskie 2017-01-13 16:48:12 -05:00
parent d9be34c3c4
commit fb193e1fde
15 changed files with 283 additions and 285 deletions

View File

@ -26,6 +26,11 @@ import (
"github.com/coreos/clair/config" "github.com/coreos/clair/config"
// Register extensions. // Register extensions.
_ "github.com/coreos/clair/ext/featurens/alpinerelease"
_ "github.com/coreos/clair/ext/featurens/aptsources"
_ "github.com/coreos/clair/ext/featurens/lsbrelease"
_ "github.com/coreos/clair/ext/featurens/osrelease"
_ "github.com/coreos/clair/ext/featurens/redhatrelease"
_ "github.com/coreos/clair/ext/imagefmt/aci" _ "github.com/coreos/clair/ext/imagefmt/aci"
_ "github.com/coreos/clair/ext/imagefmt/docker" _ "github.com/coreos/clair/ext/imagefmt/docker"
_ "github.com/coreos/clair/ext/notification/webhook" _ "github.com/coreos/clair/ext/notification/webhook"
@ -40,12 +45,6 @@ import (
_ "github.com/coreos/clair/worker/detectors/feature/dpkg" _ "github.com/coreos/clair/worker/detectors/feature/dpkg"
_ "github.com/coreos/clair/worker/detectors/feature/rpm" _ "github.com/coreos/clair/worker/detectors/feature/rpm"
_ "github.com/coreos/clair/worker/detectors/namespace/alpinerelease"
_ "github.com/coreos/clair/worker/detectors/namespace/aptsources"
_ "github.com/coreos/clair/worker/detectors/namespace/lsbrelease"
_ "github.com/coreos/clair/worker/detectors/namespace/osrelease"
_ "github.com/coreos/clair/worker/detectors/namespace/redhatrelease"
_ "github.com/coreos/clair/database/pgsql" _ "github.com/coreos/clair/database/pgsql"
) )

View File

@ -1,4 +1,4 @@
// Copyright 2016 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package alpinerelease implements a featurens.Detector for Alpine Linux based
// container image layers.
package alpinerelease package alpinerelease
import ( import (
@ -21,8 +23,9 @@ import (
"strings" "strings"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/versionfmt/dpkg" "github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/pkg/tarutil"
) )
const ( const (
@ -33,15 +36,13 @@ const (
var versionRegexp = regexp.MustCompile(`^(\d)+\.(\d)+\.(\d)+$`) var versionRegexp = regexp.MustCompile(`^(\d)+\.(\d)+\.(\d)+$`)
func init() { func init() {
detectors.RegisterNamespaceDetector("alpine-release", &detector{}) featurens.RegisterDetector("alpine-release", &detector{})
} }
// detector implements NamespaceDetector by reading the current version of
// Alpine Linux from /etc/alpine-release.
type detector struct{} type detector struct{}
func (d *detector) Detect(data map[string][]byte) *database.Namespace { func (d detector) Detect(files tarutil.FilesMap) (*database.Namespace, error) {
file, exists := data[alpineReleasePath] file, exists := files[alpineReleasePath]
if exists { if exists {
scanner := bufio.NewScanner(bytes.NewBuffer(file)) scanner := bufio.NewScanner(bytes.NewBuffer(file))
for scanner.Scan() { for scanner.Scan() {
@ -52,14 +53,14 @@ func (d *detector) Detect(data map[string][]byte) *database.Namespace {
return &database.Namespace{ return &database.Namespace{
Name: osName + ":" + "v" + versionNumbers[0] + "." + versionNumbers[1], Name: osName + ":" + "v" + versionNumbers[0] + "." + versionNumbers[1],
VersionFormat: dpkg.ParserName, VersionFormat: dpkg.ParserName,
} }, nil
} }
} }
} }
return nil return nil, nil
} }
func (d *detector) GetRequiredFiles() []string { func (d detector) RequiredFilenames() []string {
return []string{alpineReleasePath} return []string{alpineReleasePath}
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,34 +18,35 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/worker/detectors/namespace" "github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/pkg/tarutil"
) )
func TestAlpineReleaseNamespaceDetection(t *testing.T) { func TestDetector(t *testing.T) {
testData := []namespace.TestData{ testData := []featurens.TestData{
{ {
ExpectedNamespace: &database.Namespace{Name: "alpine:v3.3"}, ExpectedNamespace: &database.Namespace{Name: "alpine:v3.3"},
Data: map[string][]byte{"etc/alpine-release": []byte(`3.3.4`)}, Files: tarutil.FilesMap{"etc/alpine-release": []byte(`3.3.4`)},
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "alpine:v3.4"}, ExpectedNamespace: &database.Namespace{Name: "alpine:v3.4"},
Data: map[string][]byte{"etc/alpine-release": []byte(`3.4.0`)}, Files: tarutil.FilesMap{"etc/alpine-release": []byte(`3.4.0`)},
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "alpine:v0.3"}, ExpectedNamespace: &database.Namespace{Name: "alpine:v0.3"},
Data: map[string][]byte{"etc/alpine-release": []byte(`0.3.4`)}, Files: tarutil.FilesMap{"etc/alpine-release": []byte(`0.3.4`)},
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "alpine:v0.3"}, ExpectedNamespace: &database.Namespace{Name: "alpine:v0.3"},
Data: map[string][]byte{"etc/alpine-release": []byte(` Files: tarutil.FilesMap{"etc/alpine-release": []byte(`
0.3.4 0.3.4
`)}, `)},
}, },
{ {
ExpectedNamespace: nil, ExpectedNamespace: nil,
Data: map[string][]byte{}, Files: tarutil.FilesMap{},
}, },
} }
namespace.TestDetector(t, &detector{}, testData) featurens.TestDetector(t, &detector{}, testData)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package aptsources implements a featurens.Detector for apt based container
// image layers.
//
// This detector is necessary to determine the precise Debian version when it
// is an unstable version for instance.
package aptsources package aptsources
import ( import (
@ -19,25 +24,21 @@ import (
"strings" "strings"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/versionfmt/dpkg" "github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/pkg/tarutil"
) )
// AptSourcesNamespaceDetector implements NamespaceDetector and detects the Namespace from the type detector struct{}
// /etc/apt/sources.list file.
//
// This detector is necessary to determine precise Debian version when it is
// an unstable version for instance.
type AptSourcesNamespaceDetector struct{}
func init() { func init() {
detectors.RegisterNamespaceDetector("apt-sources", &AptSourcesNamespaceDetector{}) featurens.RegisterDetector("apt-sources", &detector{})
} }
func (detector *AptSourcesNamespaceDetector) Detect(data map[string][]byte) *database.Namespace { func (d detector) Detect(files tarutil.FilesMap) (*database.Namespace, error) {
f, hasFile := data["etc/apt/sources.list"] f, hasFile := files["etc/apt/sources.list"]
if !hasFile { if !hasFile {
return nil return nil, nil
} }
var OS, version string var OS, version string
@ -79,11 +80,11 @@ func (detector *AptSourcesNamespaceDetector) Detect(data map[string][]byte) *dat
return &database.Namespace{ return &database.Namespace{
Name: OS + ":" + version, Name: OS + ":" + version,
VersionFormat: dpkg.ParserName, VersionFormat: dpkg.ParserName,
}, nil
} }
} return nil, nil
return nil
} }
func (detector *AptSourcesNamespaceDetector) GetRequiredFiles() []string { func (d detector) RequiredFilenames() []string {
return []string{"etc/apt/sources.list"} return []string{"etc/apt/sources.list"}
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,14 +18,15 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/worker/detectors/namespace" "github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/pkg/tarutil"
) )
func TestAptSourcesNamespaceDetector(t *testing.T) { func TestDetector(t *testing.T) {
testData := []namespace.TestData{ testData := []featurens.TestData{
{ {
ExpectedNamespace: &database.Namespace{Name: "debian:unstable"}, ExpectedNamespace: &database.Namespace{Name: "debian:unstable"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/os-release": []byte( "etc/os-release": []byte(
`PRETTY_NAME="Debian GNU/Linux stretch/sid" `PRETTY_NAME="Debian GNU/Linux stretch/sid"
NAME="Debian GNU/Linux" NAME="Debian GNU/Linux"
@ -38,5 +39,5 @@ BUG_REPORT_URL="https://bugs.debian.org/"`),
}, },
} }
namespace.TestDetector(t, &AptSourcesNamespaceDetector{}, testData) featurens.TestDetector(t, &detector{}, testData)
} }

124
ext/featurens/driver.go Normal file
View File

@ -0,0 +1,124 @@
// Copyright 2017 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 featurens exposes functions to dynamically register methods for
// determining a namespace for features present in an image layer.
package featurens
import (
"sync"
"testing"
"github.com/coreos/pkg/capnslog"
"github.com/stretchr/testify/assert"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/tarutil"
)
var (
log = capnslog.NewPackageLogger("github.com/coreos/clair", "ext/featurens")
detectorsM sync.RWMutex
detectors = make(map[string]Detector)
)
// Detector represents an ability to detect a namespace used for organizing
// features present in an image layer.
type Detector interface {
// Detect attempts to determine a Namespace from a FilesMap of an image
// layer.
Detect(tarutil.FilesMap) (*database.Namespace, error)
// RequireFilenames returns the list of files required to be in the FilesMap
// provided to the Detect method.
// TODO(jzelinskie): strip "/" prefix
RequiredFilenames() []string
}
// RegisterDetector makes a detector available by the provided name.
//
// If called twice with the same name, the name is blank, or if the provided
// Detector is nil, this function panics.
func RegisterDetector(name string, d Detector) {
if name == "" {
panic("namespace: could not register a Detector with an empty name")
}
if d == nil {
panic("namespace: could not register a nil Detector")
}
detectorsM.Lock()
defer detectorsM.Unlock()
if _, dup := detectors[name]; dup {
panic("namespace: RegisterDetector called twice for " + name)
}
detectors[name] = d
}
// Detect iterators through all registered Detectors and returns the first
// non-nil detected namespace.
func Detect(files tarutil.FilesMap) (*database.Namespace, error) {
detectorsM.RLock()
defer detectorsM.RUnlock()
for name, detector := range detectors {
namespace, err := detector.Detect(files)
if err != nil {
log.Warningf("failed while attempting to detect namespace %s: %s", name, err)
return nil, err
}
if namespace != nil {
log.Debugf("detected namespace %s: %#v", name, namespace)
return namespace, nil
}
}
return nil, nil
}
// RequiredFilenames returns the total list of files required for all
// registered Detectors.
func RequiredFilenames() (files []string) {
for _, detector := range detectors {
files = append(files, detector.RequiredFilenames()...)
}
return
}
// TestData represents the data used to test an implementation of
// NameSpaceDetector.
type TestData struct {
Files tarutil.FilesMap
ExpectedNamespace *database.Namespace
}
// TestDetector runs a Detector on each provided instance of TestData and
// asserts the output to be equal to the expected output.
func TestDetector(t *testing.T, d Detector, testData []TestData) {
for _, td := range testData {
namespace, err := d.Detect(td.Files)
assert.Nil(t, err)
if namespace == nil {
assert.Equal(t, td.ExpectedNamespace, namespace)
} else {
assert.Equal(t, td.ExpectedNamespace.Name, namespace.Name)
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package lsbrelease implements a featurens.Detector for container image
// layers containing an lsb-release file.
//
// This detector is necessary for detecting Ubuntu Precise.
package lsbrelease package lsbrelease
import ( import (
@ -20,9 +24,10 @@ import (
"strings" "strings"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/versionfmt/dpkg" "github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/ext/versionfmt/rpm" "github.com/coreos/clair/ext/versionfmt/rpm"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/pkg/tarutil"
) )
var ( var (
@ -30,20 +35,16 @@ var (
lsbReleaseVersionRegexp = regexp.MustCompile(`^DISTRIB_RELEASE=(.*)`) lsbReleaseVersionRegexp = regexp.MustCompile(`^DISTRIB_RELEASE=(.*)`)
) )
// LsbReleaseNamespaceDetector implements NamespaceDetector and detects the type detector struct{}
// Namespace from the /etc/lsb-release file.
//
// This detector is necessary for Ubuntu Precise.
type LsbReleaseNamespaceDetector struct{}
func init() { func init() {
detectors.RegisterNamespaceDetector("lsb-release", &LsbReleaseNamespaceDetector{}) featurens.RegisterDetector("lsb-release", &detector{})
} }
func (detector *LsbReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace { func (d detector) Detect(files tarutil.FilesMap) (*database.Namespace, error) {
f, hasFile := data["etc/lsb-release"] f, hasFile := files["etc/lsb-release"]
if !hasFile { if !hasFile {
return nil return nil, nil
} }
var OS, version string var OS, version string
@ -79,19 +80,19 @@ func (detector *LsbReleaseNamespaceDetector) Detect(data map[string][]byte) *dat
case "centos", "rhel", "fedora", "amzn", "ol", "oracle": case "centos", "rhel", "fedora", "amzn", "ol", "oracle":
versionFormat = rpm.ParserName versionFormat = rpm.ParserName
default: default:
return nil return nil, nil
} }
if OS != "" && version != "" { if OS != "" && version != "" {
return &database.Namespace{ return &database.Namespace{
Name: OS + ":" + version, Name: OS + ":" + version,
VersionFormat: versionFormat, VersionFormat: versionFormat,
}, nil
} }
}
return nil return nil, nil
} }
// GetRequiredFiles returns the list of files that are required for Detect() func (d *detector) RequiredFilenames() []string {
func (detector *LsbReleaseNamespaceDetector) GetRequiredFiles() []string {
return []string{"etc/lsb-release"} return []string{"etc/lsb-release"}
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,14 +18,15 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/worker/detectors/namespace" "github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/pkg/tarutil"
) )
func TestLsbReleaseNamespaceDetector(t *testing.T) { func TestDetector(t *testing.T) {
testData := []namespace.TestData{ testData := []featurens.TestData{
{ {
ExpectedNamespace: &database.Namespace{Name: "ubuntu:12.04"}, ExpectedNamespace: &database.Namespace{Name: "ubuntu:12.04"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/lsb-release": []byte( "etc/lsb-release": []byte(
`DISTRIB_ID=Ubuntu `DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=12.04 DISTRIB_RELEASE=12.04
@ -35,7 +36,7 @@ DISTRIB_DESCRIPTION="Ubuntu 12.04 LTS"`),
}, },
{ // We don't care about the minor version of Debian { // We don't care about the minor version of Debian
ExpectedNamespace: &database.Namespace{Name: "debian:7"}, ExpectedNamespace: &database.Namespace{Name: "debian:7"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/lsb-release": []byte( "etc/lsb-release": []byte(
`DISTRIB_ID=Debian `DISTRIB_ID=Debian
DISTRIB_RELEASE=7.1 DISTRIB_RELEASE=7.1
@ -45,5 +46,5 @@ DISTRIB_DESCRIPTION="Debian 7.1"`),
}, },
} }
namespace.TestDetector(t, &LsbReleaseNamespaceDetector{}, testData) featurens.TestDetector(t, &detector{}, testData)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package osrelease implements a featurens.Detector for container image
// layers containing an os-release file.
//
// This detector is typically useful for detecting Debian or Ubuntu.
package osrelease package osrelease
import ( import (
@ -20,40 +24,41 @@ import (
"strings" "strings"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/versionfmt/dpkg" "github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/ext/versionfmt/rpm" "github.com/coreos/clair/ext/versionfmt/rpm"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/pkg/tarutil"
) )
var ( var (
//log = capnslog.NewPackageLogger("github.com/coreos/clair", "worker/detectors/namespace/osrelease")
osReleaseOSRegexp = regexp.MustCompile(`^ID=(.*)`) osReleaseOSRegexp = regexp.MustCompile(`^ID=(.*)`)
osReleaseVersionRegexp = regexp.MustCompile(`^VERSION_ID=(.*)`) osReleaseVersionRegexp = regexp.MustCompile(`^VERSION_ID=(.*)`)
// blacklistFilenames are files that should exclude this detector.
blacklistFilenames = []string{
"etc/oracle-release",
"etc/redhat-release",
"usr/lib/centos-release",
}
) )
// OsReleaseNamespaceDetector implements NamespaceDetector and detects the OS from the type detector struct{}
// /etc/os-release and usr/lib/os-release files.
type OsReleaseNamespaceDetector struct{}
func init() { func init() {
detectors.RegisterNamespaceDetector("os-release", &OsReleaseNamespaceDetector{}) featurens.RegisterDetector("os-release", &detector{})
} }
// Detect tries to detect OS/Version using "/etc/os-release" and "/usr/lib/os-release" func (d detector) Detect(files tarutil.FilesMap) (*database.Namespace, error) {
// Typically for Debian / Ubuntu
// /etc/debian_version can't be used, it does not make any difference between testing and unstable, it returns stretch/sid
func (detector *OsReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace {
var OS, version string var OS, version string
for _, filePath := range detector.getExcludeFiles() { for _, filePath := range blacklistFilenames {
if _, hasFile := data[filePath]; hasFile { if _, hasFile := files[filePath]; hasFile {
return nil return nil, nil
} }
} }
for _, filePath := range detector.GetRequiredFiles() { for _, filePath := range d.RequiredFilenames() {
f, hasFile := data[filePath] f, hasFile := files[filePath]
if !hasFile { if !hasFile {
continue continue
} }
@ -82,24 +87,18 @@ func (detector *OsReleaseNamespaceDetector) Detect(data map[string][]byte) *data
case "centos", "rhel", "fedora", "amzn", "ol", "oracle": case "centos", "rhel", "fedora", "amzn", "ol", "oracle":
versionFormat = rpm.ParserName versionFormat = rpm.ParserName
default: default:
return nil return nil, nil
} }
if OS != "" && version != "" { if OS != "" && version != "" {
return &database.Namespace{ return &database.Namespace{
Name: OS + ":" + version, Name: OS + ":" + version,
VersionFormat: versionFormat, VersionFormat: versionFormat,
}, nil
} }
} return nil, nil
return nil
} }
// GetRequiredFiles returns the list of files that are required for Detect() func (d detector) RequiredFilenames() []string {
func (detector *OsReleaseNamespaceDetector) GetRequiredFiles() []string {
return []string{"etc/os-release", "usr/lib/os-release"} return []string{"etc/os-release", "usr/lib/os-release"}
} }
// getExcludeFiles returns the list of files that are ought to exclude this detector from Detect()
func (detector *OsReleaseNamespaceDetector) getExcludeFiles() []string {
return []string{"etc/oracle-release", "etc/redhat-release", "usr/lib/centos-release"}
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,14 +18,15 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/worker/detectors/namespace" "github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/pkg/tarutil"
) )
func TestOsReleaseNamespaceDetector(t *testing.T) { func TestDetector(t *testing.T) {
testData := []namespace.TestData{ testData := []featurens.TestData{
{ {
ExpectedNamespace: &database.Namespace{Name: "debian:8"}, ExpectedNamespace: &database.Namespace{Name: "debian:8"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/os-release": []byte( "etc/os-release": []byte(
`PRETTY_NAME="Debian GNU/Linux 8 (jessie)" `PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
NAME="Debian GNU/Linux" NAME="Debian GNU/Linux"
@ -39,7 +40,7 @@ BUG_REPORT_URL="https://bugs.debian.org/"`),
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "ubuntu:15.10"}, ExpectedNamespace: &database.Namespace{Name: "ubuntu:15.10"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/os-release": []byte( "etc/os-release": []byte(
`NAME="Ubuntu" `NAME="Ubuntu"
VERSION="15.10 (Wily Werewolf)" VERSION="15.10 (Wily Werewolf)"
@ -54,7 +55,7 @@ BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`),
}, },
{ // Doesn't have quotes around VERSION_ID { // Doesn't have quotes around VERSION_ID
ExpectedNamespace: &database.Namespace{Name: "fedora:20"}, ExpectedNamespace: &database.Namespace{Name: "fedora:20"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/os-release": []byte( "etc/os-release": []byte(
`NAME=Fedora `NAME=Fedora
VERSION="20 (Heisenbug)" VERSION="20 (Heisenbug)"
@ -73,5 +74,5 @@ REDHAT_SUPPORT_PRODUCT_VERSION=20`),
}, },
} }
namespace.TestDetector(t, &OsReleaseNamespaceDetector{}, testData) featurens.TestDetector(t, &detector{}, testData)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2015 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package redhatrelease implements a featurens.Detector for container image
// layers containing an redhat-release-like files.
//
// This detector is typically useful for detecting CentOS and Red-Hat like
// systems.
package redhatrelease package redhatrelease
import ( import (
@ -19,77 +24,64 @@ import (
"strings" "strings"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/versionfmt/rpm" "github.com/coreos/clair/ext/versionfmt/rpm"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/pkg/tarutil"
"github.com/coreos/pkg/capnslog"
) )
var ( var (
log = capnslog.NewPackageLogger("github.com/coreos/clair", "worker/detectors/namespace/redhatrelease")
oracleReleaseRegexp = regexp.MustCompile(`(?P<os>[^\s]*) (Linux Server release) (?P<version>[\d]+)`) oracleReleaseRegexp = regexp.MustCompile(`(?P<os>[^\s]*) (Linux Server release) (?P<version>[\d]+)`)
centosReleaseRegexp = regexp.MustCompile(`(?P<os>[^\s]*) (Linux release|release) (?P<version>[\d]+)`) centosReleaseRegexp = regexp.MustCompile(`(?P<os>[^\s]*) (Linux release|release) (?P<version>[\d]+)`)
redhatReleaseRegexp = regexp.MustCompile(`(?P<os>Red Hat Enterprise Linux) (Client release|Server release|Workstation release) (?P<version>[\d]+)`) redhatReleaseRegexp = regexp.MustCompile(`(?P<os>Red Hat Enterprise Linux) (Client release|Server release|Workstation release) (?P<version>[\d]+)`)
) )
// RedhatReleaseNamespaceDetector implements NamespaceDetector and detects the OS from the type detector struct{}
// /etc/oracle-release, /etc/centos-release, /etc/redhat-release and /etc/system-release files.
//
// Typically for CentOS and Red-Hat like systems
// eg. CentOS release 5.11 (Final)
// eg. CentOS release 6.6 (Final)
// eg. CentOS Linux release 7.1.1503 (Core)
// eg. Oracle Linux Server release 7.3
// eg. Red Hat Enterprise Linux Server release 7.2 (Maipo)
type RedhatReleaseNamespaceDetector struct{}
func init() { func init() {
detectors.RegisterNamespaceDetector("redhat-release", &RedhatReleaseNamespaceDetector{}) featurens.RegisterDetector("redhat-release", &detector{})
} }
func (detector *RedhatReleaseNamespaceDetector) Detect(data map[string][]byte) *database.Namespace { func (d detector) Detect(files tarutil.FilesMap) (*database.Namespace, error) {
for _, filePath := range detector.GetRequiredFiles() { for _, filePath := range d.RequiredFilenames() {
f, hasFile := data[filePath] f, hasFile := files[filePath]
if !hasFile { if !hasFile {
continue continue
} }
var r []string var r []string
// try for Oracle Linux // Attempt to match Oracle Linux.
r = oracleReleaseRegexp.FindStringSubmatch(string(f)) r = oracleReleaseRegexp.FindStringSubmatch(string(f))
if len(r) == 4 { if len(r) == 4 {
return &database.Namespace{ return &database.Namespace{
Name: strings.ToLower(r[1]) + ":" + r[3], Name: strings.ToLower(r[1]) + ":" + r[3],
VersionFormat: rpm.ParserName, VersionFormat: rpm.ParserName,
} }, nil
} }
// try for RHEL // Attempt to match RHEL.
r = redhatReleaseRegexp.FindStringSubmatch(string(f)) r = redhatReleaseRegexp.FindStringSubmatch(string(f))
if len(r) == 4 { if len(r) == 4 {
// TODO(vbatts) this is a hack until https://github.com/coreos/clair/pull/193 // TODO(vbatts): this is a hack until https://github.com/coreos/clair/pull/193
return &database.Namespace{ return &database.Namespace{
Name: "centos" + ":" + r[3], Name: "centos" + ":" + r[3],
VersionFormat: rpm.ParserName, VersionFormat: rpm.ParserName,
} }, nil
} }
// then try centos first // Atempt to match CentOS.
r = centosReleaseRegexp.FindStringSubmatch(string(f)) r = centosReleaseRegexp.FindStringSubmatch(string(f))
if len(r) == 4 { if len(r) == 4 {
return &database.Namespace{ return &database.Namespace{
Name: strings.ToLower(r[1]) + ":" + r[3], Name: strings.ToLower(r[1]) + ":" + r[3],
VersionFormat: rpm.ParserName, VersionFormat: rpm.ParserName,
}, nil
} }
} }
} return nil, nil
return nil
} }
// GetRequiredFiles returns the list of files that are required for Detect() func (d detector) RequiredFilenames() []string {
func (detector *RedhatReleaseNamespaceDetector) GetRequiredFiles() []string {
return []string{"etc/oracle-release", "etc/centos-release", "etc/redhat-release", "etc/system-release"} return []string{"etc/oracle-release", "etc/centos-release", "etc/redhat-release", "etc/system-release"}
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 clair authors // Copyright 2017 clair authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -18,36 +18,37 @@ import (
"testing" "testing"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/worker/detectors/namespace" "github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/pkg/tarutil"
) )
func TestRedhatReleaseNamespaceDetector(t *testing.T) { func TestDetector(t *testing.T) {
testData := []namespace.TestData{ testData := []featurens.TestData{
{ {
ExpectedNamespace: &database.Namespace{Name: "oracle:6"}, ExpectedNamespace: &database.Namespace{Name: "oracle:6"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/oracle-release": []byte(`Oracle Linux Server release 6.8`), "etc/oracle-release": []byte(`Oracle Linux Server release 6.8`),
}, },
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "oracle:7"}, ExpectedNamespace: &database.Namespace{Name: "oracle:7"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/oracle-release": []byte(`Oracle Linux Server release 7.2`), "etc/oracle-release": []byte(`Oracle Linux Server release 7.2`),
}, },
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "centos:6"}, ExpectedNamespace: &database.Namespace{Name: "centos:6"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/centos-release": []byte(`CentOS release 6.6 (Final)`), "etc/centos-release": []byte(`CentOS release 6.6 (Final)`),
}, },
}, },
{ {
ExpectedNamespace: &database.Namespace{Name: "centos:7"}, ExpectedNamespace: &database.Namespace{Name: "centos:7"},
Data: map[string][]byte{ Files: tarutil.FilesMap{
"etc/system-release": []byte(`CentOS Linux release 7.1.1503 (Core)`), "etc/system-release": []byte(`CentOS Linux release 7.1.1503 (Core)`),
}, },
}, },
} }
namespace.TestDetector(t, &RedhatReleaseNamespaceDetector{}, testData) featurens.TestDetector(t, &detector{}, testData)
} }

View File

@ -1,86 +0,0 @@
// 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"
"sync"
"github.com/coreos/clair/database"
"github.com/coreos/pkg/capnslog"
)
// The NamespaceDetector interface defines a way to detect a Namespace from input data.
// A namespace is usually made of an Operating System name and its version.
type NamespaceDetector interface {
// Detect detects a Namespace and its version from input data.
Detect(map[string][]byte) *database.Namespace
// GetRequiredFiles returns the list of files required for Detect, without
// leading /.
GetRequiredFiles() []string
}
var (
nlog = capnslog.NewPackageLogger("github.com/coreos/clair", "worker/detectors")
namespaceDetectorsLock sync.Mutex
namespaceDetectors = make(map[string]NamespaceDetector)
)
// RegisterNamespaceDetector provides a way to dynamically register an implementation of a
// NamespaceDetector.
//
// If RegisterNamespaceDetector is called twice with the same name if NamespaceDetector is nil,
// or if the name is blank, it panics.
func RegisterNamespaceDetector(name string, f NamespaceDetector) {
if name == "" {
panic("Could not register a NamespaceDetector with an empty name")
}
if f == nil {
panic("Could not register a nil NamespaceDetector")
}
namespaceDetectorsLock.Lock()
defer namespaceDetectorsLock.Unlock()
if _, alreadyExists := namespaceDetectors[name]; alreadyExists {
panic(fmt.Sprintf("Detector '%s' is already registered", name))
}
namespaceDetectors[name] = f
}
// DetectNamespace finds the OS of the layer by using every registered NamespaceDetector.
func DetectNamespace(data map[string][]byte) *database.Namespace {
for name, detector := range namespaceDetectors {
if namespace := detector.Detect(data); namespace != nil {
nlog.Debugf("detector: %q; namespace: %q\n", name, namespace.Name)
return namespace
}
}
return nil
}
// GetRequiredFilesNamespace returns the list of files required for DetectNamespace for every
// registered NamespaceDetector, without leading /.
func GetRequiredFilesNamespace() (files []string) {
for _, detector := range namespaceDetectors {
files = append(files, detector.GetRequiredFiles()...)
}
return
}

View File

@ -1,45 +0,0 @@
// Copyright 2016 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 namespace implements utilities common to implementations of
// NamespaceDetector.
package namespace
import (
"testing"
"github.com/coreos/clair/database"
"github.com/coreos/clair/worker/detectors"
"github.com/stretchr/testify/assert"
)
// TestData represents the data used to test an implementation of
// NameSpaceDetector.
type TestData struct {
Data map[string][]byte
ExpectedNamespace *database.Namespace
}
// TestDetector runs a detector on each provided instance of TestData and
// asserts the output to be equal to the expected output.
func TestDetector(t *testing.T, detector detectors.NamespaceDetector, testData []TestData) {
for _, td := range testData {
detectedNamespace := detector.Detect(td.Data)
if detectedNamespace == nil {
assert.Equal(t, td.ExpectedNamespace, detectedNamespace)
} else {
assert.Equal(t, td.ExpectedNamespace.Name, detectedNamespace.Name)
}
}
}

View File

@ -20,8 +20,10 @@ import (
"github.com/coreos/pkg/capnslog" "github.com/coreos/pkg/capnslog"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/imagefmt" "github.com/coreos/clair/ext/imagefmt"
"github.com/coreos/clair/pkg/commonerr" "github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/tarutil"
"github.com/coreos/clair/utils" "github.com/coreos/clair/utils"
"github.com/coreos/clair/worker/detectors" "github.com/coreos/clair/worker/detectors"
) )
@ -110,20 +112,23 @@ func Process(datastore database.Datastore, imageFormat, name, parentName, path s
return datastore.InsertLayer(layer) return datastore.InsertLayer(layer)
} }
// detectContent downloads a layer's archive and extracts its Namespace and Features. // detectContent downloads a layer's archive and extracts its Namespace and
// Features.
func detectContent(imageFormat, name, path string, headers map[string]string, parent *database.Layer) (namespace *database.Namespace, featureVersions []database.FeatureVersion, err error) { func detectContent(imageFormat, name, path string, headers map[string]string, parent *database.Layer) (namespace *database.Namespace, featureVersions []database.FeatureVersion, err error) {
files, err := imagefmt.Extract(imageFormat, path, headers, append(detectors.GetRequiredFilesFeatures(), detectors.GetRequiredFilesNamespace()...)) totalRequiredFiles := append(detectors.GetRequiredFilesFeatures(), featurens.RequiredFilenames()...)
files, err := imagefmt.Extract(imageFormat, path, headers, totalRequiredFiles)
if err != nil { if err != nil {
log.Errorf("layer %s: failed to extract data from %s: %s", name, utils.CleanURL(path), err) log.Errorf("layer %s: failed to extract data from %s: %s", name, utils.CleanURL(path), err)
return return
} }
data := map[string][]byte(files) namespace, err = detectNamespace(name, files, parent)
if err != nil {
// Detect namespace. return
namespace = detectNamespace(name, data, parent) }
// Detect features. // Detect features.
data := map[string][]byte(files)
featureVersions, err = detectFeatureVersions(name, data, namespace, parent) featureVersions, err = detectFeatureVersions(name, data, namespace, parent)
if err != nil { if err != nil {
return return
@ -135,15 +140,17 @@ func detectContent(imageFormat, name, path string, headers map[string]string, pa
return return
} }
func detectNamespace(name string, data map[string][]byte, parent *database.Layer) (namespace *database.Namespace) { func detectNamespace(name string, files tarutil.FilesMap, parent *database.Layer) (namespace *database.Namespace, err error) {
// Use registered detectors to get the Namespace. namespace, err = featurens.Detect(files)
namespace = detectors.DetectNamespace(data) if err != nil {
return
}
if namespace != nil { if namespace != nil {
log.Debugf("layer %s: detected namespace %q", name, namespace.Name) log.Debugf("layer %s: detected namespace %q", name, namespace.Name)
return return
} }
// Use the parent's Namespace. // Fallback to the parent's namespace.
if parent != nil { if parent != nil {
namespace = parent.Namespace namespace = parent.Namespace
if namespace != nil { if namespace != nil {