7f1ff8f979
Inserting packages in a single transaction does not actually buy us anything as we often delete quads during an insertion and thus, Cayley could not use COPY and do a single round-trip. Inserting multiple packages in a single transaction actually creates deadlocks when a transaction tries to insert (A,B) and another one tries to insert (B,A).
448 lines
16 KiB
Go
448 lines
16 KiB
Go
// 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 database
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"github.com/coreos/clair/utils"
|
|
cerrors "github.com/coreos/clair/utils/errors"
|
|
"github.com/coreos/clair/utils/types"
|
|
"github.com/google/cayley"
|
|
"github.com/google/cayley/graph"
|
|
"github.com/google/cayley/graph/path"
|
|
)
|
|
|
|
const (
|
|
FieldPackageIsValue = "package"
|
|
FieldPackageOS = "os"
|
|
FieldPackageName = "name"
|
|
FieldPackageVersion = "version"
|
|
FieldPackageNextVersion = "nextVersion"
|
|
FieldPackagePreviousVersion = "previousVersion"
|
|
)
|
|
|
|
var FieldPackageAll = []string{FieldPackageOS, FieldPackageName, FieldPackageVersion, FieldPackageNextVersion, FieldPackagePreviousVersion}
|
|
|
|
// Package represents a package
|
|
type Package struct {
|
|
Node string `json:"-"`
|
|
OS string
|
|
Name string
|
|
Version types.Version
|
|
NextVersionNode string `json:"-"`
|
|
PreviousVersionNode string `json:"-"`
|
|
}
|
|
|
|
// GetNode returns an unique identifier for the graph node
|
|
// Requires the key fields: OS, Name, Version
|
|
func (p *Package) GetNode() string {
|
|
return FieldPackageIsValue + ":" + utils.Hash(p.Key())
|
|
}
|
|
|
|
// Key returns an unique string defining p
|
|
// Requires the key fields: OS, Name, Version
|
|
func (p *Package) Key() string {
|
|
return p.OS + ":" + p.Name + ":" + p.Version.String()
|
|
}
|
|
|
|
// Branch returns an unique string defined the Branch of p (os, name)
|
|
// Requires the key fields: OS, Name
|
|
func (p *Package) Branch() string {
|
|
return p.OS + ":" + p.Name
|
|
}
|
|
|
|
// AbstractPackage is a package that abstract types.MaxVersion by modifying
|
|
// using a AllVersion boolean field and renaming Version to BeforeVersion
|
|
// which makes more sense for an usage with a Vulnerability
|
|
type AbstractPackage struct {
|
|
OS string
|
|
Name string
|
|
|
|
AllVersions bool
|
|
BeforeVersion types.Version
|
|
}
|
|
|
|
// PackagesToAbstractPackages converts several Packages to AbstractPackages
|
|
func PackagesToAbstractPackages(packages []*Package) (abstractPackages []*AbstractPackage) {
|
|
for _, p := range packages {
|
|
ap := &AbstractPackage{OS: p.OS, Name: p.Name}
|
|
if p.Version != types.MaxVersion {
|
|
ap.BeforeVersion = p.Version
|
|
} else {
|
|
ap.AllVersions = true
|
|
}
|
|
abstractPackages = append(abstractPackages, ap)
|
|
}
|
|
return
|
|
}
|
|
|
|
// AbstractPackagesToPackages converts several AbstractPackages to Packages
|
|
func AbstractPackagesToPackages(abstractPackages []*AbstractPackage) (packages []*Package) {
|
|
for _, ap := range abstractPackages {
|
|
p := &Package{OS: ap.OS, Name: ap.Name}
|
|
if ap.AllVersions {
|
|
p.Version = types.MaxVersion
|
|
} else {
|
|
p.Version = ap.BeforeVersion
|
|
}
|
|
packages = append(packages, p)
|
|
}
|
|
return
|
|
}
|
|
|
|
// InsertPackages inserts several packages in the database in one transaction
|
|
// Packages are stored in linked lists, one per Branch. Each linked list has a start package and an end package defined with types.MinVersion/types.MaxVersion versions
|
|
//
|
|
// OS, Name and Version fields have to be specified.
|
|
// If the insertion is successfull, the Node field is filled and represents the graph node identifier.
|
|
func InsertPackages(packageParameters []*Package) error {
|
|
if len(packageParameters) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Verify parameters
|
|
for _, pkg := range packageParameters {
|
|
if pkg.OS == "" || pkg.Name == "" || pkg.Version.String() == "" {
|
|
log.Warningf("could not insert an incomplete package [OS: %s, Name: %s, Version: %s]", pkg.OS, pkg.Name, pkg.Version)
|
|
return cerrors.NewBadRequestError("could not insert an incomplete package")
|
|
}
|
|
}
|
|
|
|
// Iterate over all the packages we need to insert
|
|
for _, packageParameter := range packageParameters {
|
|
t := cayley.NewTransaction()
|
|
|
|
// Is the package already existing ?
|
|
pkg, err := FindOnePackage(packageParameter.OS, packageParameter.Name, packageParameter.Version, []string{})
|
|
if err != nil && err != cerrors.ErrNotFound {
|
|
return err
|
|
}
|
|
if pkg != nil {
|
|
packageParameter.Node = pkg.Node
|
|
continue
|
|
}
|
|
|
|
// Get all packages of the same branch (both from local cache and database)
|
|
branchPackages, err := FindAllPackagesByBranch(packageParameter.OS, packageParameter.Name, []string{FieldPackageOS, FieldPackageName, FieldPackageVersion, FieldPackageNextVersion})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(branchPackages) == 0 {
|
|
// The branch does not exist yet
|
|
insertingStartPackage := packageParameter.Version == types.MinVersion
|
|
insertingEndPackage := packageParameter.Version == types.MaxVersion
|
|
|
|
// Create and insert a end package
|
|
endPackage := &Package{
|
|
OS: packageParameter.OS,
|
|
Name: packageParameter.Name,
|
|
Version: types.MaxVersion,
|
|
}
|
|
endPackage.Node = endPackage.GetNode()
|
|
|
|
t.AddQuad(cayley.Quad(endPackage.Node, FieldIs, FieldPackageIsValue, ""))
|
|
t.AddQuad(cayley.Quad(endPackage.Node, FieldPackageOS, endPackage.OS, ""))
|
|
t.AddQuad(cayley.Quad(endPackage.Node, FieldPackageName, endPackage.Name, ""))
|
|
t.AddQuad(cayley.Quad(endPackage.Node, FieldPackageVersion, endPackage.Version.String(), ""))
|
|
t.AddQuad(cayley.Quad(endPackage.Node, FieldPackageNextVersion, "", ""))
|
|
|
|
// Create the inserted package if it is different than a start/end package
|
|
var newPackage *Package
|
|
if !insertingStartPackage && !insertingEndPackage {
|
|
newPackage = &Package{
|
|
OS: packageParameter.OS,
|
|
Name: packageParameter.Name,
|
|
Version: packageParameter.Version,
|
|
}
|
|
newPackage.Node = newPackage.GetNode()
|
|
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldIs, FieldPackageIsValue, ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageOS, newPackage.OS, ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageName, newPackage.Name, ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageVersion, newPackage.Version.String(), ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageNextVersion, endPackage.Node, ""))
|
|
|
|
packageParameter.Node = newPackage.Node
|
|
}
|
|
|
|
// Create and insert a start package
|
|
startPackage := &Package{
|
|
OS: packageParameter.OS,
|
|
Name: packageParameter.Name,
|
|
Version: types.MinVersion,
|
|
}
|
|
startPackage.Node = startPackage.GetNode()
|
|
|
|
t.AddQuad(cayley.Quad(startPackage.Node, FieldIs, FieldPackageIsValue, ""))
|
|
t.AddQuad(cayley.Quad(startPackage.Node, FieldPackageOS, startPackage.OS, ""))
|
|
t.AddQuad(cayley.Quad(startPackage.Node, FieldPackageName, startPackage.Name, ""))
|
|
t.AddQuad(cayley.Quad(startPackage.Node, FieldPackageVersion, startPackage.Version.String(), ""))
|
|
if !insertingStartPackage && !insertingEndPackage {
|
|
t.AddQuad(cayley.Quad(startPackage.Node, FieldPackageNextVersion, newPackage.Node, ""))
|
|
} else {
|
|
t.AddQuad(cayley.Quad(startPackage.Node, FieldPackageNextVersion, endPackage.Node, ""))
|
|
}
|
|
|
|
// Set package node
|
|
if insertingEndPackage {
|
|
packageParameter.Node = endPackage.Node
|
|
} else if insertingStartPackage {
|
|
packageParameter.Node = startPackage.Node
|
|
}
|
|
} else {
|
|
// The branch already exists
|
|
|
|
// Create the package
|
|
newPackage := &Package{OS: packageParameter.OS, Name: packageParameter.Name, Version: packageParameter.Version}
|
|
newPackage.Node = "package:" + utils.Hash(newPackage.Key())
|
|
packageParameter.Node = newPackage.Node
|
|
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldIs, FieldPackageIsValue, ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageOS, newPackage.OS, ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageName, newPackage.Name, ""))
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageVersion, newPackage.Version.String(), ""))
|
|
|
|
// Sort branchPackages by version (including the new package)
|
|
branchPackages = append(branchPackages, newPackage)
|
|
sort.Sort(ByVersion(branchPackages))
|
|
|
|
// Find my prec/succ GraphID in the sorted slice now
|
|
newPackageKey := newPackage.Key()
|
|
var pred, succ *Package
|
|
var found bool
|
|
for _, p := range branchPackages {
|
|
equal := p.Key() == newPackageKey
|
|
if !equal && !found {
|
|
pred = p
|
|
} else if found {
|
|
succ = p
|
|
break
|
|
} else if equal {
|
|
found = true
|
|
continue
|
|
}
|
|
}
|
|
if pred == nil || succ == nil {
|
|
log.Warningf("could not find any package predecessor/successor of: [OS: %s, Name: %s, Version: %s].", packageParameter.OS, packageParameter.Name, packageParameter.Version)
|
|
return cerrors.NewBadRequestError("could not find package predecessor/successor")
|
|
}
|
|
|
|
// Link the new packages with the branch
|
|
t.RemoveQuad(cayley.Quad(pred.Node, FieldPackageNextVersion, succ.Node, ""))
|
|
|
|
pred.NextVersionNode = newPackage.Node
|
|
t.AddQuad(cayley.Quad(pred.Node, FieldPackageNextVersion, newPackage.Node, ""))
|
|
|
|
newPackage.NextVersionNode = succ.Node
|
|
t.AddQuad(cayley.Quad(newPackage.Node, FieldPackageNextVersion, succ.Node, ""))
|
|
}
|
|
|
|
// Apply transaction
|
|
if err := store.ApplyTransaction(t); err != nil {
|
|
log.Errorf("failed transaction (InsertPackages): %s", err)
|
|
return ErrTransaction
|
|
}
|
|
}
|
|
|
|
// Return
|
|
return nil
|
|
}
|
|
|
|
// FindOnePackage finds and returns a single package having the given OS, name and version, selecting the specified fields
|
|
func FindOnePackage(OS, name string, version types.Version, selectedFields []string) (*Package, error) {
|
|
packageParameter := Package{OS: OS, Name: name, Version: version}
|
|
p, err := toPackages(cayley.StartPath(store, packageParameter.GetNode()).Has(FieldIs, FieldPackageIsValue), selectedFields)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(p) == 1 {
|
|
return p[0], nil
|
|
}
|
|
if len(p) > 1 {
|
|
log.Errorf("found multiple packages with identical data [OS: %s, Name: %s, Version: %s]", OS, name, version)
|
|
return nil, ErrInconsistent
|
|
}
|
|
return nil, cerrors.ErrNotFound
|
|
}
|
|
|
|
// FindAllPackagesByNodes finds and returns all packages given by their nodes, selecting the specified fields
|
|
func FindAllPackagesByNodes(nodes []string, selectedFields []string) ([]*Package, error) {
|
|
if len(nodes) == 0 {
|
|
log.Warning("could not FindAllPackagesByNodes with an empty nodes array.")
|
|
return []*Package{}, nil
|
|
}
|
|
|
|
return toPackages(cayley.StartPath(store, nodes...).Has(FieldIs, FieldPackageIsValue), selectedFields)
|
|
}
|
|
|
|
// FindAllPackagesByBranch finds and returns all packages that belong to the given Branch, selecting the specified fields
|
|
func FindAllPackagesByBranch(OS, name string, selectedFields []string) ([]*Package, error) {
|
|
return toPackages(cayley.StartPath(store, name).In(FieldPackageName).Has(FieldPackageOS, OS), selectedFields)
|
|
}
|
|
|
|
// toPackages converts a path leading to one or multiple packages to Package structs, selecting the specified fields
|
|
func toPackages(path *path.Path, selectedFields []string) ([]*Package, error) {
|
|
var packages []*Package
|
|
var err error
|
|
|
|
saveFields(path, selectedFields, []string{FieldPackagePreviousVersion})
|
|
it, _ := path.BuildIterator().Optimize()
|
|
defer it.Close()
|
|
for cayley.RawNext(it) {
|
|
tags := make(map[string]graph.Value)
|
|
it.TagResults(tags)
|
|
|
|
pkg := Package{Node: store.NameOf(it.Result())}
|
|
for _, selectedField := range selectedFields {
|
|
switch selectedField {
|
|
case FieldPackageOS:
|
|
pkg.OS = store.NameOf(tags[FieldPackageOS])
|
|
case FieldPackageName:
|
|
pkg.Name = store.NameOf(tags[FieldPackageName])
|
|
case FieldPackageVersion:
|
|
pkg.Version, err = types.NewVersion(store.NameOf(tags[FieldPackageVersion]))
|
|
if err != nil {
|
|
log.Warningf("could not parse version of package %s: %s", pkg.Node, err.Error())
|
|
}
|
|
case FieldPackageNextVersion:
|
|
pkg.NextVersionNode = store.NameOf(tags[FieldPackageNextVersion])
|
|
case FieldPackagePreviousVersion:
|
|
pkg.PreviousVersionNode, err = toValue(cayley.StartPath(store, pkg.Node).In(FieldPackageNextVersion))
|
|
if err != nil {
|
|
log.Warningf("could not get previousVersion on package %s: %s.", pkg.Node, err.Error())
|
|
return []*Package{}, ErrInconsistent
|
|
}
|
|
default:
|
|
panic("unknown selectedField")
|
|
}
|
|
}
|
|
packages = append(packages, &pkg)
|
|
}
|
|
if it.Err() != nil {
|
|
log.Errorf("failed query in toPackages: %s", it.Err())
|
|
return []*Package{}, ErrBackendException
|
|
}
|
|
|
|
return packages, nil
|
|
}
|
|
|
|
// NextVersion find and returns the package of the same branch that has a higher version number, selecting the specified fields
|
|
// It requires that FieldPackageNextVersion field has been selected on p
|
|
func (p *Package) NextVersion(selectedFields []string) (*Package, error) {
|
|
if p.NextVersionNode == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
v, err := FindAllPackagesByNodes([]string{p.NextVersionNode}, selectedFields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(v) != 1 {
|
|
log.Errorf("found multiple packages when getting next version of package %s", p.Node)
|
|
return nil, ErrInconsistent
|
|
}
|
|
return v[0], nil
|
|
}
|
|
|
|
// NextVersions find and returns all the packages of the same branch that have
|
|
// a higher version number, selecting the specified fields
|
|
// It requires that FieldPackageNextVersion field has been selected on p
|
|
// The immediate higher version is listed first, and the special end-of-Branch package is last, p is not listed
|
|
func (p *Package) NextVersions(selectedFields []string) ([]*Package, error) {
|
|
var nextVersions []*Package
|
|
|
|
if !utils.Contains(FieldPackageNextVersion, selectedFields) {
|
|
selectedFields = append(selectedFields, FieldPackageNextVersion)
|
|
}
|
|
|
|
nextVersion, err := p.NextVersion(selectedFields)
|
|
if err != nil {
|
|
return []*Package{}, err
|
|
}
|
|
if nextVersion != nil {
|
|
nextVersions = append(nextVersions, nextVersion)
|
|
|
|
nextNextVersions, err := nextVersion.NextVersions(selectedFields)
|
|
if err != nil {
|
|
return []*Package{}, err
|
|
}
|
|
nextVersions = append(nextVersions, nextNextVersions...)
|
|
}
|
|
|
|
return nextVersions, nil
|
|
}
|
|
|
|
// PreviousVersion find and returns the package of the same branch that has an
|
|
// immediate lower version number, selecting the specified fields
|
|
// It requires that FieldPackagePreviousVersion field has been selected on p
|
|
func (p *Package) PreviousVersion(selectedFields []string) (*Package, error) {
|
|
if p.PreviousVersionNode == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
v, err := FindAllPackagesByNodes([]string{p.PreviousVersionNode}, selectedFields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(v) == 0 {
|
|
return nil, nil
|
|
}
|
|
if len(v) != 1 {
|
|
log.Errorf("found multiple packages when getting previous version of package %s", p.Node)
|
|
return nil, ErrInconsistent
|
|
}
|
|
return v[0], nil
|
|
}
|
|
|
|
// PreviousVersions find and returns all the packages of the same branch that
|
|
// have a lower version number, selecting the specified fields
|
|
// It requires that FieldPackageNextVersion field has been selected on p
|
|
// The immediate lower version is listed first, and the special start-of-Branch
|
|
// package is last, p is not listed
|
|
func (p *Package) PreviousVersions(selectedFields []string) ([]*Package, error) {
|
|
var previousVersions []*Package
|
|
|
|
if !utils.Contains(FieldPackagePreviousVersion, selectedFields) {
|
|
selectedFields = append(selectedFields, FieldPackagePreviousVersion)
|
|
}
|
|
|
|
previousVersion, err := p.PreviousVersion(selectedFields)
|
|
if err != nil {
|
|
return []*Package{}, err
|
|
}
|
|
if previousVersion != nil {
|
|
previousVersions = append(previousVersions, previousVersion)
|
|
|
|
previousPreviousVersions, err := previousVersion.PreviousVersions(selectedFields)
|
|
if err != nil {
|
|
return []*Package{}, err
|
|
}
|
|
previousVersions = append(previousVersions, previousPreviousVersions...)
|
|
}
|
|
|
|
return previousVersions, nil
|
|
}
|
|
|
|
// ByVersion implements sort.Interface for []*Package based on the Version field
|
|
// It uses github.com/quentin-m/dpkgcomp internally and makes use of types.MinVersion/types.MaxVersion
|
|
type ByVersion []*Package
|
|
|
|
func (p ByVersion) Len() int { return len(p) }
|
|
func (p ByVersion) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
func (p ByVersion) Less(i, j int) bool { return p[i].Version.Compare(p[j].Version) < 0 }
|