Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit d5d5e77

Browse files
committed
Better progress logging
Signed-off-by: Christian Dupuis <[email protected]>
1 parent f5d4d75 commit d5d5e77

File tree

9 files changed

+215
-156
lines changed

9 files changed

+215
-156
lines changed

commands/cmd.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ func NewRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Comman
5555
cli.DisableFlagsInUseLine(cmd)
5656
}
5757

58+
skill.Log.SetOutput(os.Stderr)
59+
5860
config := dockerCli.ConfigFile()
5961

6062
var (
@@ -85,7 +87,7 @@ func NewRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Comman
8587
return err
8688
}
8789
if valid, err := query.CheckAuth(workspace, apiKey); err == nil && valid {
88-
skill.Log.Info("Login successful")
90+
fmt.Println("Login successful")
8991
config.SetPluginConfig("index", "workspace", workspace)
9092
config.SetPluginConfig("index", "api-key", apiKey)
9193
return config.Save()
@@ -130,7 +132,7 @@ func NewRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Comman
130132
_ = os.WriteFile(output, js, 0644)
131133
skill.Log.Infof("SBOM written to %s", output)
132134
} else {
133-
os.Stdout.WriteString(string(js) + "\n")
135+
fmt.Println(string(js))
134136
}
135137
return nil
136138
},
@@ -214,27 +216,27 @@ func NewRootCmd(name string, isPlugin bool, dockerCli command.Cli) *cobra.Comman
214216

215217
if len(*cves) > 0 {
216218
for _, c := range *cves {
217-
skill.Log.Warnf("Detected %s at", cve)
218-
skill.Log.Warnf("")
219+
fmt.Println(fmt.Sprintf("Detected %s at", cve))
220+
fmt.Println("")
219221
purl := c.Purl
220222
for _, p := range sb.Artifacts {
221223
if p.Purl == purl {
222-
skill.Log.Warnf(" %s", p.Purl)
224+
fmt.Println(fmt.Sprintf(" %s", p.Purl))
223225
loc := p.Locations[0]
224226
for i, l := range sb.Source.Image.Config.RootFS.DiffIDs {
225227
if l.String() == loc.DiffId {
226228
h := sb.Source.Image.Config.History[i]
227-
skill.Log.Warnf(" ")
228-
skill.Log.Warnf(" Instruction: %s", h.CreatedBy)
229-
skill.Log.Warnf(" Layer %d: %s", i, loc.Digest)
229+
fmt.Println(" ")
230+
fmt.Println(fmt.Sprintf(" Instruction: %s", h.CreatedBy))
231+
fmt.Println(fmt.Sprintf(" Layer %d: %s", i, loc.Digest))
230232
}
231233
}
232234
}
233235
}
234236
}
235237
os.Exit(1)
236238
} else {
237-
skill.Log.Infof("%s not detected", cve)
239+
fmt.Println(fmt.Sprintf("%s not detected", cve))
238240
os.Exit(0)
239241
}
240242
return nil

registry/save.go

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ import (
2424

2525
"github.com/atomist-skills/go-skill"
2626
"github.com/docker/docker/client"
27+
"github.com/dustin/go-humanize"
2728
"github.com/google/go-containerregistry/pkg/authn"
2829
"github.com/google/go-containerregistry/pkg/name"
2930
v1 "github.com/google/go-containerregistry/pkg/v1"
3031
"github.com/google/go-containerregistry/pkg/v1/daemon"
31-
"github.com/google/go-containerregistry/pkg/v1/empty"
32-
"github.com/google/go-containerregistry/pkg/v1/layout"
3332
"github.com/google/go-containerregistry/pkg/v1/remote"
33+
"github.com/google/go-containerregistry/pkg/v1/tarball"
3434
"github.com/pkg/errors"
35+
"github.com/sirupsen/logrus"
3536
)
3637

3738
type ImageId struct {
@@ -58,11 +59,13 @@ func (i ImageId) String() string {
5859
return i.name
5960
}
6061

62+
type Cleanup = func()
63+
6164
// SaveImage stores the v1.Image at path returned in OCI format
62-
func SaveImage(image string, client client.APIClient) (v1.Image, string, error) {
65+
func SaveImage(image string, client client.APIClient) (v1.Image, string, Cleanup, error) {
6366
ref, err := name.ParseReference(image)
6467
if err != nil {
65-
return nil, "", errors.Wrapf(err, "failed to parse reference: %s", image)
68+
return nil, "", nil, errors.Wrapf(err, "failed to parse reference: %s", image)
6669
}
6770

6871
var path string
@@ -76,22 +79,22 @@ func SaveImage(image string, client client.APIClient) (v1.Image, string, error)
7679
if err != nil {
7780
img, err := daemon.Image(ImageId{name: image}, daemon.WithClient(client))
7881
if err != nil {
79-
return nil, "", errors.Wrapf(err, "failed to pull image: %s", image)
82+
return nil, "", nil, errors.Wrapf(err, "failed to pull image: %s", image)
8083
} else {
8184
im, _, err := client.ImageInspectWithRaw(context.Background(), image)
8285
if err != nil {
83-
return nil, "", errors.Wrapf(err, "failed to get local image: %s", image)
86+
return nil, "", nil, errors.Wrapf(err, "failed to get local image: %s", image)
8487
}
85-
path, err = saveOci(im.ID, img, ref, path)
88+
path, cleanup, err := saveTar(im.ID, img, ref, path)
8689
if err != nil {
87-
return nil, "", errors.Wrapf(err, "failed to save image: %s", image)
90+
return nil, "", nil, errors.Wrapf(err, "failed to save image: %s", image)
8891
}
92+
return img, path, cleanup, nil
8993
}
90-
return img, path, nil
9194
} else {
9295
img, err := desc.Image()
9396
if err != nil {
94-
return nil, "", errors.Wrapf(err, "failed to pull image: %s", image)
97+
return nil, "", nil, errors.Wrapf(err, "failed to pull image: %s", image)
9598
}
9699
var digest string
97100
identifier := ref.Identifier()
@@ -101,38 +104,70 @@ func SaveImage(image string, client client.APIClient) (v1.Image, string, error)
101104
digestHash, _ := img.Digest()
102105
digest = digestHash.String()
103106
}
104-
path, err = saveOci(digest, img, ref, path)
107+
path, cleanup, err := saveTar(digest, img, ref, path)
105108
if err != nil {
106-
return nil, "", errors.Wrapf(err, "failed to save image: %s", image)
109+
return nil, "", nil, errors.Wrapf(err, "failed to save image: %s", image)
107110
}
108-
return img, path, nil
111+
return img, path, cleanup, nil
109112
}
110113
}
111114

112-
// saveOci writes the v1.Image img as an OCI Image Layout at path. If a layout
113-
// already exists at that path, it will add the image to the index.
114-
func saveOci(digest string, img v1.Image, ref name.Reference, path string) (string, error) {
115+
func saveTar(digest string, img v1.Image, ref name.Reference, path string) (string, Cleanup, error) {
115116
finalPath := strings.Replace(filepath.Join(path, digest), ":", string(os.PathSeparator), 1)
117+
tarPath := filepath.Join(finalPath, "archive.tar")
116118
skill.Log.Debugf("Copying image to %s", finalPath)
117119

118-
if _, err := os.Stat(finalPath); !os.IsNotExist(err) {
119-
return finalPath, nil
120+
if _, err := os.Stat(tarPath); !os.IsNotExist(err) {
121+
return finalPath, nil, nil
120122
}
121123
err := os.MkdirAll(finalPath, os.ModePerm)
122124
if err != nil {
123-
return "", err
125+
return "", nil, err
124126
}
125-
p, err := layout.FromPath(finalPath)
126-
if err != nil {
127-
p, err = layout.Write(finalPath, empty.Index)
128-
if err != nil {
129-
return "", err
127+
128+
c := make(chan v1.Update, 200)
129+
errchan := make(chan error)
130+
go func() {
131+
if err := tarball.WriteToFile(tarPath, ref, img, tarball.WithProgress(c)); err != nil {
132+
errchan <- errors.Wrapf(err, "failed to write image to tar")
133+
}
134+
errchan <- nil
135+
}()
136+
137+
cleanup := func() {
138+
e := os.Remove(tarPath)
139+
if e != nil {
140+
skill.Log.Warnf("Failed to delete tmp image archive %s", tarPath)
130141
}
131142
}
132-
if err = p.AppendImage(img); err != nil {
133-
return "", err
143+
144+
var update v1.Update
145+
var pp int64
146+
for {
147+
select {
148+
case update = <-c:
149+
p := 100 * update.Complete / update.Total
150+
if p%10 == 0 && pp != p {
151+
skill.Log.WithFields(logrus.Fields{
152+
"event": "copy",
153+
"total": update.Total,
154+
"complete": update.Complete,
155+
}).Debugf("Copying image %3d%% %s/%s", p, humanize.Bytes(uint64(update.Complete)), humanize.Bytes(uint64(update.Total)))
156+
pp = p
157+
}
158+
case err = <-errchan:
159+
if err != nil {
160+
return "", cleanup, err
161+
} else {
162+
skill.Log.WithFields(logrus.Fields{
163+
"event": "copy",
164+
"total": update.Total,
165+
"complete": update.Complete,
166+
}).Debugf("Copying image completed")
167+
return finalPath, cleanup, nil
168+
}
169+
}
134170
}
135-
return finalPath, nil
136171
}
137172

138173
func withAuth() remote.Option {

sbom/detect/detect.go

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
package detect
1818

1919
import (
20+
"bufio"
21+
"fmt"
22+
"io"
23+
"regexp"
24+
"strconv"
25+
"strings"
26+
2027
"github.com/anchore/syft/syft/source"
2128
"github.com/docker/index-cli-plugin/types"
2229
)
@@ -26,7 +33,7 @@ type PackageDetector = func(packages []types.Package, image source.Source, lm ty
2633
var detectors []PackageDetector
2734

2835
func init() {
29-
detectors = []PackageDetector{nodePackageDetector}
36+
detectors = []PackageDetector{nodePackageDetector()}
3037
}
3138

3239
func AdditionalPackages(packages []types.Package, image source.Source, lm types.LayerMapping) []types.Package {
@@ -36,3 +43,112 @@ func AdditionalPackages(packages []types.Package, image source.Source, lm types.
3643
}
3744
return additionalPackages
3845
}
46+
47+
func stringsNodeDetector(executable string, versionEnvVar string, expr *regexp.Regexp, pkg types.Package) PackageDetector {
48+
return func(packages []types.Package, image source.Source, lm types.LayerMapping) []types.Package {
49+
// Already found via package manager
50+
for _, p := range packages {
51+
if purl, err := types.ToPackageUrl(p.Purl); err == nil && purl.Name == pkg.Name {
52+
return []types.Package{}
53+
}
54+
}
55+
56+
var path []string
57+
var version string
58+
59+
env := image.Image.Metadata.Config.Config.Env
60+
for _, e := range env {
61+
k := strings.Split(e, "=")[0]
62+
v := strings.Split(e, "=")[1]
63+
switch k {
64+
case versionEnvVar:
65+
version = v
66+
case "PATH":
67+
path = strings.Split(v, ":")
68+
}
69+
}
70+
71+
if len(path) > 0 {
72+
res, _ := image.FileResolver(source.SquashedScope)
73+
for _, p := range path {
74+
fp := fmt.Sprintf("%s/%s", p, executable)
75+
if locations, err := res.FilesByPath(fp); err == nil && len(locations) > 0 {
76+
loc := locations[0]
77+
78+
if version == "" {
79+
f, _ := res.FileContentsByLocation(loc)
80+
values := readStrings(f, expr)
81+
if len(values) > 0 {
82+
version = values[0][1]
83+
}
84+
}
85+
86+
if version == "" {
87+
continue
88+
}
89+
90+
pkg.Version = version
91+
pkg.Purl = types.PackageToPackageUrl(pkg).String()
92+
pkg.Locations = []types.Location{{
93+
Path: fp,
94+
DiffId: loc.FileSystemID,
95+
Digest: lm.ByDiffId[loc.FileSystemID],
96+
}}
97+
return []types.Package{pkg}
98+
}
99+
}
100+
}
101+
102+
return []types.Package{}
103+
}
104+
}
105+
106+
var (
107+
min = 6
108+
max = 256
109+
ascii = true
110+
)
111+
112+
func readStrings(reader io.ReadCloser, expr *regexp.Regexp) [][]string {
113+
defer reader.Close()
114+
in := bufio.NewReader(reader)
115+
str := make([]rune, 0, max)
116+
filePos := int64(0)
117+
verify := func() [][]string {
118+
if len(str) >= min {
119+
s := string(str)
120+
if m := expr.FindAllStringSubmatch(s, -1); len(m) > 0 {
121+
return m
122+
}
123+
}
124+
str = str[0:0]
125+
return [][]string{}
126+
}
127+
for {
128+
var (
129+
r rune
130+
wid int
131+
err error
132+
)
133+
for ; ; filePos += int64(wid) {
134+
r, wid, err = in.ReadRune()
135+
if err != nil {
136+
return [][]string{}
137+
}
138+
if !strconv.IsPrint(r) || ascii && r >= 0xFF {
139+
if d := verify(); len(d) > 0 {
140+
return d
141+
}
142+
continue
143+
}
144+
// It's printable. Keep it.
145+
if len(str) >= max {
146+
if d := verify(); len(d) > 0 {
147+
return d
148+
}
149+
}
150+
str = append(str, r)
151+
}
152+
}
153+
return [][]string{}
154+
}

sbom/detect/detect_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestNodeDetector(t *testing.T) {
3838
Location: ociPath,
3939
}
4040
src, _, _ := source.New(i, nil, nil)
41-
packages := nodePackageDetector([]types.Package{}, *src, lm)
41+
packages := nodePackageDetector()([]types.Package{}, *src, lm)
4242
if len(packages) != 1 {
4343
t.Errorf("Expected package missing")
4444
}

0 commit comments

Comments
 (0)