pgsql: Split feature.go to table based files in feature module
This commit is contained in:
parent
43f3ea87d8
commit
c50a2339b7
@ -1,398 +0,0 @@
|
||||
// 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 pgsql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sort"
|
||||
|
||||
"github.com/lib/pq"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/ext/versionfmt"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
const (
|
||||
soiNamespacedFeature = `
|
||||
WITH new_feature_ns AS (
|
||||
INSERT INTO namespaced_feature(feature_id, namespace_id)
|
||||
SELECT CAST ($1 AS INTEGER), CAST ($2 AS INTEGER)
|
||||
WHERE NOT EXISTS ( SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2)
|
||||
RETURNING id
|
||||
)
|
||||
SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2
|
||||
UNION
|
||||
SELECT id FROM new_feature_ns`
|
||||
|
||||
searchPotentialAffectingVulneraibilities = `
|
||||
SELECT nf.id, v.id, vaf.affected_version, vaf.id
|
||||
FROM vulnerability_affected_feature AS vaf, vulnerability AS v,
|
||||
namespaced_feature AS nf, feature AS f
|
||||
WHERE nf.id = ANY($1)
|
||||
AND nf.feature_id = f.id
|
||||
AND nf.namespace_id = v.namespace_id
|
||||
AND vaf.feature_name = f.name
|
||||
AND vaf.feature_type = f.type
|
||||
AND vaf.vulnerability_id = v.id
|
||||
AND v.deleted_at IS NULL`
|
||||
|
||||
searchNamespacedFeaturesVulnerabilities = `
|
||||
SELECT vanf.namespaced_feature_id, v.name, v.description, v.link,
|
||||
v.severity, v.metadata, vaf.fixedin, n.name, n.version_format
|
||||
FROM vulnerability_affected_namespaced_feature AS vanf,
|
||||
Vulnerability AS v,
|
||||
vulnerability_affected_feature AS vaf,
|
||||
namespace AS n
|
||||
WHERE vanf.namespaced_feature_id = ANY($1)
|
||||
AND vaf.id = vanf.added_by
|
||||
AND v.id = vanf.vulnerability_id
|
||||
AND n.id = v.namespace_id
|
||||
AND v.deleted_at IS NULL`
|
||||
)
|
||||
|
||||
func (tx *pgSession) PersistFeatures(features []database.Feature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
types, err := tx.getFeatureTypeMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sorting is needed before inserting into database to prevent deadlock.
|
||||
sort.Slice(features, func(i, j int) bool {
|
||||
return features[i].Name < features[j].Name ||
|
||||
features[i].Version < features[j].Version ||
|
||||
features[i].VersionFormat < features[j].VersionFormat
|
||||
})
|
||||
|
||||
// TODO(Sida): A better interface for bulk insertion is needed.
|
||||
keys := make([]interface{}, 0, len(features)*3)
|
||||
for _, f := range features {
|
||||
keys = append(keys, f.Name, f.Version, f.VersionFormat, types.byName[f.Type])
|
||||
if f.Name == "" || f.Version == "" || f.VersionFormat == "" {
|
||||
return commonerr.NewBadRequestError("Empty feature name, version or version format is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(queryPersistFeature(len(features)), keys...)
|
||||
return handleError("queryPersistFeature", err)
|
||||
}
|
||||
|
||||
type namespacedFeatureWithID struct {
|
||||
database.NamespacedFeature
|
||||
|
||||
ID int64
|
||||
}
|
||||
|
||||
type vulnerabilityCache struct {
|
||||
nsFeatureID int64
|
||||
vulnID int64
|
||||
vulnAffectingID int64
|
||||
}
|
||||
|
||||
func (tx *pgSession) searchAffectingVulnerabilities(features []database.NamespacedFeature) ([]vulnerabilityCache, error) {
|
||||
if len(features) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ids, err := tx.findNamespacedFeatureIDs(features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fMap := map[int64]database.NamespacedFeature{}
|
||||
for i, f := range features {
|
||||
if !ids[i].Valid {
|
||||
return nil, database.ErrMissingEntities
|
||||
}
|
||||
fMap[ids[i].Int64] = f
|
||||
}
|
||||
|
||||
cacheTable := []vulnerabilityCache{}
|
||||
rows, err := tx.Query(searchPotentialAffectingVulneraibilities, pq.Array(ids))
|
||||
if err != nil {
|
||||
return nil, handleError("searchPotentialAffectingVulneraibilities", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var (
|
||||
cache vulnerabilityCache
|
||||
affected string
|
||||
)
|
||||
|
||||
err := rows.Scan(&cache.nsFeatureID, &cache.vulnID, &affected, &cache.vulnAffectingID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok, err := versionfmt.InRange(fMap[cache.nsFeatureID].VersionFormat, fMap[cache.nsFeatureID].Version, affected); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
cacheTable = append(cacheTable, cache)
|
||||
}
|
||||
}
|
||||
|
||||
return cacheTable, nil
|
||||
}
|
||||
|
||||
func (tx *pgSession) CacheAffectedNamespacedFeatures(features []database.NamespacedFeature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := tx.Exec(lockVulnerabilityAffects)
|
||||
if err != nil {
|
||||
return handleError("lockVulnerabilityAffects", err)
|
||||
}
|
||||
|
||||
cache, err := tx.searchAffectingVulnerabilities(features)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keys := make([]interface{}, 0, len(cache)*3)
|
||||
for _, c := range cache {
|
||||
keys = append(keys, c.vulnID, c.nsFeatureID, c.vulnAffectingID)
|
||||
}
|
||||
|
||||
if len(cache) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
affected, err := tx.Exec(queryPersistVulnerabilityAffectedNamespacedFeature(len(cache)), keys...)
|
||||
if err != nil {
|
||||
return handleError("persistVulnerabilityAffectedNamespacedFeature", err)
|
||||
}
|
||||
if count, err := affected.RowsAffected(); err != nil {
|
||||
log.Debugf("Cached %d features in vulnerability_affected_namespaced_feature", count)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *pgSession) PersistNamespacedFeatures(features []database.NamespacedFeature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nsIDs := map[database.Namespace]sql.NullInt64{}
|
||||
fIDs := map[database.Feature]sql.NullInt64{}
|
||||
for _, f := range features {
|
||||
nsIDs[f.Namespace] = sql.NullInt64{}
|
||||
fIDs[f.Feature] = sql.NullInt64{}
|
||||
}
|
||||
|
||||
fToFind := []database.Feature{}
|
||||
for f := range fIDs {
|
||||
fToFind = append(fToFind, f)
|
||||
}
|
||||
|
||||
sort.Slice(fToFind, func(i, j int) bool {
|
||||
return fToFind[i].Name < fToFind[j].Name ||
|
||||
fToFind[i].Version < fToFind[j].Version ||
|
||||
fToFind[i].VersionFormat < fToFind[j].VersionFormat
|
||||
})
|
||||
|
||||
if ids, err := tx.findFeatureIDs(fToFind); err == nil {
|
||||
for i, id := range ids {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
fIDs[fToFind[i]] = id
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
nsToFind := []database.Namespace{}
|
||||
for ns := range nsIDs {
|
||||
nsToFind = append(nsToFind, ns)
|
||||
}
|
||||
|
||||
if ids, err := tx.findNamespaceIDs(nsToFind); err == nil {
|
||||
for i, id := range ids {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
nsIDs[nsToFind[i]] = id
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
keys := make([]interface{}, 0, len(features)*2)
|
||||
for _, f := range features {
|
||||
keys = append(keys, fIDs[f.Feature], nsIDs[f.Namespace])
|
||||
}
|
||||
|
||||
_, err := tx.Exec(queryPersistNamespacedFeature(len(features)), keys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindAffectedNamespacedFeatures retrieves vulnerabilities associated with the
|
||||
// feature.
|
||||
func (tx *pgSession) FindAffectedNamespacedFeatures(features []database.NamespacedFeature) ([]database.NullableAffectedNamespacedFeature, error) {
|
||||
if len(features) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
vulnerableFeatures := make([]database.NullableAffectedNamespacedFeature, len(features))
|
||||
featureIDs, err := tx.findNamespacedFeatureIDs(features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, id := range featureIDs {
|
||||
if id.Valid {
|
||||
vulnerableFeatures[i].Valid = true
|
||||
vulnerableFeatures[i].NamespacedFeature = features[i]
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := tx.Query(searchNamespacedFeaturesVulnerabilities, pq.Array(featureIDs))
|
||||
if err != nil {
|
||||
return nil, handleError("searchNamespacedFeaturesVulnerabilities", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
featureID int64
|
||||
vuln database.VulnerabilityWithFixedIn
|
||||
)
|
||||
|
||||
err := rows.Scan(&featureID,
|
||||
&vuln.Name,
|
||||
&vuln.Description,
|
||||
&vuln.Link,
|
||||
&vuln.Severity,
|
||||
&vuln.Metadata,
|
||||
&vuln.FixedInVersion,
|
||||
&vuln.Namespace.Name,
|
||||
&vuln.Namespace.VersionFormat,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, handleError("searchNamespacedFeaturesVulnerabilities", err)
|
||||
}
|
||||
|
||||
for i, id := range featureIDs {
|
||||
if id.Valid && id.Int64 == featureID {
|
||||
vulnerableFeatures[i].AffectedNamespacedFeature.AffectedBy = append(vulnerableFeatures[i].AffectedNamespacedFeature.AffectedBy, vuln)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return vulnerableFeatures, nil
|
||||
}
|
||||
|
||||
func (tx *pgSession) findNamespacedFeatureIDs(nfs []database.NamespacedFeature) ([]sql.NullInt64, error) {
|
||||
if len(nfs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nfsMap := map[database.NamespacedFeature]int64{}
|
||||
keys := make([]interface{}, 0, len(nfs)*5)
|
||||
for _, nf := range nfs {
|
||||
keys = append(keys, nf.Name, nf.Version, nf.VersionFormat, nf.Type, nf.Namespace.Name)
|
||||
}
|
||||
|
||||
rows, err := tx.Query(querySearchNamespacedFeature(len(nfs)), keys...)
|
||||
if err != nil {
|
||||
return nil, handleError("searchNamespacedFeature", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var (
|
||||
id int64
|
||||
nf database.NamespacedFeature
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&id, &nf.Name, &nf.Version, &nf.VersionFormat, &nf.Type, &nf.Namespace.Name)
|
||||
nf.Namespace.VersionFormat = nf.VersionFormat
|
||||
if err != nil {
|
||||
return nil, handleError("searchNamespacedFeature", err)
|
||||
}
|
||||
nfsMap[nf] = id
|
||||
}
|
||||
|
||||
ids := make([]sql.NullInt64, len(nfs))
|
||||
for i, nf := range nfs {
|
||||
if id, ok := nfsMap[nf]; ok {
|
||||
ids[i] = sql.NullInt64{id, true}
|
||||
} else {
|
||||
ids[i] = sql.NullInt64{}
|
||||
}
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (tx *pgSession) findFeatureIDs(fs []database.Feature) ([]sql.NullInt64, error) {
|
||||
if len(fs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
types, err := tx.getFeatureTypeMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fMap := map[database.Feature]sql.NullInt64{}
|
||||
|
||||
keys := make([]interface{}, 0, len(fs)*4)
|
||||
for _, f := range fs {
|
||||
typeID := types.byName[f.Type]
|
||||
keys = append(keys, f.Name, f.Version, f.VersionFormat, typeID)
|
||||
fMap[f] = sql.NullInt64{}
|
||||
}
|
||||
|
||||
rows, err := tx.Query(querySearchFeatureID(len(fs)), keys...)
|
||||
if err != nil {
|
||||
return nil, handleError("querySearchFeatureID", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
id sql.NullInt64
|
||||
f database.Feature
|
||||
)
|
||||
for rows.Next() {
|
||||
var typeID int
|
||||
err := rows.Scan(&id, &f.Name, &f.Version, &f.VersionFormat, &typeID)
|
||||
if err != nil {
|
||||
return nil, handleError("querySearchFeatureID", err)
|
||||
}
|
||||
|
||||
f.Type = types.byID[typeID]
|
||||
fMap[f] = id
|
||||
}
|
||||
|
||||
ids := make([]sql.NullInt64, len(fs))
|
||||
for i, f := range fs {
|
||||
ids[i] = fMap[f]
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
121
database/pgsql/feature/feature.go
Normal file
121
database/pgsql/feature/feature.go
Normal file
@ -0,0 +1,121 @@
|
||||
// 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 feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
"github.com/coreos/clair/pkg/commonerr"
|
||||
)
|
||||
|
||||
func queryPersistFeature(count int) string {
|
||||
return util.QueryPersist(count,
|
||||
"feature",
|
||||
"feature_name_version_version_format_type_key",
|
||||
"name",
|
||||
"version",
|
||||
"version_format",
|
||||
"type")
|
||||
}
|
||||
|
||||
func querySearchFeatureID(featureCount int) string {
|
||||
return fmt.Sprintf(`
|
||||
SELECT id, name, version, version_format, type
|
||||
FROM Feature WHERE (name, version, version_format, type) IN (%s)`,
|
||||
util.QueryString(4, featureCount),
|
||||
)
|
||||
}
|
||||
|
||||
func PersistFeatures(tx *sql.Tx, features []database.Feature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sorting is needed before inserting into database to prevent deadlock.
|
||||
sort.Slice(features, func(i, j int) bool {
|
||||
return features[i].Name < features[j].Name ||
|
||||
features[i].Version < features[j].Version ||
|
||||
features[i].VersionFormat < features[j].VersionFormat
|
||||
})
|
||||
|
||||
// TODO(Sida): A better interface for bulk insertion is needed.
|
||||
keys := make([]interface{}, 0, len(features)*3)
|
||||
for _, f := range features {
|
||||
keys = append(keys, f.Name, f.Version, f.VersionFormat, types.ByName[f.Type])
|
||||
if f.Name == "" || f.Version == "" || f.VersionFormat == "" {
|
||||
return commonerr.NewBadRequestError("Empty feature name, version or version format is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(queryPersistFeature(len(features)), keys...)
|
||||
return util.HandleError("queryPersistFeature", err)
|
||||
}
|
||||
|
||||
func FindFeatureIDs(tx *sql.Tx, fs []database.Feature) ([]sql.NullInt64, error) {
|
||||
if len(fs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fMap := map[database.Feature]sql.NullInt64{}
|
||||
|
||||
keys := make([]interface{}, 0, len(fs)*4)
|
||||
for _, f := range fs {
|
||||
typeID := types.ByName[f.Type]
|
||||
keys = append(keys, f.Name, f.Version, f.VersionFormat, typeID)
|
||||
fMap[f] = sql.NullInt64{}
|
||||
}
|
||||
|
||||
rows, err := tx.Query(querySearchFeatureID(len(fs)), keys...)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("querySearchFeatureID", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
id sql.NullInt64
|
||||
f database.Feature
|
||||
)
|
||||
for rows.Next() {
|
||||
var typeID int
|
||||
err := rows.Scan(&id, &f.Name, &f.Version, &f.VersionFormat, &typeID)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("querySearchFeatureID", err)
|
||||
}
|
||||
|
||||
f.Type = types.ByID[typeID]
|
||||
fMap[f] = id
|
||||
}
|
||||
|
||||
ids := make([]sql.NullInt64, len(fs))
|
||||
for i, f := range fs {
|
||||
ids[i] = fMap[f]
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
@ -12,36 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
package feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
func TestPersistFeatures(t *testing.T) {
|
||||
tx, cleanup := createTestPgSession(t, "TestPersistFeatures")
|
||||
tx, cleanup := testutil.CreateTestTx(t, "TestPersistFeatures")
|
||||
defer cleanup()
|
||||
|
||||
invalid := database.Feature{}
|
||||
valid := *database.NewBinaryPackage("mount", "2.31.1-0.4ubuntu3.1", "dpkg")
|
||||
|
||||
// invalid
|
||||
require.NotNil(t, tx.PersistFeatures([]database.Feature{invalid}))
|
||||
require.NotNil(t, PersistFeatures(tx, []database.Feature{invalid}))
|
||||
// existing
|
||||
require.Nil(t, tx.PersistFeatures([]database.Feature{valid}))
|
||||
require.Nil(t, tx.PersistFeatures([]database.Feature{valid}))
|
||||
require.Nil(t, PersistFeatures(tx, []database.Feature{valid}))
|
||||
require.Nil(t, PersistFeatures(tx, []database.Feature{valid}))
|
||||
|
||||
features := selectAllFeatures(t, tx)
|
||||
assert.Equal(t, []database.Feature{valid}, features)
|
||||
}
|
||||
|
||||
func TestPersistNamespacedFeatures(t *testing.T) {
|
||||
tx, cleanup := createTestPgSessionWithFixtures(t, "TestPersistNamespacedFeatures")
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestPersistNamespacedFeatures")
|
||||
defer cleanup()
|
||||
|
||||
// existing features
|
||||
@ -58,42 +60,17 @@ func TestPersistNamespacedFeatures(t *testing.T) {
|
||||
nf2 := database.NewNamespacedFeature(n2, f2)
|
||||
// namespaced features with namespaces or features not in the database will
|
||||
// generate error.
|
||||
assert.Nil(t, tx.PersistNamespacedFeatures([]database.NamespacedFeature{}))
|
||||
assert.NotNil(t, tx.PersistNamespacedFeatures([]database.NamespacedFeature{*nf1, *nf2}))
|
||||
assert.Nil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{}))
|
||||
assert.NotNil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{*nf1, *nf2}))
|
||||
// valid case: insert nf3
|
||||
assert.Nil(t, tx.PersistNamespacedFeatures([]database.NamespacedFeature{*nf1}))
|
||||
assert.Nil(t, PersistNamespacedFeatures(tx, []database.NamespacedFeature{*nf1}))
|
||||
|
||||
all := listNamespacedFeatures(t, tx)
|
||||
assert.Contains(t, all, *nf1)
|
||||
}
|
||||
|
||||
func TestFindAffectedNamespacedFeatures(t *testing.T) {
|
||||
datastore, tx := openSessionForTest(t, "FindAffectedNamespacedFeatures", true)
|
||||
defer closeTest(t, datastore, tx)
|
||||
ns := database.NamespacedFeature{
|
||||
Feature: database.Feature{
|
||||
Name: "openssl",
|
||||
Version: "1.0",
|
||||
VersionFormat: "dpkg",
|
||||
Type: database.SourcePackage,
|
||||
},
|
||||
Namespace: database.Namespace{
|
||||
Name: "debian:7",
|
||||
VersionFormat: "dpkg",
|
||||
},
|
||||
}
|
||||
|
||||
ans, err := tx.FindAffectedNamespacedFeatures([]database.NamespacedFeature{ns})
|
||||
if assert.Nil(t, err) &&
|
||||
assert.Len(t, ans, 1) &&
|
||||
assert.True(t, ans[0].Valid) &&
|
||||
assert.Len(t, ans[0].AffectedBy, 1) {
|
||||
assert.Equal(t, "CVE-OPENSSL-1-DEB7", ans[0].AffectedBy[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func listNamespacedFeatures(t *testing.T, tx *pgSession) []database.NamespacedFeature {
|
||||
types, err := tx.getFeatureTypeMap()
|
||||
func listNamespacedFeatures(t *testing.T, tx *sql.Tx) []database.NamespacedFeature {
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -114,15 +91,15 @@ func listNamespacedFeatures(t *testing.T, tx *pgSession) []database.NamespacedFe
|
||||
panic(err)
|
||||
}
|
||||
|
||||
f.Type = types.byID[typeID]
|
||||
f.Type = types.ByID[typeID]
|
||||
nf = append(nf, f)
|
||||
}
|
||||
|
||||
return nf
|
||||
}
|
||||
|
||||
func selectAllFeatures(t *testing.T, tx *pgSession) []database.Feature {
|
||||
types, err := tx.getFeatureTypeMap()
|
||||
func selectAllFeatures(t *testing.T, tx *sql.Tx) []database.Feature {
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -137,7 +114,7 @@ func selectAllFeatures(t *testing.T, tx *pgSession) []database.Feature {
|
||||
f := database.Feature{}
|
||||
var typeID int
|
||||
err := rows.Scan(&f.Name, &f.Version, &f.VersionFormat, &typeID)
|
||||
f.Type = types.byID[typeID]
|
||||
f.Type = types.ByID[typeID]
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
}
|
||||
@ -146,45 +123,24 @@ func selectAllFeatures(t *testing.T, tx *pgSession) []database.Feature {
|
||||
return fs
|
||||
}
|
||||
|
||||
func assertNamespacedFeatureEqual(t *testing.T, expected []database.NamespacedFeature, actual []database.NamespacedFeature) bool {
|
||||
if assert.Len(t, actual, len(expected)) {
|
||||
has := map[database.NamespacedFeature]bool{}
|
||||
for _, nf := range expected {
|
||||
has[nf] = false
|
||||
}
|
||||
|
||||
for _, nf := range actual {
|
||||
has[nf] = true
|
||||
}
|
||||
|
||||
for nf, visited := range has {
|
||||
if !assert.True(t, visited, nf.Namespace.Name+":"+nf.Name+" is expected") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestFindNamespacedFeatureIDs(t *testing.T) {
|
||||
tx, cleanup := createTestPgSessionWithFixtures(t, "TestFindNamespacedFeatureIDs")
|
||||
tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestFindNamespacedFeatureIDs")
|
||||
defer cleanup()
|
||||
|
||||
features := []database.NamespacedFeature{}
|
||||
expectedIDs := []int{}
|
||||
for id, feature := range realNamespacedFeatures {
|
||||
for id, feature := range testutil.RealNamespacedFeatures {
|
||||
features = append(features, feature)
|
||||
expectedIDs = append(expectedIDs, id)
|
||||
}
|
||||
|
||||
features = append(features, realNamespacedFeatures[1]) // test duplicated
|
||||
features = append(features, testutil.RealNamespacedFeatures[1]) // test duplicated
|
||||
expectedIDs = append(expectedIDs, 1)
|
||||
|
||||
namespace := realNamespaces[1]
|
||||
namespace := testutil.RealNamespaces[1]
|
||||
features = append(features, *database.NewNamespacedFeature(&namespace, database.NewBinaryPackage("not-found", "1.0", "dpkg"))) // test not found feature
|
||||
|
||||
ids, err := tx.findNamespacedFeatureIDs(features)
|
||||
ids, err := FindNamespacedFeatureIDs(tx, features)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, ids, len(expectedIDs)+1)
|
||||
for i, id := range ids {
|
@ -12,24 +12,28 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
package feature
|
||||
|
||||
import "github.com/coreos/clair/database"
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
)
|
||||
|
||||
const (
|
||||
selectAllFeatureTypes = `SELECT id, name FROM feature_type`
|
||||
)
|
||||
|
||||
type featureTypes struct {
|
||||
byID map[int]database.FeatureType
|
||||
byName map[database.FeatureType]int
|
||||
type FeatureTypes struct {
|
||||
ByID map[int]database.FeatureType
|
||||
ByName map[database.FeatureType]int
|
||||
}
|
||||
|
||||
func newFeatureTypes() *featureTypes {
|
||||
return &featureTypes{make(map[int]database.FeatureType), make(map[database.FeatureType]int)}
|
||||
func newFeatureTypes() *FeatureTypes {
|
||||
return &FeatureTypes{make(map[int]database.FeatureType), make(map[database.FeatureType]int)}
|
||||
}
|
||||
|
||||
func (tx *pgSession) getFeatureTypeMap() (*featureTypes, error) {
|
||||
func GetFeatureTypeMap(tx *sql.Tx) (*FeatureTypes, error) {
|
||||
rows, err := tx.Query(selectAllFeatureTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -45,8 +49,8 @@ func (tx *pgSession) getFeatureTypeMap() (*featureTypes, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types.byID[id] = name
|
||||
types.byName[name] = id
|
||||
types.ByID[id] = name
|
||||
types.ByName[name] = id
|
||||
}
|
||||
|
||||
return types, nil
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pgsql
|
||||
package feature
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -20,19 +20,20 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/testutil"
|
||||
)
|
||||
|
||||
func TestGetFeatureTypeMap(t *testing.T) {
|
||||
tx, cleanup := createTestPgSession(t, "TestGetFeatureTypeMap")
|
||||
tx, cleanup := testutil.CreateTestTx(t, "TestGetFeatureTypeMap")
|
||||
defer cleanup()
|
||||
|
||||
types, err := tx.getFeatureTypeMap()
|
||||
types, err := GetFeatureTypeMap(tx)
|
||||
if err != nil {
|
||||
require.Nil(t, err, err.Error())
|
||||
}
|
||||
|
||||
require.Equal(t, database.SourcePackage, types.byID[1])
|
||||
require.Equal(t, database.BinaryPackage, types.byID[2])
|
||||
require.Equal(t, 1, types.byName[database.SourcePackage])
|
||||
require.Equal(t, 2, types.byName[database.BinaryPackage])
|
||||
require.Equal(t, database.SourcePackage, types.ByID[1])
|
||||
require.Equal(t, database.BinaryPackage, types.ByID[2])
|
||||
require.Equal(t, 1, types.ByName[database.SourcePackage])
|
||||
require.Equal(t, 2, types.ByName[database.BinaryPackage])
|
||||
}
|
168
database/pgsql/feature/namespaced_feature.go
Normal file
168
database/pgsql/feature/namespaced_feature.go
Normal file
@ -0,0 +1,168 @@
|
||||
// Copyright 2019 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 feature
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/coreos/clair/database"
|
||||
"github.com/coreos/clair/database/pgsql/namespace"
|
||||
"github.com/coreos/clair/database/pgsql/util"
|
||||
)
|
||||
|
||||
var soiNamespacedFeature = `
|
||||
WITH new_feature_ns AS (
|
||||
INSERT INTO namespaced_feature(feature_id, namespace_id)
|
||||
SELECT CAST ($1 AS INTEGER), CAST ($2 AS INTEGER)
|
||||
WHERE NOT EXISTS ( SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2)
|
||||
RETURNING id
|
||||
)
|
||||
SELECT id FROM namespaced_feature WHERE namespaced_feature.feature_id = $1 AND namespaced_feature.namespace_id = $2
|
||||
UNION
|
||||
SELECT id FROM new_feature_ns`
|
||||
|
||||
func queryPersistNamespacedFeature(count int) string {
|
||||
return util.QueryPersist(count, "namespaced_feature",
|
||||
"namespaced_feature_namespace_id_feature_id_key",
|
||||
"feature_id",
|
||||
"namespace_id")
|
||||
}
|
||||
|
||||
func querySearchNamespacedFeature(nsfCount int) string {
|
||||
return fmt.Sprintf(`
|
||||
SELECT nf.id, f.name, f.version, f.version_format, t.name, n.name
|
||||
FROM namespaced_feature AS nf, feature AS f, namespace AS n, feature_type AS t
|
||||
WHERE nf.feature_id = f.id
|
||||
AND nf.namespace_id = n.id
|
||||
AND n.version_format = f.version_format
|
||||
AND f.type = t.id
|
||||
AND (f.name, f.version, f.version_format, t.name, n.name) IN (%s)`,
|
||||
util.QueryString(5, nsfCount),
|
||||
)
|
||||
}
|
||||
|
||||
type namespacedFeatureWithID struct {
|
||||
database.NamespacedFeature
|
||||
|
||||
ID int64
|
||||
}
|
||||
|
||||
func PersistNamespacedFeatures(tx *sql.Tx, features []database.NamespacedFeature) error {
|
||||
if len(features) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nsIDs := map[database.Namespace]sql.NullInt64{}
|
||||
fIDs := map[database.Feature]sql.NullInt64{}
|
||||
for _, f := range features {
|
||||
nsIDs[f.Namespace] = sql.NullInt64{}
|
||||
fIDs[f.Feature] = sql.NullInt64{}
|
||||
}
|
||||
|
||||
fToFind := []database.Feature{}
|
||||
for f := range fIDs {
|
||||
fToFind = append(fToFind, f)
|
||||
}
|
||||
|
||||
sort.Slice(fToFind, func(i, j int) bool {
|
||||
return fToFind[i].Name < fToFind[j].Name ||
|
||||
fToFind[i].Version < fToFind[j].Version ||
|
||||
fToFind[i].VersionFormat < fToFind[j].VersionFormat
|
||||
})
|
||||
|
||||
if ids, err := FindFeatureIDs(tx, fToFind); err == nil {
|
||||
for i, id := range ids {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
fIDs[fToFind[i]] = id
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
nsToFind := []database.Namespace{}
|
||||
for ns := range nsIDs {
|
||||
nsToFind = append(nsToFind, ns)
|
||||
}
|
||||
|
||||
if ids, err := namespace.FindNamespaceIDs(tx, nsToFind); err == nil {
|
||||
for i, id := range ids {
|
||||
if !id.Valid {
|
||||
return database.ErrMissingEntities
|
||||
}
|
||||
nsIDs[nsToFind[i]] = id
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
keys := make([]interface{}, 0, len(features)*2)
|
||||
for _, f := range features {
|
||||
keys = append(keys, fIDs[f.Feature], nsIDs[f.Namespace])
|
||||
}
|
||||
|
||||
_, err := tx.Exec(queryPersistNamespacedFeature(len(features)), keys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindNamespacedFeatureIDs(tx *sql.Tx, nfs []database.NamespacedFeature) ([]sql.NullInt64, error) {
|
||||
if len(nfs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nfsMap := map[database.NamespacedFeature]int64{}
|
||||
keys := make([]interface{}, 0, len(nfs)*5)
|
||||
for _, nf := range nfs {
|
||||
keys = append(keys, nf.Name, nf.Version, nf.VersionFormat, nf.Type, nf.Namespace.Name)
|
||||
}
|
||||
|
||||
rows, err := tx.Query(querySearchNamespacedFeature(len(nfs)), keys...)
|
||||
if err != nil {
|
||||
return nil, util.HandleError("searchNamespacedFeature", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
var (
|
||||
id int64
|
||||
nf database.NamespacedFeature
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&id, &nf.Name, &nf.Version, &nf.VersionFormat, &nf.Type, &nf.Namespace.Name)
|
||||
nf.Namespace.VersionFormat = nf.VersionFormat
|
||||
if err != nil {
|
||||
return nil, util.HandleError("searchNamespacedFeature", err)
|
||||
}
|
||||
nfsMap[nf] = id
|
||||
}
|
||||
|
||||
ids := make([]sql.NullInt64, len(nfs))
|
||||
for i, nf := range nfs {
|
||||
if id, ok := nfsMap[nf]; ok {
|
||||
ids[i] = sql.NullInt64{id, true}
|
||||
} else {
|
||||
ids[i] = sql.NullInt64{}
|
||||
}
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user