Skip to content

Commit

Permalink
rpm: implement an RPM filescanner to discern RPM filepaths
Browse files Browse the repository at this point in the history
Using the filepaths discovered by the RPM filescanner we can judge
whether or not a language package has been installed via RPM or not.

Signed-off-by: crozzy <[email protected]>
  • Loading branch information
crozzy committed Apr 15, 2024
1 parent b28ae8c commit 217186a
Show file tree
Hide file tree
Showing 17 changed files with 11,748 additions and 153 deletions.
1 change: 1 addition & 0 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type FileKind string

const (
FileKindWhiteout = FileKind("whiteout")
FileKindRPM = FileKind("rpm")
)

// File represents interesting files that are found in the layer.
Expand Down
2 changes: 1 addition & 1 deletion indexer/controller/coalesce.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func MergeSR(source *claircore.IndexReport, merge []*claircore.IndexReport) *cla
}

for k, v := range ir.Files {
source.Files[k] = v
source.Files[k] = append(source.Files[k], v...)

Check warning on line 131 in indexer/controller/coalesce.go

View check run for this annotation

Codecov / codecov/patch

indexer/controller/coalesce.go#L131

Added line #L131 was not covered by tests
}
}
return source
Expand Down
2 changes: 1 addition & 1 deletion indexer/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func New(options *indexer.Options) *Controller {
Environments: map[string][]*claircore.Environment{},
Distributions: map[string]*claircore.Distribution{},
Repositories: map[string]*claircore.Repository{},
Files: map[string]claircore.File{},
Files: map[string][]claircore.File{},
}

s := &Controller{
Expand Down
2 changes: 1 addition & 1 deletion indexreport.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type IndexReport struct {
// an error string in the case the index did not succeed
Err string `json:"err"`
// Files doesn't end up in the json report but needs to be available at post-coalesce
Files map[string]File `json:"-"`
Files map[string][]File `json:"-"`
}

// IndexRecords returns a list of IndexRecords derived from the IndexReport
Expand Down
1 change: 1 addition & 0 deletions libindex/libindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func New(ctx context.Context, opts *Options, cl *http.Client) (*Libindex, error)
opts.Ecosystems = append(opts.Ecosystems, whiteout.NewEcosystem(ctx))
opts.Resolvers = []indexer.Resolver{
&whiteout.Resolver{},
&rpm.Resolver{},
}

if cl == nil {
Expand Down
6 changes: 6 additions & 0 deletions linux/coalescer.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ func (c *Coalescer) Coalesce(ctx context.Context, layerArtifacts []*indexer.Laye
for db, pkgs := range tmp {
dbs[db] = pkgs
}
for _, f := range artifacts.Files {
if c.ir.Files == nil {
c.ir.Files = make(map[string][]claircore.File)

Check warning on line 63 in linux/coalescer.go

View check run for this annotation

Codecov / codecov/patch

linux/coalescer.go#L62-L63

Added lines #L62 - L63 were not covered by tests
}
c.ir.Files[artifacts.Hash.String()] = append(c.ir.Files[artifacts.Hash.String()], f)

Check warning on line 65 in linux/coalescer.go

View check run for this annotation

Codecov / codecov/patch

linux/coalescer.go#L65

Added line #L65 was not covered by tests
}
}

for db, packages := range dbs {
Expand Down
3 changes: 3 additions & 0 deletions rpm/ecosystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func NewEcosystem(_ context.Context) *indexer.Ecosystem {
RepositoryScanners: func(ctx context.Context) ([]indexer.RepositoryScanner, error) {
return []indexer.RepositoryScanner{}, nil
},
FileScanners: func(ctx context.Context) ([]indexer.FileScanner, error) {
return []indexer.FileScanner{&FileScanner{}}, nil
},
Coalescer: func(ctx context.Context) (indexer.Coalescer, error) {

Check warning on line 34 in rpm/ecosystem.go

View check run for this annotation

Codecov / codecov/patch

rpm/ecosystem.go#L31-L34

Added lines #L31 - L34 were not covered by tests
return linux.NewCoalescer(), nil
},
Expand Down
168 changes: 168 additions & 0 deletions rpm/filescanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package rpm

import (
"context"
"fmt"
"io"
"io/fs"
"os"
"path"
"runtime/trace"

"github.com/quay/zlog"

"github.com/quay/claircore"
"github.com/quay/claircore/indexer"
"github.com/quay/claircore/rpm/bdb"
"github.com/quay/claircore/rpm/ndb"
"github.com/quay/claircore/rpm/sqlite"
)

const (
scannerName = "rpm"
scannerVersion = "1"
scannerKind = "file"
)

var (
_ indexer.FileScanner = (*FileScanner)(nil)
_ indexer.VersionedScanner = (*FileScanner)(nil)
)

type FileScanner struct{}

func (*FileScanner) Name() string { return scannerName }

func (*FileScanner) Version() string { return scannerVersion }

func (*FileScanner) Kind() string { return scannerKind }

Check warning on line 38 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L38

Added line #L38 was not covered by tests

func (s *FileScanner) Scan(ctx context.Context, layer *claircore.Layer) ([]claircore.File, error) {
if err := ctx.Err(); err != nil {
return nil, err

Check warning on line 42 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L42

Added line #L42 was not covered by tests
}
defer trace.StartRegion(ctx, "FileScanner.Scan").End()
trace.Log(ctx, "layer", layer.Hash.String())
ctx = zlog.ContextWithValues(ctx,
"component", "rpm/FileScanner.Scan",
"version", s.Version(),
"layer", layer.Hash.String())
zlog.Debug(ctx).Msg("start")
defer zlog.Debug(ctx).Msg("done")

sys, err := layer.FS()
if err != nil {
return nil, fmt.Errorf("rpm: unable to open layer: %w", err)

Check warning on line 55 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L55

Added line #L55 was not covered by tests
}

found := make([]foundDB, 0)
if err := fs.WalkDir(sys, ".", findDBs(ctx, &found, sys)); err != nil {
return nil, fmt.Errorf("rpm: error walking fs: %w", err)

Check warning on line 60 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L60

Added line #L60 was not covered by tests
}
if len(found) == 0 {
return nil, nil

Check warning on line 63 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L63

Added line #L63 was not covered by tests
}

done := map[string]struct{}{}
files := []claircore.File{}

for _, db := range found {
ctx := zlog.ContextWithValues(ctx, "db", db.String())
zlog.Debug(ctx).Msg("examining database")
if _, ok := done[db.Path]; ok {
zlog.Debug(ctx).Msg("already seen, skipping")
continue

Check warning on line 74 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L73-L74

Added lines #L73 - L74 were not covered by tests
}
done[db.Path] = struct{}{}
fs, err := dbFileToNativeDB(ctx, sys, db)
if err != nil {
return nil, fmt.Errorf("rpm: error getting native DBs: %w", err)

Check warning on line 79 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L79

Added line #L79 was not covered by tests
}
files = append(files, fs...)
}

zlog.Debug(ctx).Int("count", len(found)).Msg("found possible databases")

return files, nil
}

func dbFileToNativeDB(ctx context.Context, sys fs.FS, db foundDB) ([]claircore.File, error) {
var nat nativeDB // see native_db.go:/nativeDB
switch db.Kind {
case kindSQLite:
r, err := sys.Open(path.Join(db.Path, `rpmdb.sqlite`))
if err != nil {
return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)

Check warning on line 95 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L95

Added line #L95 was not covered by tests
}
defer func() {
if err := r.Close(); err != nil {
zlog.Warn(ctx).Err(err).Msg("unable to close sqlite db")

Check warning on line 99 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L99

Added line #L99 was not covered by tests
}
}()
f, err := os.CreateTemp(os.TempDir(), `rpmdb.sqlite.*`)
if err != nil {
return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)

Check warning on line 104 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L104

Added line #L104 was not covered by tests
}
defer func() {
if err := os.Remove(f.Name()); err != nil {
zlog.Error(ctx).Err(err).Msg("unable to unlink sqlite db")

Check warning on line 108 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L108

Added line #L108 was not covered by tests
}
if err := f.Close(); err != nil {
zlog.Warn(ctx).Err(err).Msg("unable to close sqlite db")

Check warning on line 111 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L111

Added line #L111 was not covered by tests
}
}()
zlog.Debug(ctx).Str("file", f.Name()).Msg("copying sqlite db out of FS")
if _, err := io.Copy(f, r); err != nil {
return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)

Check warning on line 116 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L116

Added line #L116 was not covered by tests
}
if err := f.Sync(); err != nil {
return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)

Check warning on line 119 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L119

Added line #L119 was not covered by tests
}
sdb, err := sqlite.Open(f.Name())
if err != nil {
return nil, fmt.Errorf("rpm: error reading sqlite db: %w", err)

Check warning on line 123 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L123

Added line #L123 was not covered by tests
}
defer sdb.Close()
nat = sdb
case kindBDB:
f, err := sys.Open(path.Join(db.Path, `Packages`))
if err != nil {
return nil, fmt.Errorf("rpm: error reading bdb db: %w", err)

Check warning on line 130 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L127-L130

Added lines #L127 - L130 were not covered by tests
}
defer f.Close()
r, done, err := mkAt(ctx, db.Kind, f)
if err != nil {
return nil, fmt.Errorf("rpm: error reading bdb db: %w", err)

Check warning on line 135 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L132-L135

Added lines #L132 - L135 were not covered by tests
}
defer done()
var bpdb bdb.PackageDB
if err := bpdb.Parse(r); err != nil {
return nil, fmt.Errorf("rpm: error parsing bdb db: %w", err)

Check warning on line 140 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L137-L140

Added lines #L137 - L140 were not covered by tests
}
nat = &bpdb
case kindNDB:
f, err := sys.Open(path.Join(db.Path, `Packages.db`))
if err != nil {
return nil, fmt.Errorf("rpm: error reading ndb db: %w", err)

Check warning on line 146 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L142-L146

Added lines #L142 - L146 were not covered by tests
}
defer f.Close()
r, done, err := mkAt(ctx, db.Kind, f)
if err != nil {
return nil, fmt.Errorf("rpm: error reading ndb db: %w", err)

Check warning on line 151 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L148-L151

Added lines #L148 - L151 were not covered by tests
}
defer done()
var npdb ndb.PackageDB
if err := npdb.Parse(r); err != nil {
return nil, fmt.Errorf("rpm: error parsing ndb db: %w", err)

Check warning on line 156 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L153-L156

Added lines #L153 - L156 were not covered by tests
}
nat = &npdb
default:
panic("programmer error: bad kind: " + db.Kind.String())

Check warning on line 160 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L158-L160

Added lines #L158 - L160 were not covered by tests
}
if err := nat.Validate(ctx); err != nil {
zlog.Warn(ctx).
Err(err).
Msg("rpm: invalid native DB")

Check warning on line 165 in rpm/filescanner.go

View check run for this annotation

Codecov / codecov/patch

rpm/filescanner.go#L163-L165

Added lines #L163 - L165 were not covered by tests
}
return filesFromDB(ctx, nat)
}
76 changes: 76 additions & 0 deletions rpm/filescanner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package rpm

import (
"context"
"fmt"
"testing"

"github.com/quay/zlog"

"github.com/quay/claircore"
"github.com/quay/claircore/test"
)

var testcases = []struct {
name string
expectedFiles int
ref test.LayerRef
}{
{
name: "python files",
expectedFiles: 7,
ref: test.LayerRef{
Registry: "registry.access.redhat.com",
Name: "ubi9/nodejs-18",
Digest: `sha256:1ae06b64755052cef4c32979aded82a18f664c66fa7b50a6d2924afac2849c6e`,
},
},
}

func TestFileScannerLayer(t *testing.T) {
ctx := zlog.Test(context.Background(), t)
var s FileScanner
a := test.NewCachedArena(t)
t.Cleanup(func() {
if err := a.Close(ctx); err != nil {
t.Error(err)
}
})

for _, tt := range testcases {
t.Run(tt.name, func(t *testing.T) {
ctx := zlog.Test(ctx, t)
a.LoadLayerFromRegistry(ctx, t, tt.ref)
r := a.Realizer(ctx).(*test.CachedRealizer)
t.Cleanup(func() {
if err := r.Close(); err != nil {
t.Error(err)
}
})
ls, err := r.RealizeDescriptions(ctx, []claircore.LayerDescription{
{
Digest: tt.ref.Digest,
URI: "http://example.com",
MediaType: test.MediaType,
Headers: make(map[string][]string),
},
})
if err != nil {
t.Fatal(err)
}

got, err := s.Scan(ctx, &ls[0])
if err != nil {
t.Error(err)
}
for _, f := range got {
fmt.Println(f)
}
t.Logf("found %d files", len(got))
if len(got) != tt.expectedFiles {
t.Fatalf("expected %d files but got %d", tt.expectedFiles, len(got))
}
t.Log(got)
})
}
}
Loading

0 comments on commit 217186a

Please sign in to comment.