clair: Implement worker detector support

The worker is changed to accommodate the new database model and API.
Worker is refactored to move the database query helper functions to pkg.
master
Sida Chen 6 years ago
parent 48427e9b88
commit 028324014b

@ -102,27 +102,13 @@ func stopCPUProfiling(f *os.File) {
} }
func configClairVersion(config *Config) { func configClairVersion(config *Config) {
listers := featurefmt.ListListers() clair.EnabledDetectors = append(featurefmt.ListListers(), featurens.ListDetectors()...)
detectors := featurens.ListDetectors() clair.EnabledUpdaters = strutil.Intersect(config.Updater.EnabledUpdaters, vulnsrc.ListUpdaters())
updaters := vulnsrc.ListUpdaters()
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"Listers": strings.Join(listers, ","), "Detectors": database.SerializeDetectors(clair.EnabledDetectors),
"Detectors": strings.Join(detectors, ","), "Updaters": clair.EnabledUpdaters,
"Updaters": strings.Join(updaters, ","), }).Info("enabled Clair extensions")
}).Info("Clair registered components")
unregUpdaters := strutil.CompareStringLists(config.Updater.EnabledUpdaters, updaters)
if len(unregUpdaters) != 0 {
log.WithFields(log.Fields{
"Unknown Updaters": strings.Join(unregUpdaters, ","),
"Available Updaters": strings.Join(vulnsrc.ListUpdaters(), ","),
}).Fatal("Unknown or unregistered components are configured")
}
// All listers and detectors are enabled.
clair.Processors = database.Processors{Detectors: detectors, Listers: listers}
clair.EnabledUpdaters = strutil.CompareStringListsInBoth(config.Updater.EnabledUpdaters, updaters)
} }
// Boot starts Clair instance with the provided config. // Boot starts Clair instance with the provided config.
@ -147,6 +133,7 @@ func Boot(config *Config) {
defer db.Close() defer db.Close()
clair.InitWorker(db)
// Start notifier // Start notifier
st.Begin() st.Begin()
go clair.RunNotifier(config.Notifier, db, st) go clair.RunNotifier(config.Notifier, db, st)
@ -167,6 +154,18 @@ func Boot(config *Config) {
st.Stop() st.Stop()
} }
// Initialize logging system
func configureLogger(flagLogLevel *string) {
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
if err != nil {
log.WithError(err).Error("failed to set logger parser level")
}
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
}
func main() { func main() {
// Parse command-line arguments // Parse command-line arguments
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
@ -176,6 +175,7 @@ func main() {
flagInsecureTLS := flag.Bool("insecure-tls", false, "Disable TLS server's certificate chain and hostname verification when pulling layers.") flagInsecureTLS := flag.Bool("insecure-tls", false, "Disable TLS server's certificate chain and hostname verification when pulling layers.")
flag.Parse() flag.Parse()
configureLogger(flagLogLevel)
// Check for dependencies. // Check for dependencies.
for _, bin := range BinaryDependencies { for _, bin := range BinaryDependencies {
_, err := exec.LookPath(bin) _, err := exec.LookPath(bin)
@ -184,12 +184,6 @@ func main() {
} }
} }
// Initialize logging system
logLevel, err := log.ParseLevel(strings.ToUpper(*flagLogLevel))
log.SetLevel(logLevel)
log.SetOutput(os.Stdout)
log.SetFormatter(&formatter.JSONExtendedFormatter{ShowLn: true})
config, err := LoadConfig(*flagConfigPath) config, err := LoadConfig(*flagConfigPath)
if err != nil { if err != nil {
log.WithError(err).Fatal("failed to load configuration") log.WithError(err).Fatal("failed to load configuration")

@ -0,0 +1,288 @@
// Copyright 2018 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 dbutil
import (
"github.com/deckarep/golang-set"
"github.com/coreos/clair/database"
)
// DeduplicateNamespaces deduplicates a list of namespaces.
func DeduplicateNamespaces(namespaces ...database.Namespace) []database.Namespace {
nsSet := mapset.NewSet()
for _, ns := range namespaces {
nsSet.Add(ns)
}
result := make([]database.Namespace, 0, nsSet.Cardinality())
for ns := range nsSet.Iter() {
result = append(result, ns.(database.Namespace))
}
return result
}
// DeduplicateFeatures deduplicates a list of list of features.
func DeduplicateFeatures(features ...database.Feature) []database.Feature {
fSet := mapset.NewSet()
for _, f := range features {
fSet.Add(f)
}
result := make([]database.Feature, 0, fSet.Cardinality())
for f := range fSet.Iter() {
result = append(result, f.(database.Feature))
}
return result
}
// PersistPartialLayer wraps session PersistLayer function with begin and
// commit.
func PersistPartialLayer(datastore database.Datastore, layer *database.Layer) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistLayer(layer.Hash, layer.Features, layer.Namespaces, layer.By); err != nil {
return err
}
return tx.Commit()
}
// PersistFeatures wraps session PersistFeatures function with begin and commit.
func PersistFeatures(datastore database.Datastore, features []database.Feature) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistFeatures(features); err != nil {
return err
}
return tx.Commit()
}
// PersistNamespaces wraps session PersistNamespaces function with begin and
// commit.
func PersistNamespaces(datastore database.Datastore, namespaces []database.Namespace) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := tx.PersistNamespaces(namespaces); err != nil {
return err
}
return tx.Commit()
}
// FindAncestry wraps session FindAncestry function with begin and rollback.
func FindAncestry(datastore database.Datastore, name string) (database.Ancestry, bool, error) {
tx, err := datastore.Begin()
defer tx.Rollback()
if err != nil {
return database.Ancestry{}, false, err
}
return tx.FindAncestry(name)
}
// FindLayer wraps session FindLayer function with begin and rollback.
func FindLayer(datastore database.Datastore, hash string) (layer database.Layer, ok bool, err error) {
var tx database.Session
if tx, err = datastore.Begin(); err != nil {
return
}
defer tx.Rollback()
layer, ok, err = tx.FindLayer(hash)
return
}
// DeduplicateNamespacedFeatures returns a copy of all unique features in the
// input.
func DeduplicateNamespacedFeatures(features []database.NamespacedFeature) []database.NamespacedFeature {
nsSet := mapset.NewSet()
for _, ns := range features {
nsSet.Add(ns)
}
result := make([]database.NamespacedFeature, 0, nsSet.Cardinality())
for ns := range nsSet.Iter() {
result = append(result, ns.(database.NamespacedFeature))
}
return result
}
// GetAncestryFeatures returns a list of unique namespaced features in the
// ancestry.
func GetAncestryFeatures(ancestry database.Ancestry) []database.NamespacedFeature {
features := []database.NamespacedFeature{}
for _, layer := range ancestry.Layers {
features = append(features, layer.GetFeatures()...)
}
return DeduplicateNamespacedFeatures(features)
}
// UpsertAncestry wraps session UpsertAncestry function with begin and commit.
func UpsertAncestry(datastore database.Datastore, ancestry database.Ancestry) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
if err = tx.UpsertAncestry(ancestry); err != nil {
tx.Rollback()
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
// PersistNamespacedFeatures wraps session PersistNamespacedFeatures function
// with begin and commit.
func PersistNamespacedFeatures(datastore database.Datastore, features []database.NamespacedFeature) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
if err := tx.PersistNamespacedFeatures(features); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
// CacheRelatedVulnerability wraps session CacheAffectedNamespacedFeatures
// function with begin and commit.
func CacheRelatedVulnerability(datastore database.Datastore, features []database.NamespacedFeature) error {
tx, err := datastore.Begin()
if err != nil {
return err
}
if err := tx.CacheAffectedNamespacedFeatures(features); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
// IntersectDetectors returns the detectors in both d1 and d2.
func IntersectDetectors(d1 []database.Detector, d2 []database.Detector) []database.Detector {
d1Set := mapset.NewSet()
for _, d := range d1 {
d1Set.Add(d)
}
d2Set := mapset.NewSet()
for _, d := range d2 {
d2Set.Add(d)
}
inter := d1Set.Intersect(d2Set)
result := make([]database.Detector, 0, inter.Cardinality())
for d := range inter.Iter() {
result = append(result, d.(database.Detector))
}
return result
}
// DiffDetectors returns the detectors belongs to d1 but not d2
func DiffDetectors(d1 []database.Detector, d2 []database.Detector) []database.Detector {
d1Set := mapset.NewSet()
for _, d := range d1 {
d1Set.Add(d)
}
d2Set := mapset.NewSet()
for _, d := range d2 {
d2Set.Add(d)
}
diff := d1Set.Difference(d2Set)
result := make([]database.Detector, 0, diff.Cardinality())
for d := range diff.Iter() {
result = append(result, d.(database.Detector))
}
return result
}
// MergeLayers merges all content in new layer to l, where the content is
// updated.
func MergeLayers(l *database.Layer, new *database.Layer) *database.Layer {
featureSet := mapset.NewSet()
namespaceSet := mapset.NewSet()
bySet := mapset.NewSet()
for _, f := range l.Features {
featureSet.Add(f)
}
for _, ns := range l.Namespaces {
namespaceSet.Add(ns)
}
for _, d := range l.By {
bySet.Add(d)
}
for _, feature := range new.Features {
if !featureSet.Contains(feature) {
l.Features = append(l.Features, feature)
featureSet.Add(feature)
}
}
for _, namespace := range new.Namespaces {
if !namespaceSet.Contains(namespace) {
l.Namespaces = append(l.Namespaces, namespace)
namespaceSet.Add(namespace)
}
}
for _, detector := range new.By {
if !bySet.Contains(detector) {
l.By = append(l.By, detector)
bySet.Add(detector)
}
}
return l
}

@ -14,42 +14,46 @@
package strutil package strutil
// CompareStringLists returns the strings that are present in X but not in Y. import (
func CompareStringLists(X, Y []string) []string { "regexp"
m := make(map[string]bool)
for _, y := range Y { set "github.com/deckarep/golang-set"
m[y] = true )
}
var urlParametersRegexp = regexp.MustCompile(`(\?|\&)([^=]+)\=([^ &]+)`)
diff := []string{} func convertToSet(X []string) set.Set {
s := set.NewSet()
for _, x := range X { for _, x := range X {
if m[x] { s.Add(x)
continue }
} return s
}
diff = append(diff, x) func setToStringSlice(s set.Set) []string {
m[x] = true strs := make([]string, 0, s.Cardinality())
for _, str := range s.ToSlice() {
strs = append(strs, str.(string))
} }
return diff return strs
} }
// CompareStringListsInBoth returns the strings that are present in both X and Y. // Difference returns the strings that are present in X but not in Y.
func CompareStringListsInBoth(X, Y []string) []string { func Difference(X, Y []string) []string {
m := make(map[string]struct{}) x := convertToSet(X)
y := convertToSet(Y)
for _, y := range Y { return setToStringSlice(x.Difference(y))
m[y] = struct{}{} }
}
diff := []string{} // Intersect returns the strings that are present in both X and Y.
for _, x := range X { func Intersect(X, Y []string) []string {
if _, e := m[x]; e { x := convertToSet(X)
diff = append(diff, x) y := convertToSet(Y)
delete(m, x) return setToStringSlice(x.Intersect(y))
} }
}
return diff // CleanURL removes all parameters from an URL.
func CleanURL(str string) string {
return urlParametersRegexp.ReplaceAllString(str, "")
} }

@ -21,12 +21,12 @@ import (
) )
func TestStringComparison(t *testing.T) { func TestStringComparison(t *testing.T) {
cmp := CompareStringLists([]string{"a", "b", "b", "a"}, []string{"a", "c"}) cmp := Difference([]string{"a", "b", "b", "a"}, []string{"a", "c"})
assert.Len(t, cmp, 1) assert.Len(t, cmp, 1)
assert.NotContains(t, cmp, "a") assert.NotContains(t, cmp, "a")
assert.Contains(t, cmp, "b") assert.Contains(t, cmp, "b")
cmp = CompareStringListsInBoth([]string{"a", "a", "b", "c"}, []string{"a", "c", "c"}) cmp = Intersect([]string{"a", "a", "b", "c"}, []string{"a", "c", "c"})
assert.Len(t, cmp, 2) assert.Len(t, cmp, 2)
assert.NotContains(t, cmp, "b") assert.NotContains(t, cmp, "b")
assert.Contains(t, cmp, "a") assert.Contains(t, cmp, "a")

@ -21,6 +21,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/coreos/clair/pkg/dbutil"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -208,7 +210,7 @@ func update(datastore database.Datastore, firstUpdate bool) {
namespaces = append(namespaces, ns) namespaces = append(namespaces, ns)
} }
if err := persistNamespaces(datastore, namespaces); err != nil { if err := dbutil.PersistNamespaces(datastore, namespaces); err != nil {
log.WithError(err).Error("Unable to insert namespaces") log.WithError(err).Error("Unable to insert namespaces")
return return
} }

@ -16,9 +16,9 @@ package clair
import ( import (
"errors" "errors"
"regexp"
"sync" "sync"
"github.com/deckarep/golang-set"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
@ -26,11 +26,9 @@ import (
"github.com/coreos/clair/ext/featurens" "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/dbutil"
"github.com/coreos/clair/pkg/strutil" "github.com/coreos/clair/pkg/strutil"
) "github.com/coreos/clair/pkg/tarutil"
const (
logLayerName = "layer"
) )
var ( var (
@ -38,18 +36,8 @@ var (
// manager is not supported. // manager is not supported.
ErrUnsupported = commonerr.NewBadRequestError("worker: OS and/or package manager are not supported") ErrUnsupported = commonerr.NewBadRequestError("worker: OS and/or package manager are not supported")
// ErrParentUnknown is the error that should be raised when a parent layer // EnabledDetectors are detectors to be used to scan the layers.
// has yet to be processed for the current layer. EnabledDetectors []database.Detector
ErrParentUnknown = commonerr.NewBadRequestError("worker: parent layer is unknown, it must be processed first")
urlParametersRegexp = regexp.MustCompile(`(\?|\&)([^=]+)\=([^ &]+)`)
// Processors contain the names of namespace detectors and feature listers
// enabled in this instance of Clair.
//
// Processors are initialized during booting and configured in the
// configuration file.
Processors database.Processors
) )
// LayerRequest represents all information necessary to download and process a // LayerRequest represents all information necessary to download and process a
@ -60,296 +48,176 @@ type LayerRequest struct {
Headers map[string]string Headers map[string]string
} }
// partialLayer stores layer's content detected by `processedBy` processors. type processResult struct {
type partialLayer struct { existingLayer *database.Layer
hash string newLayerContent *database.Layer
processedBy database.Processors err error
namespaces []database.Namespace
features []database.Feature
err error
} }
// processRequest stores parameters used for processing layers. // processRequest stores parameters used for processing a layer.
type processRequest struct { type processRequest struct {
request LayerRequest LayerRequest
// notProcessedBy represents a set of processors used to process the
// request. existingLayer *database.Layer
notProcessedBy database.Processors detectors []database.Detector
} }
// cleanURL removes all parameters from an URL. type introducedFeature struct {
func cleanURL(str string) string { feature database.AncestryFeature
return urlParametersRegexp.ReplaceAllString(str, "") layerIndex int
} }
// processLayers in parallel processes a set of requests for unique set of layers // processRequests in parallel processes a set of requests for unique set of layers
// and returns sets of unique namespaces, features and layers to be inserted // and returns sets of unique namespaces, features and layers to be inserted
// into the database. // into the database.
func processRequests(imageFormat string, toDetect []processRequest) ([]database.Namespace, []database.Feature, map[string]partialLayer, error) { func processRequests(imageFormat string, toDetect map[string]*processRequest) (map[string]*processResult, error) {
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
wg.Add(len(toDetect)) wg.Add(len(toDetect))
results := make([]partialLayer, len(toDetect))
results := map[string]*processResult{}
for i := range toDetect {
results[i] = nil
}
for i := range toDetect { for i := range toDetect {
go func(req *processRequest, res *partialLayer) { result := processResult{}
res.hash = req.request.Hash results[i] = &result
res.processedBy = req.notProcessedBy go func(req *processRequest, res *processResult) {
res.namespaces, res.features, res.err = detectContent(imageFormat, req.request.Hash, req.request.Path, req.request.Headers, req.notProcessedBy) *res = *detectContent(imageFormat, req)
wg.Done() wg.Done()
}(&toDetect[i], &results[i]) }(toDetect[i], &result)
} }
wg.Wait()
distinctNS := map[database.Namespace]struct{}{}
distinctF := map[database.Feature]struct{}{}
wg.Wait()
errs := []error{} errs := []error{}
for _, r := range results { for _, r := range results {
errs = append(errs, r.err) errs = append(errs, r.err)
} }
if err := commonerr.CombineErrors(errs...); err != nil { if err := commonerr.CombineErrors(errs...); err != nil {
return nil, nil, nil, err return nil, err
}
updates := map[string]partialLayer{}
for _, r := range results {
for _, ns := range r.namespaces {
distinctNS[ns] = struct{}{}
}
for _, f := range r.features {
distinctF[f] = struct{}{}
}
if _, ok := updates[r.hash]; !ok {
updates[r.hash] = r
} else {
return nil, nil, nil, errors.New("Duplicated updates is not allowed")
}
}
namespaces := make([]database.Namespace, 0, len(distinctNS))
features := make([]database.Feature, 0, len(distinctF))
for ns := range distinctNS {
namespaces = append(namespaces, ns)
} }
for f := range distinctF { return results, nil
features = append(features, f)
}
return namespaces, features, updates, nil
} }
func getLayer(datastore database.Datastore, req LayerRequest) (layer database.Layer, preq *processRequest, err error) { func getProcessRequest(datastore database.Datastore, req LayerRequest) (preq *processRequest, err error) {
var ( layer, ok, err := dbutil.FindLayer(datastore, req.Hash)
tx database.Session if err != nil {
ok bool
)
if tx, err = datastore.Begin(); err != nil {
return
}
defer tx.Rollback()
if layer, ok, err = tx.FindLayer(req.Hash); err != nil {
return return
} }
if !ok { if !ok {
layer = database.Layer{ log.WithField("layer", req.Hash).Debug("found no existing layer in database")
LayerMetadata: database.LayerMetadata{
Hash: req.Hash,
},
}
preq = &processRequest{ preq = &processRequest{
request: req, LayerRequest: req,
notProcessedBy: Processors, existingLayer: &database.Layer{Hash: req.Hash},
detectors: EnabledDetectors,
} }
} else { } else {
notProcessed := getNotProcessedBy(layer.ProcessedBy) log.WithFields(log.Fields{
if !(len(notProcessed.Detectors) == 0 && len(notProcessed.Listers) == 0 && ok) { "layer": layer.Hash,
preq = &processRequest{ "detectors": layer.By,
request: req, "feature count": len(layer.Features),
notProcessedBy: notProcessed, "namespace count": len(layer.Namespaces),
} }).Debug("found existing layer in database")
preq = &processRequest{
LayerRequest: req,
existingLayer: &layer,
detectors: dbutil.DiffDetectors(EnabledDetectors, layer.By),
} }
} }
return return
} }
// processLayers processes a set of post layer requests, stores layers and func persistProcessResult(datastore database.Datastore, results map[string]*processResult) error {
// returns an ordered list of processed layers with detected features and features := []database.Feature{}
// namespaces. namespaces := []database.Namespace{}
func processLayers(datastore database.Datastore, imageFormat string, requests []LayerRequest) ([]database.Layer, error) { for _, r := range results {
toDetect := []processRequest{} features = append(features, r.newLayerContent.GetFeatures()...)
layers := map[string]database.Layer{} namespaces = append(namespaces, r.newLayerContent.GetNamespaces()...)
for _, req := range requests {
if _, ok := layers[req.Hash]; ok {
continue
}
layer, preq, err := getLayer(datastore, req)
if err != nil {
return nil, err
}
layers[req.Hash] = layer
if preq != nil {
toDetect = append(toDetect, *preq)
}
}
namespaces, features, partialLayers, err := processRequests(imageFormat, toDetect)
if err != nil {
return nil, err
} }
// Store partial results. features = dbutil.DeduplicateFeatures(features...)
if err := persistNamespaces(datastore, namespaces); err != nil { namespaces = dbutil.DeduplicateNamespaces(namespaces...)
return nil, err if err := dbutil.PersistNamespaces(datastore, namespaces); err != nil {
return err
} }
if err := persistFeatures(datastore, features); err != nil { if err := dbutil.PersistFeatures(datastore, features); err != nil {
return nil, err return err
} }
for _, layer := range partialLayers { for _, layer := range results {
if err := persistPartialLayer(datastore, layer); err != nil { if err := dbutil.PersistPartialLayer(datastore, layer.newLayerContent); err != nil {
return nil, err return err
}
log.WithFields(log.Fields{
"Hash": layer.hash,
"namespace count": len(layer.namespaces),
"feature count": len(layer.features),
"namespace detectors": layer.processedBy.Detectors,
"feature listers": layer.processedBy.Listers,
}).Debug("saved layer")
}
// NOTE(Sida): The full layers are computed using partially
// processed layers in current database session. If any other instances of
// Clair are changing some layers in this set of layers, it might generate
// different results especially when the other Clair is with different
// processors.
completeLayers := []database.Layer{}
for _, req := range requests {
if partialLayer, ok := partialLayers[req.Hash]; ok {
completeLayers = append(completeLayers, combineLayers(layers[req.Hash], partialLayer))
} else {
completeLayers = append(completeLayers, layers[req.Hash])
} }
} }
return completeLayers, nil return nil
} }
func persistPartialLayer(datastore database.Datastore, layer partialLayer) error { // processLayers processes a set of post layer requests, stores layers and
tx, err := datastore.Begin() // returns an ordered list of processed layers with detected features and
if err != nil { // namespaces.
return err func processLayers(datastore database.Datastore, imageFormat string, requests []LayerRequest) ([]database.Layer, error) {
} var (
defer tx.Rollback() reqMap = make(map[string]*processRequest)
err error
)
if err := tx.PersistLayer(layer.hash, layer.namespaces, layer.features, layer.processedBy); err != nil { for _, r := range requests {
return err reqMap[r.Hash], err = getProcessRequest(datastore, r)
if err != nil {
return nil, err
}
} }
return tx.Commit() results, err := processRequests(imageFormat, reqMap)
}
func persistFeatures(datastore database.Datastore, features []database.Feature) error {
tx, err := datastore.Begin()
if err != nil { if err != nil {
return err return nil, err
} }
defer tx.Rollback()
if err := tx.PersistFeatures(features); err != nil { if err := persistProcessResult(datastore, results); err != nil {
return err return nil, err
}
return tx.Commit()
}
func persistNamespaces(datastore database.Datastore, namespaces []database.Namespace) error {
tx, err := datastore.Begin()
if err != nil {
return err
} }
defer tx.Rollback()
if err := tx.PersistNamespaces(namespaces); err != nil { completeLayers := getProcessResultLayers(results)
return err layers := make([]database.Layer, 0, len(requests))
for _, r := range requests {
layers = append(layers, completeLayers[r.Hash])
} }
return tx.Commit() return layers, nil
} }
// combineLayers merges `layer` and `partial` without duplicated content. func getProcessResultLayers(results map[string]*processResult) map[string]database.Layer {
func combineLayers(layer database.Layer, partial partialLayer) database.Layer { layers := map[string]database.Layer{}
mapF := map[database.Feature]struct{}{} for name, r := range results {
mapNS := map[database.Namespace]struct{}{} layers[name] = *dbutil.MergeLayers(r.existingLayer, r.newLayerContent)
for _, f := range layer.Features {
mapF[f] = struct{}{}
}
for _, ns := range layer.Namespaces {
mapNS[ns] = struct{}{}
}
for _, f := range partial.features {
mapF[f] = struct{}{}
}
for _, ns := range partial.namespaces {
mapNS[ns] = struct{}{}
}
features := make([]database.Feature, 0, len(mapF))
namespaces := make([]database.Namespace, 0, len(mapNS))
for f := range mapF {
features = append(features, f)
}
for ns := range mapNS {
namespaces = append(namespaces, ns)
} }
layer.ProcessedBy.Detectors = append(layer.ProcessedBy.Detectors, strutil.CompareStringLists(partial.processedBy.Detectors, layer.ProcessedBy.Detectors)...) return layers
layer.ProcessedBy.Listers = append(layer.ProcessedBy.Listers, strutil.CompareStringLists(partial.processedBy.Listers, layer.ProcessedBy.Listers)...)
return database.Layer{
LayerMetadata: database.LayerMetadata{
Hash: layer.Hash,
ProcessedBy: layer.ProcessedBy,
},
Features: features,
Namespaces: namespaces,
}
} }
func isAncestryProcessed(datastore database.Datastore, name string) (bool, error) { func isAncestryProcessed(datastore database.Datastore, name string) (bool, error) {
tx, err := datastore.Begin() ancestry, ok, err := dbutil.FindAncestry(datastore, name)
if err != nil { if err != nil || !ok {
return false, err return ok, err
}
defer tx.Rollback()
ancestry, ok, err := tx.FindAncestry(name)
if err != nil {
return false, err
}
if !ok {
return false, nil
} }
notProcessed := getNotProcessedBy(ancestry.ProcessedBy) return len(dbutil.DiffDetectors(EnabledDetectors, ancestry.By)) == 0, nil
return len(notProcessed.Detectors) == 0 && len(notProcessed.Listers) == 0, nil
} }
// ProcessAncestry downloads and scans an ancestry if it's not scanned by all // ProcessAncestry downloads and scans an ancestry if it's not scanned by all
// enabled processors in this instance of Clair. // enabled processors in this instance of Clair.
func ProcessAncestry(datastore database.Datastore, imageFormat, name string, layerRequest []LayerRequest) error { func ProcessAncestry(datastore database.Datastore, imageFormat, name string, layerRequest []LayerRequest) error {
var ( var (
err error err error
ok bool ok bool
layers []database.Layer layers []database.Layer
commonProcessors database.Processors
) )
if name == "" { if name == "" {
@ -360,10 +228,12 @@ func ProcessAncestry(datastore database.Datastore, imageFormat, name string, lay
return commonerr.NewBadRequestError("could not process a layer which does not have a format") return commonerr.NewBadRequestError("could not process a layer which does not have a format")
} }
log.WithField("ancestry", name).Debug("start processing ancestry...")
if ok, err = isAncestryProcessed(datastore, name); err != nil { if ok, err = isAncestryProcessed(datastore, name); err != nil {
log.WithError(err).Error("could not determine if ancestry is processed")
return err return err
} else if ok { } else if ok {
log.WithField("name", name).Debug("ancestry is already processed") log.WithField("ancestry", name).Debug("ancestry is already processed")
return nil return nil
} }
@ -371,155 +241,100 @@ func ProcessAncestry(datastore database.Datastore, imageFormat, name string, lay
return err return err
} }
if commonProcessors, err = getProcessors(layers); err != nil { return processAncestry(datastore, name, layers)
return err
}
return processAncestry(datastore, name, layers, commonProcessors)
}
// getNamespacedFeatures extracts the namespaced features introduced in each
// layer into one array.
func getNamespacedFeatures(layers []database.AncestryLayer) []database.NamespacedFeature {
features := []database.NamespacedFeature{}
for _, layer := range layers {
features = append(features, layer.DetectedFeatures...)
}
return features
} }
func processAncestry(datastore database.Datastore, name string, layers []database.Layer, commonProcessors database.Processors) error { func processAncestry(datastore database.Datastore, name string, layers []database.Layer) error {
var ( var (
ancestry database.Ancestry ancestry = database.Ancestry{Name: name}
err error err error
) )
ancestry.Name = name ancestry.Layers, ancestry.By, err = computeAncestryLayers(layers)
ancestry.ProcessedBy = commonProcessors
ancestry.Layers, err = computeAncestryLayers(layers, commonProcessors)
if err != nil { if err != nil {
return err return err
} }
ancestryFeatures := getNamespacedFeatures(ancestry.Layers) ancestryFeatures := dbutil.GetAncestryFeatures(ancestry)
log.WithFields(log.Fields{ log.WithFields(log.Fields{
"ancestry": name, "ancestry": name,
"number of features": len(ancestryFeatures), "processed by": EnabledDetectors,
"processed by": Processors, "features count": len(ancestryFeatures),
"number of layers": len(ancestry.Layers), "layer count": len(ancestry.Layers),
}).Debug("compute ancestry features") }).Debug("compute ancestry features")
if err := persistNamespacedFeatures(datastore, ancestryFeatures); err != nil { if err := dbutil.PersistNamespacedFeatures(datastore, ancestryFeatures); err != nil {
log.WithField("ancestry", name).WithError(err).Error("could not persist namespaced features for ancestry")
return err return err
} }
tx, err := datastore.Begin() if err := dbutil.CacheRelatedVulnerability(datastore, ancestryFeatures); err != nil {
if err != nil { log.WithField("ancestry", name).WithError(err).Error("failed to cache feature related vulnerability")
return err return err
} }
err = tx.UpsertAncestry(ancestry) if err := dbutil.UpsertAncestry(datastore, ancestry); err != nil {
if err != nil { log.WithField("ancestry", name).WithError(err).Error("could not upsert ancestry")
tx.Rollback()
return err return err
} }
err = tx.Commit()
if err != nil {
return err
}
return nil return nil
} }
func persistNamespacedFeatures(datastore database.Datastore, features []database.NamespacedFeature) error { func getCommonDetectors(layers []database.Layer) mapset.Set {
tx, err := datastore.Begin() // find the common detector for all layers and filter the namespaces and
if err != nil { // features based on that.
return err commonDetectors := mapset.NewSet()
} for _, d := range layers[0].By {
commonDetectors.Add(d)
if err := tx.PersistNamespacedFeatures(features); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return err
}
tx, err = datastore.Begin()
if err != nil {
return err
} }
if err := tx.CacheAffectedNamespacedFeatures(features); err != nil { for _, l := range layers {
tx.Rollback() detectors := mapset.NewSet()
return err for _, d := range l.By {
} detectors.Add(d)
return tx.Commit()
}
// getProcessors retrieves common subset of the processors of each layer.
func getProcessors(layers []database.Layer) (database.Processors, error) {
if len(layers) == 0 {
return database.Processors{}, nil
}
detectors := layers[0].ProcessedBy.Detectors
listers := layers[0].ProcessedBy.Listers
detectorsLen := len(detectors)
listersLen := len(listers)
for _, l := range layers[1:] {
detectors := strutil.CompareStringListsInBoth(detectors, l.ProcessedBy.Detectors)
listers := strutil.CompareStringListsInBoth(listers, l.ProcessedBy.Listers)
if len(detectors) != detectorsLen || len(listers) != listersLen {
// This error might be triggered because of multiple workers are
// processing the same instance with different processors.
// TODO(sidchen): Once the features can be associated with
// Detectors/Listers, we can support dynamically generating ancestry's
// detector/lister based on the layers.
return database.Processors{}, errors.New("processing layers with different Clair instances is currently unsupported")
} }
commonDetectors = commonDetectors.Intersect(detectors)
} }
return database.Processors{
Detectors: detectors,
Listers: listers,
}, nil
}
type introducedFeature struct { return commonDetectors
feature database.NamespacedFeature
layerIndex int
} }
// computeAncestryLayers computes ancestry's layers along with what features are // computeAncestryLayers computes ancestry's layers along with what features are
// introduced. // introduced.
func computeAncestryLayers(layers []database.Layer, commonProcessors database.Processors) ([]database.AncestryLayer, error) { func computeAncestryLayers(layers []database.Layer) ([]database.AncestryLayer, []database.Detector, error) {
// TODO(sidchen): Once the features are linked to specific processor, we if len(layers) == 0 {
// will use commonProcessors to filter out the features for this ancestry. return nil, nil, nil
}
commonDetectors := getCommonDetectors(layers)
// version format -> namespace // version format -> namespace
namespaces := map[string]database.Namespace{} namespaces := map[string]database.LayerNamespace{}
// version format -> feature ID -> feature // version format -> feature ID -> feature
features := map[string]map[string]introducedFeature{} features := map[string]map[string]introducedFeature{}
ancestryLayers := []database.AncestryLayer{} ancestryLayers := []database.AncestryLayer{}
for index, layer := range layers { for index, layer := range layers {
// Initialize the ancestry Layer initializedLayer := database.AncestryLayer{Hash: layer.Hash}
initializedLayer := database.AncestryLayer{LayerMetadata: layer.LayerMetadata, DetectedFeatures: []database.NamespacedFeature{}}
ancestryLayers = append(ancestryLayers, initializedLayer) ancestryLayers = append(ancestryLayers, initializedLayer)
// Precondition: namespaces and features contain the result from union // Precondition: namespaces and features contain the result from union
// of all parents. // of all parents.
for _, ns := range layer.Namespaces { for _, ns := range layer.Namespaces {
if !commonDetectors.Contains(ns.By) {
continue
}
namespaces[ns.VersionFormat] = ns namespaces[ns.VersionFormat] = ns
} }
// version format -> feature ID -> feature // version format -> feature ID -> feature
currentFeatures := map[string]map[string]introducedFeature{} currentFeatures := map[string]map[string]introducedFeature{}
for _, f := range layer.Features { for _, f := range layer.Features {
if !commonDetectors.Contains(f.By) {
continue
}
if ns, ok := namespaces[f.VersionFormat]; ok { if ns, ok := namespaces[f.VersionFormat]; ok {
var currentMap map[string]introducedFeature var currentMap map[string]introducedFeature
if currentMap, ok = currentFeatures[f.VersionFormat]; !ok { if currentMap, ok = currentFeatures[f.VersionFormat]; !ok {
@ -537,16 +352,20 @@ func computeAncestryLayers(layers []database.Layer, commonProcessors database.Pr
if !inherited { if !inherited {
currentMap[f.Name+":"+f.Version] = introducedFeature{ currentMap[f.Name+":"+f.Version] = introducedFeature{
feature: database.NamespacedFeature{ feature: database.AncestryFeature{
Feature: f, NamespacedFeature: database.NamespacedFeature{
Namespace: ns, Feature: f.Feature,
Namespace: ns.Namespace,
},
NamespaceBy: ns.By,
FeatureBy: f.By,
}, },
layerIndex: index, layerIndex: index,
} }
} }
} else { } else {
return nil, errors.New("No corresponding version format") return nil, nil, errors.New("No corresponding version format")
} }
} }
@ -564,57 +383,97 @@ func computeAncestryLayers(layers []database.Layer, commonProcessors database.Pr
for _, featureMap := range features { for _, featureMap := range features {
for _, feature := range featureMap { for _, feature := range featureMap {
ancestryLayers[feature.layerIndex].DetectedFeatures = append( ancestryLayers[feature.layerIndex].Features = append(
ancestryLayers[feature.layerIndex].DetectedFeatures, ancestryLayers[feature.layerIndex].Features,
feature.feature, feature.feature,
) )
} }
} }
return ancestryLayers, nil detectors := make([]database.Detector, 0, commonDetectors.Cardinality())
for d := range commonDetectors.Iter() {
detectors = append(detectors, d.(database.Detector))
}
return ancestryLayers, detectors, nil
} }
// getNotProcessedBy returns a processors, which contains the detectors and func extractRequiredFiles(imageFormat string, req *processRequest) (tarutil.FilesMap, error) {
// listers not in `processedBy` but implemented in the current clair instance. requiredFiles := append(featurefmt.RequiredFilenames(req.detectors), featurens.RequiredFilenames(req.detectors)...)
func getNotProcessedBy(processedBy database.Processors) database.Processors { if len(requiredFiles) == 0 {
notProcessedLister := strutil.CompareStringLists(Processors.Listers, processedBy.Listers) log.WithFields(log.Fields{
notProcessedDetector := strutil.CompareStringLists(Processors.Detectors, processedBy.Detectors) "layer": req.Hash,
return database.Processors{ "detectors": req.detectors,
Listers: notProcessedLister, }).Info("layer requires no file to extract")
Detectors: notProcessedDetector, return make(tarutil.FilesMap), nil
} }
}
// detectContent downloads a layer and detects all features and namespaces. files, err := imagefmt.Extract(imageFormat, req.Path, req.Headers, requiredFiles)
func detectContent(imageFormat, name, path string, headers map[string]string, toProcess database.Processors) (namespaces []database.Namespace, featureVersions []database.Feature, err error) {
log.WithFields(log.Fields{"Hash": name}).Debug("Process Layer")
totalRequiredFiles := append(featurefmt.RequiredFilenames(toProcess.Listers), featurens.RequiredFilenames(toProcess.Detectors)...)
files, err := imagefmt.Extract(imageFormat, path, headers, totalRequiredFiles)
if err != nil { if err != nil {
log.WithError(err).WithFields(log.Fields{ log.WithError(err).WithFields(log.Fields{
logLayerName: name, "layer": req.Hash,
"path": cleanURL(path), "path": strutil.CleanURL(req.Path),
}).Error("failed to extract data from path") }).Error("failed to extract data from path")
return nil, err
}
return files, err
}
// detectContent downloads a layer and detects all features and namespaces.
func detectContent(imageFormat string, req *processRequest) (res *processResult) {
var (
files tarutil.FilesMap
layer = database.Layer{Hash: req.Hash, By: req.detectors}
)
res = &processResult{req.existingLayer, &layer, nil}
log.WithFields(log.Fields{
"layer": req.Hash,
"detectors": req.detectors,
}).Info("detecting layer content...")
files, res.err = extractRequiredFiles(imageFormat, req)
if res.err != nil {
return return
} }
namespaces, err = featurens.Detect(files, toProcess.Detectors) if layer.Namespaces, res.err = featurens.Detect(files, req.detectors); res.err != nil {
if err != nil {
return return
} }
if len(featureVersions) > 0 { if layer.Features, res.err = featurefmt.ListFeatures(files, req.detectors); res.err != nil {
log.WithFields(log.Fields{logLayerName: name, "count": len(namespaces)}).Debug("detected layer namespaces") return
} }
featureVersions, err = featurefmt.ListFeatures(files, toProcess.Listers) log.WithFields(log.Fields{
if err != nil { "layer": req.Hash,
"detectors": req.detectors,
"namespace count": len(layer.Namespaces),
"feature count": len(layer.Features),
}).Info("processed layer")
return
}
// InitWorker initializes the worker.
func InitWorker(datastore database.Datastore) {
if len(EnabledDetectors) == 0 {
log.Warn("no enabled detector, and therefore, no ancestry will be processed.")
return return
} }
if len(featureVersions) > 0 { tx, err := datastore.Begin()
log.WithFields(log.Fields{logLayerName: name, "count": len(featureVersions)}).Debug("detected layer features") if err != nil {
log.WithError(err).Fatal("cannot connect to database to initialize worker")
} }
return defer tx.Rollback()
if err := tx.PersistDetectors(EnabledDetectors); err != nil {
log.WithError(err).Fatal("cannot insert detectors to initialize worker")
}
if err := tx.Commit(); err != nil {
log.WithError(err).Fatal("cannot commit detector changes to initialize worker")
}
} }

@ -22,12 +22,14 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/ext/featurefmt" "github.com/coreos/clair/ext/featurefmt"
"github.com/coreos/clair/ext/featurens" "github.com/coreos/clair/ext/featurens"
"github.com/coreos/clair/ext/versionfmt/dpkg" "github.com/coreos/clair/ext/versionfmt/dpkg"
"github.com/coreos/clair/pkg/strutil" "github.com/coreos/clair/pkg/dbutil"
"github.com/coreos/clair/pkg/testutil"
// Register the required detectors. // Register the required detectors.
_ "github.com/coreos/clair/ext/featurefmt/dpkg" _ "github.com/coreos/clair/ext/featurefmt/dpkg"
@ -58,55 +60,27 @@ type mockSession struct {
func copyDatastore(md *mockDatastore) mockDatastore { func copyDatastore(md *mockDatastore) mockDatastore {
layers := map[string]database.Layer{} layers := map[string]database.Layer{}
for k, l := range md.layers { for k, l := range md.layers {
features := append([]database.Feature(nil), l.Features...)
namespaces := append([]database.Namespace(nil), l.Namespaces...)
listers := append([]string(nil), l.ProcessedBy.Listers...)
detectors := append([]string(nil), l.ProcessedBy.Detectors...)
layers[k] = database.Layer{ layers[k] = database.Layer{
LayerMetadata: database.LayerMetadata{ Hash: l.Hash,
Hash: l.Hash, By: append([]database.Detector{}, l.By...),
ProcessedBy: database.Processors{ Features: append([]database.LayerFeature{}, l.Features...),
Listers: listers, Namespaces: append([]database.LayerNamespace{}, l.Namespaces...),
Detectors: detectors,
},
},
Features: features,
Namespaces: namespaces,
} }
} }
ancestry := map[string]database.Ancestry{} ancestry := map[string]database.Ancestry{}
for k, a := range md.ancestry { for k, a := range md.ancestry {
ancestryLayers := []database.AncestryLayer{} ancestryLayers := []database.AncestryLayer{}
layers := []database.LayerMetadata{}
for _, layer := range a.Layers { for _, layer := range a.Layers {
layers = append(layers, database.LayerMetadata{
Hash: layer.Hash,
ProcessedBy: database.Processors{
Detectors: append([]string(nil), layer.LayerMetadata.ProcessedBy.Detectors...),
Listers: append([]string(nil), layer.LayerMetadata.ProcessedBy.Listers...),
},
})
ancestryLayers = append(ancestryLayers, database.AncestryLayer{ ancestryLayers = append(ancestryLayers, database.AncestryLayer{
LayerMetadata: database.LayerMetadata{ Hash: layer.Hash,
Hash: layer.Hash, Features: append([]database.AncestryFeature{}, layer.Features...),
ProcessedBy: database.Processors{
Detectors: append([]string(nil), layer.LayerMetadata.ProcessedBy.Detectors...),
Listers: append([]string(nil), layer.LayerMetadata.ProcessedBy.Listers...),
},
},
DetectedFeatures: append([]database.NamespacedFeature(nil), layer.DetectedFeatures...),
}) })
} }
ancestry[k] = database.Ancestry{ ancestry[k] = database.Ancestry{
Name: a.Name, Name: a.Name,
ProcessedBy: database.Processors{ By: append([]database.Detector{}, a.By...),
Detectors: append([]string(nil), a.ProcessedBy.Detectors...),
Listers: append([]string(nil), a.ProcessedBy.Listers...),
},
Layers: ancestryLayers, Layers: ancestryLayers,
} }
} }
@ -125,6 +99,7 @@ func copyDatastore(md *mockDatastore) mockDatastore {
for k, f := range md.namespacedFeatures { for k, f := range md.namespacedFeatures {
namespacedFeatures[k] = f namespacedFeatures[k] = f
} }
return mockDatastore{ return mockDatastore{
layers: layers, layers: layers,
ancestry: ancestry, ancestry: ancestry,
@ -194,10 +169,7 @@ func newMockDatastore() *mockDatastore {
return errSessionDone return errSessionDone
} }
for _, n := range ns { for _, n := range ns {
_, ok := session.copy.namespaces[n.Name] session.copy.namespaces[NamespaceKey(&n)] = n
if !ok {
session.copy.namespaces[n.Name] = n
}
} }
return nil return nil
} }
@ -207,63 +179,36 @@ func newMockDatastore() *mockDatastore {
return errSessionDone return errSessionDone
} }
for _, f := range fs { for _, f := range fs {
key := FeatureKey(&f) session.copy.features[FeatureKey(&f)] = f
_, ok := session.copy.features[key]
if !ok {
session.copy.features[key] = f
}
} }
return nil return nil
} }
session.FctPersistLayer = func(hash string, namespaces []database.Namespace, features []database.Feature, processedBy database.Processors) error { session.FctPersistLayer = func(hash string, features []database.LayerFeature, namespaces []database.LayerNamespace, by []database.Detector) error {
if session.terminated { if session.terminated {
return errSessionDone return errSessionDone
} }
// update the layer
_, ok := session.copy.layers[hash]
if !ok {
session.copy.layers[hash] = database.Layer{}
}
layer, ok := session.copy.layers[hash]
if !ok {
return errors.New("Failed to insert layer")
}
layerFeatures := map[string]database.Feature{}
layerNamespaces := map[string]database.Namespace{}
for _, f := range layer.Features {
layerFeatures[FeatureKey(&f)] = f
}
for _, n := range layer.Namespaces {
layerNamespaces[n.Name] = n
}
// ensure that all the namespaces, features are in the database
for _, ns := range namespaces { for _, ns := range namespaces {
if _, ok := session.copy.namespaces[ns.Name]; !ok { if _, ok := session.copy.namespaces[NamespaceKey(&ns.Namespace)]; !ok {
return errors.New("Namespaces should be in the database") panic("")
}
if _, ok := layerNamespaces[ns.Name]; !ok {
layer.Namespaces = append(layer.Namespaces, ns)
layerNamespaces[ns.Name] = ns
} }
} }
for _, f := range features { for _, f := range features {
if _, ok := session.copy.features[FeatureKey(&f)]; !ok { if _, ok := session.copy.features[FeatureKey(&f.Feature)]; !ok {
return errors.New("Namespaces should be in the database") panic("")
}
if _, ok := layerFeatures[FeatureKey(&f)]; !ok {
layer.Features = append(layer.Features, f)
layerFeatures[FeatureKey(&f)] = f
} }
} }
layer.ProcessedBy.Detectors = append(layer.ProcessedBy.Detectors, strutil.CompareStringLists(processedBy.Detectors, layer.ProcessedBy.Detectors)...) layer, _ := session.copy.layers[hash]
layer.ProcessedBy.Listers = append(layer.ProcessedBy.Listers, strutil.CompareStringLists(processedBy.Listers, layer.ProcessedBy.Listers)...) dbutil.MergeLayers(&layer, &database.Layer{
Hash: hash,
By: by,
Namespaces: namespaces,
Features: features,
})
session.copy.layers[hash] = layer session.copy.layers[hash] = layer
return nil return nil
@ -274,11 +219,12 @@ func newMockDatastore() *mockDatastore {
return errSessionDone return errSessionDone
} }
features := getNamespacedFeatures(ancestry.Layers) // ensure the namespaces features are in the code base
// ensure features are in the database for _, l := range ancestry.Layers {
for _, f := range features { for _, f := range l.GetFeatures() {
if _, ok := session.copy.namespacedFeatures[NamespacedFeatureKey(&f)]; !ok { if _, ok := session.copy.namespacedFeatures[NamespacedFeatureKey(&f)]; !ok {
return errors.New("namespaced feature not in db") panic("")
}
} }
} }
@ -288,6 +234,14 @@ func newMockDatastore() *mockDatastore {
session.FctPersistNamespacedFeatures = func(namespacedFeatures []database.NamespacedFeature) error { session.FctPersistNamespacedFeatures = func(namespacedFeatures []database.NamespacedFeature) error {
for i, f := range namespacedFeatures { for i, f := range namespacedFeatures {
if _, ok := session.copy.features[FeatureKey(&f.Feature)]; !ok {
panic("")
}
if _, ok := session.copy.namespaces[NamespaceKey(&f.Namespace)]; !ok {
panic("")
}
session.copy.namespacedFeatures[NamespacedFeatureKey(&f)] = namespacedFeatures[i] session.copy.namespacedFeatures[NamespacedFeatureKey(&f)] = namespacedFeatures[i]
} }
return nil return nil
@ -304,10 +258,7 @@ func newMockDatastore() *mockDatastore {
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
Processors = database.Processors{ EnabledDetectors = append(featurefmt.ListListers(), featurens.ListDetectors()...)
Listers: featurefmt.ListListers(),
Detectors: featurens.ListDetectors(),
}
m.Run() m.Run()
} }
@ -315,11 +266,16 @@ func FeatureKey(f *database.Feature) string {
return strings.Join([]string{f.Name, f.VersionFormat, f.Version}, "__") return strings.Join([]string{f.Name, f.VersionFormat, f.Version}, "__")
} }
func NamespaceKey(ns *database.Namespace) string {
return strings.Join([]string{ns.Name, ns.VersionFormat}, "__")
}
func NamespacedFeatureKey(f *database.NamespacedFeature) string { func NamespacedFeatureKey(f *database.NamespacedFeature) string {
return strings.Join([]string{f.Name, f.Namespace.Name}, "__") return strings.Join([]string{f.Name, f.Namespace.Name}, "__")
} }
func TestProcessAncestryWithDistUpgrade(t *testing.T) { func TestProcessAncestryWithDistUpgrade(t *testing.T) {
// TODO(sidac): Change to use table driven tests.
// Create the list of Features that should not been upgraded from one layer to another. // Create the list of Features that should not been upgraded from one layer to another.
nonUpgradedFeatures := []database.Feature{ nonUpgradedFeatures := []database.Feature{
{Name: "libtext-wrapi18n-perl", Version: "0.06-7"}, {Name: "libtext-wrapi18n-perl", Version: "0.06-7"},
@ -358,7 +314,12 @@ func TestProcessAncestryWithDistUpgrade(t *testing.T) {
assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock", layers)) assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock", layers))
// check the ancestry features // check the ancestry features
features := getNamespacedFeatures(datastore.ancestry["Mock"].Layers) features := []database.AncestryFeature{}
for i, l := range datastore.ancestry["Mock"].Layers {
assert.Equal(t, layers[i].Hash, l.Hash)
features = append(features, l.Features...)
}
assert.Len(t, features, 74) assert.Len(t, features, 74)
for _, f := range features { for _, f := range features {
if _, ok := nonUpgradedMap[f.Feature]; ok { if _, ok := nonUpgradedMap[f.Feature]; ok {
@ -367,12 +328,6 @@ func TestProcessAncestryWithDistUpgrade(t *testing.T) {
assert.Equal(t, "debian:8", f.Namespace.Name) assert.Equal(t, "debian:8", f.Namespace.Name)
} }
} }
assert.Equal(t, []database.LayerMetadata{
{Hash: "blank"},
{Hash: "wheezy"},
{Hash: "jessie"},
}, datastore.ancestry["Mock"].Layers)
} }
func TestProcessLayers(t *testing.T) { func TestProcessLayers(t *testing.T) {
@ -404,8 +359,7 @@ func TestProcessLayers(t *testing.T) {
// Ensure each layer has expected namespaces and features detected // Ensure each layer has expected namespaces and features detected
if blank, ok := datastore.layers["blank"]; ok { if blank, ok := datastore.layers["blank"]; ok {
assert.Equal(t, blank.ProcessedBy.Detectors, Processors.Detectors) testutil.AssertDetectorsEqual(t, EnabledDetectors, blank.By)
assert.Equal(t, blank.ProcessedBy.Listers, Processors.Listers)
assert.Len(t, blank.Namespaces, 0) assert.Len(t, blank.Namespaces, 0)
assert.Len(t, blank.Features, 0) assert.Len(t, blank.Features, 0)
} else { } else {
@ -414,9 +368,11 @@ func TestProcessLayers(t *testing.T) {
} }
if wheezy, ok := datastore.layers["wheezy"]; ok { if wheezy, ok := datastore.layers["wheezy"]; ok {
assert.Equal(t, wheezy.ProcessedBy.Detectors, Processors.Detectors) testutil.AssertDetectorsEqual(t, EnabledDetectors, wheezy.By)
assert.Equal(t, wheezy.ProcessedBy.Listers, Processors.Listers) assert.Equal(t, []database.LayerNamespace{
assert.Equal(t, wheezy.Namespaces, []database.Namespace{{Name: "debian:7", VersionFormat: dpkg.ParserName}}) {database.Namespace{"debian:7", dpkg.ParserName}, database.NewNamespaceDetector("os-release", "1.0")},
}, wheezy.Namespaces)
assert.Len(t, wheezy.Features, 52) assert.Len(t, wheezy.Features, 52)
} else { } else {
assert.Fail(t, "wheezy is not stored") assert.Fail(t, "wheezy is not stored")
@ -424,9 +380,10 @@ func TestProcessLayers(t *testing.T) {
} }
if jessie, ok := datastore.layers["jessie"]; ok { if jessie, ok := datastore.layers["jessie"]; ok {
assert.Equal(t, jessie.ProcessedBy.Detectors, Processors.Detectors) testutil.AssertDetectorsEqual(t, EnabledDetectors, jessie.By)
assert.Equal(t, jessie.ProcessedBy.Listers, Processors.Listers) assert.Equal(t, []database.LayerNamespace{
assert.Equal(t, jessie.Namespaces, []database.Namespace{{Name: "debian:8", VersionFormat: dpkg.ParserName}}) {database.Namespace{"debian:8", dpkg.ParserName}, database.NewNamespaceDetector("os-release", "1.0")},
}, jessie.Namespaces)
assert.Len(t, jessie.Features, 74) assert.Len(t, jessie.Features, 74)
} else { } else {
assert.Fail(t, "jessie is not stored") assert.Fail(t, "jessie is not stored")
@ -434,157 +391,124 @@ func TestProcessLayers(t *testing.T) {
} }
} }
// TestUpgradeClair checks if a clair is upgraded and certain ancestry's func getFeatures(a database.Ancestry) []database.AncestryFeature {
// features should not change. We assume that Clair should only upgrade features := []database.AncestryFeature{}
func TestClairUpgrade(t *testing.T) { for _, l := range a.Layers {
_, f, _, _ := runtime.Caller(0) features = append(features, l.Features...)
testDataPath := filepath.Join(filepath.Dir(f)) + "/testdata/DistUpgrade/"
datastore := newMockDatastore()
// suppose there are two ancestries.
layers := []LayerRequest{
{Hash: "blank", Path: testDataPath + "blank.tar.gz"},
{Hash: "wheezy", Path: testDataPath + "wheezy.tar.gz"},
{Hash: "jessie", Path: testDataPath + "jessie.tar.gz"},
} }
layers2 := []LayerRequest{ return features
{Hash: "blank", Path: testDataPath + "blank.tar.gz"},
{Hash: "wheezy", Path: testDataPath + "wheezy.tar.gz"},
}
// Suppose user scan an ancestry with an old instance of Clair.
Processors = database.Processors{
Detectors: []string{"os-release"},
Listers: []string{"rpm"},
}
assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock", layers))
assert.Len(t, getNamespacedFeatures(datastore.ancestry["Mock"].Layers), 0)
assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock2", layers2))
assert.Len(t, getNamespacedFeatures(datastore.ancestry["Mock2"].Layers), 0)
// Clair is upgraded to use a new namespace detector. The expected
// behavior is that all layers will be rescanned with "apt-sources" and
// the ancestry's features are recalculated.
Processors = database.Processors{
Detectors: []string{"os-release", "apt-sources"},
Listers: []string{"rpm"},
}
// Even though Clair processors are upgraded, the ancestry's features should
// not be upgraded without posting the ancestry to Clair again.
assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock", layers))
assert.Len(t, getNamespacedFeatures(datastore.ancestry["Mock"].Layers), 0)
// Clair is upgraded to use a new feature lister. The expected behavior is
// that all layers will be rescanned with "dpkg" and the ancestry's features
// are invalidated and recalculated.
Processors = database.Processors{
Detectors: []string{"os-release", "apt-sources"},
Listers: []string{"rpm", "dpkg"},
}
assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock", layers))
assert.Len(t, getNamespacedFeatures(datastore.ancestry["Mock"].Layers), 74)
assert.Nil(t, ProcessAncestry(datastore, "Docker", "Mock2", layers2))
assert.Len(t, getNamespacedFeatures(datastore.ancestry["Mock2"].Layers), 52)
// check the namespaces are correct
for _, f := range getNamespacedFeatures(datastore.ancestry["Mock"].Layers) {
if !assert.NotEqual(t, database.Namespace{}, f.Namespace) {
assert.Fail(t, "Every feature should have a namespace attached")
}
}
for _, f := range getNamespacedFeatures(datastore.ancestry["Mock2"].Layers) {
if !assert.NotEqual(t, database.Namespace{}, f.Namespace) {
assert.Fail(t, "Every feature should have a namespace attached")
}
}
} }
// TestMultipleNamespaces tests computing ancestry features
func TestComputeAncestryFeatures(t *testing.T) { func TestComputeAncestryFeatures(t *testing.T) {
vf1 := "format 1" vf1 := "format 1"
vf2 := "format 2" vf2 := "format 2"
ns1a := database.Namespace{ nd1 := database.NewNamespaceDetector("apk", "1.0")
Name: "namespace 1:a", fd1 := database.NewFeatureDetector("fd1", "1.0")
VersionFormat: vf1, // this detector only scans one layer with one extra feature, this one
} // should be omitted.
fd2 := database.NewFeatureDetector("fd2", "1.0")
ns1b := database.Namespace{
Name: "namespace 1:b", ns1a := database.LayerNamespace{
VersionFormat: vf1, database.Namespace{
} Name: "namespace 1:a",
VersionFormat: vf1,
ns2a := database.Namespace{ }, nd1,
Name: "namespace 2:a", }
VersionFormat: vf2,
} ns1b := database.LayerNamespace{
database.Namespace{
ns2b := database.Namespace{ Name: "namespace 1:b",
Name: "namespace 2:b", VersionFormat: vf1,
VersionFormat: vf2, }, nd1}
}
ns2a := database.LayerNamespace{
f1 := database.Feature{ database.Namespace{
Name: "feature 1", Name: "namespace 2:a",
Version: "0.1", VersionFormat: vf2,
VersionFormat: vf1, }, nd1}
}
ns2b := database.LayerNamespace{
f2 := database.Feature{ database.Namespace{
Name: "namespace 2:b",
VersionFormat: vf2,
}, nd1}
f1 := database.LayerFeature{
database.Feature{
Name: "feature 1",
Version: "0.1",
VersionFormat: vf1,
}, fd1}
f2 := database.LayerFeature{database.Feature{
Name: "feature 2", Name: "feature 2",
Version: "0.2", Version: "0.2",
VersionFormat: vf1, VersionFormat: vf1,
} }, fd2}
f3 := database.Feature{ f3 := database.LayerFeature{
Name: "feature 1", database.Feature{
Version: "0.3", Name: "feature 1",
VersionFormat: vf2, Version: "0.3",
} VersionFormat: vf2,
}, fd1}
f4 := database.Feature{
Name: "feature 2", f4 := database.LayerFeature{
Version: "0.3", database.Feature{
VersionFormat: vf2, Name: "feature 2",
Version: "0.3",
VersionFormat: vf2,
}, fd1}
f5 := database.LayerFeature{
database.Feature{
Name: "feature 3",
Version: "0.3",
VersionFormat: vf2,
},
fd2,
} }
// Suppose Clair is watching two files for namespaces one containing ns1 // Suppose Clair is watching two files for namespaces one containing ns1
// changes e.g. os-release and the other one containing ns2 changes e.g. // changes e.g. os-release and the other one containing ns2 changes e.g.
// node. // node.
blank := database.Layer{LayerMetadata: database.LayerMetadata{Hash: "blank"}} blank := database.Layer{
Hash: "blank",
By: []database.Detector{nd1, fd1, fd1},
}
initNS1a := database.Layer{ initNS1a := database.Layer{
LayerMetadata: database.LayerMetadata{Hash: "init ns1a"}, Hash: "initNS1a",
Namespaces: []database.Namespace{ns1a}, By: []database.Detector{nd1, fd1, fd1},
Features: []database.Feature{f1, f2}, Namespaces: []database.LayerNamespace{ns1a},
Features: []database.LayerFeature{f1, f2},
} }
upgradeNS2b := database.Layer{ upgradeNS2b := database.Layer{
LayerMetadata: database.LayerMetadata{Hash: "upgrade ns2b"}, Hash: "upgradeNS2b",
Namespaces: []database.Namespace{ns2b}, By: []database.Detector{nd1, fd1, fd1},
Namespaces: []database.LayerNamespace{ns2b},
} }
upgradeNS1b := database.Layer{ upgradeNS1b := database.Layer{
LayerMetadata: database.LayerMetadata{Hash: "upgrade ns1b"}, Hash: "upgradeNS1b",
Namespaces: []database.Namespace{ns1b}, By: []database.Detector{nd1, fd1, fd1, fd2},
Features: []database.Feature{f1, f2}, Namespaces: []database.LayerNamespace{ns1b},
Features: []database.LayerFeature{f1, f2, f5},
} }
initNS2a := database.Layer{ initNS2a := database.Layer{
LayerMetadata: database.LayerMetadata{Hash: "init ns2a"}, Hash: "initNS2a",
Namespaces: []database.Namespace{ns2a}, By: []database.Detector{nd1, fd1, fd1},
Features: []database.Feature{f3, f4}, Namespaces: []database.LayerNamespace{ns2a},
Features: []database.LayerFeature{f3, f4},
} }
removeF2 := database.Layer{ removeF2 := database.Layer{
LayerMetadata: database.LayerMetadata{Hash: "remove f2"}, Hash: "removeF2",
Features: []database.Feature{f1}, By: []database.Detector{nd1, fd1, fd1},
Features: []database.LayerFeature{f1},
} }
// blank -> ns1:a, f1 f2 (init) // blank -> ns1:a, f1 f2 (init)
@ -597,44 +521,65 @@ func TestComputeAncestryFeatures(t *testing.T) {
// -> blank (empty) // -> blank (empty)
layers := []database.Layer{ layers := []database.Layer{
blank, blank, // empty
initNS1a, initNS1a, // namespace: NS1a, features: f1, f2
removeF2, removeF2, // namespace: , features: f1
initNS2a, initNS2a, // namespace: NS2a, features: f3, f4 ( under NS2a )
upgradeNS2b, upgradeNS2b, // namespace: NS2b, ( f3, f4 are now under NS2b )
blank, blank, // empty
upgradeNS1b, upgradeNS1b, // namespace: NS1b, ( f1, f2 are now under NS1b, and they are introduced in this layer. )
removeF2, removeF2, // namespace: , features: f1
blank, blank,
} }
expected := map[database.NamespacedFeature]bool{ expected := []database.AncestryLayer{
{
"blank",
[]database.AncestryFeature{},
},
{ {
Feature: f1, "initNS1a",
Namespace: ns1a, []database.AncestryFeature{{database.NamespacedFeature{f1.Feature, ns1a.Namespace}, f1.By, ns1a.By}},
}: false, },
{ {
Feature: f3, "removeF2",
Namespace: ns2a, []database.AncestryFeature{},
}: false, },
{ {
Feature: f4, "initNS2a",
Namespace: ns2a, []database.AncestryFeature{
}: false, {database.NamespacedFeature{f3.Feature, ns2a.Namespace}, f3.By, ns2a.By},
{database.NamespacedFeature{f4.Feature, ns2a.Namespace}, f4.By, ns2a.By},
},
},
{
"upgradeNS2b",
[]database.AncestryFeature{},
},
{
"blank",
[]database.AncestryFeature{},
},
{
"upgradeNS1b",
[]database.AncestryFeature{},
},
{
"removeF2",
[]database.AncestryFeature{},
},
{
"blank",
[]database.AncestryFeature{},
},
} }
ancestryLayers, err := computeAncestryLayers(layers, database.Processors{}) expectedDetectors := []database.Detector{nd1, fd1}
assert.Nil(t, err) ancestryLayers, detectors, err := computeAncestryLayers(layers)
features := getNamespacedFeatures(ancestryLayers) require.Nil(t, err)
for _, f := range features {
if assert.Contains(t, expected, f) {
if assert.False(t, expected[f]) {
expected[f] = true
}
}
}
for f, visited := range expected { testutil.AssertDetectorsEqual(t, expectedDetectors, detectors)
assert.True(t, visited, "expected feature is missing : "+f.Namespace.Name+":"+f.Name) for i := range expected {
testutil.AssertAncestryLayerEqual(t, &expected[i], &ancestryLayers[i])
} }
} }

Loading…
Cancel
Save