diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index cd0b012e1b..16646f84f6 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -1,10 +1,8 @@ package main import ( - "encoding/json" "flag" "fmt" - "io" "net/http" _ "net/http/pprof" "os" @@ -12,7 +10,6 @@ import ( "github.com/eraser-dev/eraser/pkg/cri" "github.com/eraser-dev/eraser/pkg/logger" - "golang.org/x/sys/unix" logf "sigs.k8s.io/controller-runtime/pkg/log" util "github.com/eraser-dev/eraser/pkg/utils" @@ -73,57 +70,33 @@ func main() { } log.Info("images collected", "finalImages:", finalImages) - data, err := json.Marshal(finalImages) - if err != nil { - log.Error(err, "failed to encode finalImages") - os.Exit(1) - } - path := util.CollectScanPath if *scanDisabled { path = util.ScanErasePath } - if err := unix.Mkfifo(path, util.PipeMode); err != nil { - log.Error(err, "failed to create pipe", "pipeFile", path) - os.Exit(1) - } - - //nolint:gosec // G304: Opening pipe file is intended functionality - file, err := os.OpenFile(path, os.O_WRONLY, 0) + // Published before the payload, not after: the peer can finish and signal + // back the moment it has read the list, so an endpoint created afterwards + // can be missed entirely. The scanner already publishes in this order. + completion, err := util.CreateCompletionPipe(util.EraseCompleteCollectPath) if err != nil { - log.Error(err, "failed to open pipe", "pipeFile", path) - os.Exit(1) - } - - if _, err := file.Write(data); err != nil { - log.Error(err, "failed to write to pipe", "pipeFile", path) - os.Exit(1) - } - - if err := file.Close(); err != nil { - log.Error(err, "failed to close pipe", "pipeFile", path) - os.Exit(1) - } - if err := unix.Mkfifo(util.EraseCompleteCollectPath, util.PipeMode); err != nil { log.Error(err, "failed to create pipe", "pipeFile", util.EraseCompleteCollectPath) os.Exit(1) } - file, err = os.OpenFile(util.EraseCompleteCollectPath, os.O_RDONLY, 0) - if err != nil { - log.Error(err, "failed to open pipe", "pipeFile", util.EraseCompleteCollectPath) + if err := util.WriteImagesPipe(path, finalImages); err != nil { + log.Error(err, "failed to send images", "pipeFile", path) os.Exit(1) } - data, err = io.ReadAll(file) + data, err := completion.Await() if err != nil { log.Error(err, "failed to read pipe", "pipeFile", util.EraseCompleteCollectPath) os.Exit(1) } - if err := file.Close(); err != nil { + if err := completion.Close(); err != nil { log.Error(err, "failed to close pipe", "pipeFile", util.EraseCompleteCollectPath) os.Exit(1) } diff --git a/pkg/remover/remover.go b/pkg/remover/remover.go index 4f714ad1f6..5db5bb2dd5 100644 --- a/pkg/remover/remover.go +++ b/pkg/remover/remover.go @@ -2,11 +2,8 @@ package main import ( "context" - "encoding/json" "flag" "fmt" - "io" - "io/fs" "net/http" _ "net/http/pprof" "os" @@ -21,7 +18,6 @@ import ( "github.com/eraser-dev/eraser/pkg/logger" "github.com/eraser-dev/eraser/pkg/metrics" - "github.com/eraser-dev/eraser/api/unversioned" util "github.com/eraser-dev/eraser/pkg/utils" ) @@ -78,38 +74,11 @@ func main() { } if *imageListPtr == "" { - var f *os.File - for { - var err error - - f, err = os.OpenFile(util.ScanErasePath, os.O_RDONLY, 0) - if err == nil { - break - } - if !os.IsNotExist(err) { - log.Error(err, "error opening scanErase pipe") - os.Exit(generalErr) - } - time.Sleep(1 * time.Second) - continue - } - - // json data is list of []unversioned.Image - data, err := io.ReadAll(f) + nonCompliantImages, err := util.ReadImagesPipe(context.Background(), util.ScanErasePath) if err != nil { log.Error(err, "error reading non-compliant images") os.Exit(generalErr) } - if err := f.Close(); err != nil { - log.Error(err, "error closing non-compliant images file") - os.Exit(generalErr) - } - - nonCompliantImages := []unversioned.Image{} - if err = json.Unmarshal(data, &nonCompliantImages); err != nil { - log.Error(err, "error in unmarshal non-compliant images") - os.Exit(generalErr) - } for _, img := range nonCompliantImages { imagelist = append(imagelist, img.ImageID) @@ -158,39 +127,18 @@ func main() { } if *imageListPtr == "" { - file, err := os.OpenFile(util.EraseCompleteCollectPath, os.O_WRONLY, 0) - if err != nil { - log.Error(err, "unable to open pipe", "pipeFile", util.EraseCompleteCollectPath) + if err := util.WriteCompletionPipe(util.EraseCompleteCollectPath); err != nil { + log.Error(err, "unable to signal completion", "pipeFile", util.EraseCompleteCollectPath) os.Exit(generalErr) } - if _, err := file.WriteString(util.EraseCompleteMessage); err != nil { - log.Error(err, "unable to write to pipe", "pipeFile", util.EraseCompleteCollectPath) - os.Exit(generalErr) - } - - if err := file.Close(); err != nil { - log.Error(err, "unable to close pipe", "pipeFile", util.EraseCompleteCollectPath) - os.Exit(generalErr) - } - - file, err = os.OpenFile(util.EraseCompleteScanPath, os.O_WRONLY, fs.ModeNamedPipe) + err := util.WriteCompletionPipe(util.EraseCompleteScanPath) // if the scanner is disabled if os.IsNotExist(err) { return } if err != nil { - log.Error(err, "unable to open pipe", "pipeFile", util.EraseCompleteCollectPath) - os.Exit(generalErr) - } - - if _, err := file.WriteString(util.EraseCompleteMessage); err != nil { - log.Error(err, "unable to write to pipe", "pipeFile", util.EraseCompleteCollectPath) - os.Exit(generalErr) - } - - if err := file.Close(); err != nil { - log.Error(err, "unable to close pipe", "pipeFile", util.EraseCompleteScanPath) + log.Error(err, "unable to signal completion", "pipeFile", util.EraseCompleteScanPath) os.Exit(generalErr) } } diff --git a/pkg/scanners/template/scanner_template.go b/pkg/scanners/template/scanner_template.go index f00da5d05d..9f12dcfa21 100644 --- a/pkg/scanners/template/scanner_template.go +++ b/pkg/scanners/template/scanner_template.go @@ -2,14 +2,12 @@ package template import ( "context" - "io" "os" "os/signal" "syscall" "github.com/eraser-dev/eraser/api/unversioned" "github.com/go-logr/logr" - "golang.org/x/sys/unix" "github.com/eraser-dev/eraser/pkg/metrics" util "github.com/eraser-dev/eraser/pkg/utils" @@ -35,6 +33,10 @@ type config struct { deleteScanFailedImages bool deleteEOLImages bool reportMetrics bool + + // held from ReceiveImages until Finish: the endpoint must stay published so + // the remover can tell a scanner is present + completion *util.CompletionPipe } type ConfigFunc func(*config) @@ -59,7 +61,9 @@ func NewImageProvider(funcs ...ConfigFunc) ImageProvider { func (cfg *config) ReceiveImages() ([]unversioned.Image, error) { var err error - if err := unix.Mkfifo(util.EraseCompleteScanPath, util.PipeMode); err != nil { + // published up front so the remover can tell a scanner is present + cfg.completion, err = util.CreateCompletionPipe(util.EraseCompleteScanPath) + if err != nil { cfg.log.Error(err, "failed to create pipe", "pipeName", util.EraseCompleteScanPath) return nil, err } @@ -107,23 +111,14 @@ func (cfg *config) SendImages(nonCompliantImages, failedImages []unversioned.Ima } func (cfg *config) Finish() error { - file, err := os.OpenFile(util.EraseCompleteScanPath, os.O_RDONLY, 0) - if err != nil { - cfg.log.Error(err, "failed to open pipe", "pipeName", util.EraseCompleteScanPath) - return err - } + defer func() { _ = cfg.completion.Close() }() - data, err := io.ReadAll(file) + data, err := cfg.completion.Await() if err != nil { cfg.log.Error(err, "failed to read pipe", "pipeName", util.EraseCompleteScanPath) return err } - if err := file.Close(); err != nil { - cfg.log.Error(err, "failed to close pipe", "pipeName", util.EraseCompleteScanPath) - return err - } - if string(data) != util.EraseCompleteMessage { cfg.log.Info("garbage in pipe", "pipeName", util.EraseCompleteScanPath, "in_pipe", string(data)) return err diff --git a/pkg/utils/handoff_test.go b/pkg/utils/handoff_test.go new file mode 100644 index 0000000000..0d1e45a227 --- /dev/null +++ b/pkg/utils/handoff_test.go @@ -0,0 +1,113 @@ +package utils + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/eraser-dev/eraser/api/unversioned" +) + +// These run against whichever implementation the platform selects: FIFOs on +// Unix, Unix domain sockets on Windows. Keeping them build-tag free is the point +// -- the two implementations have to stay behaviorally identical. + +// shortTempDir keeps paths well inside the sun_path limit that applies to the +// socket implementation. +func shortTempDir(t *testing.T) string { + t.Helper() + + dir, err := os.MkdirTemp("", "h") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.RemoveAll(dir) }) + + return dir +} + +func TestImagesHandoffRoundTrip(t *testing.T) { + path := filepath.Join(shortTempDir(t), "images") + + want := []unversioned.Image{ + {ImageID: "sha256:aaaa", Names: []string{"repo/one:v1"}}, + {ImageID: "sha256:bbbb", Names: []string{"repo/two:v2"}}, + } + + errCh := make(chan error, 1) + go func() { errCh <- WriteImagesPipe(path, want) }() + + got, err := ReadImagesPipe(context.Background(), path) + if err != nil { + t.Fatalf("ReadImagesPipe: %v", err) + } + if err := <-errCh; err != nil { + t.Fatalf("WriteImagesPipe: %v", err) + } + + if len(got) != len(want) { + t.Fatalf("got %d images, want %d", len(got), len(want)) + } + for i := range want { + if got[i].ImageID != want[i].ImageID { + t.Errorf("image %d = %q, want %q", i, got[i].ImageID, want[i].ImageID) + } + } +} + +func TestCompletionHandoffRoundTrip(t *testing.T) { + path := filepath.Join(shortTempDir(t), "complete") + + pipe, err := CreateCompletionPipe(path) + if err != nil { + t.Fatalf("CreateCompletionPipe: %v", err) + } + defer func() { _ = pipe.Close() }() + + errCh := make(chan error, 1) + go func() { errCh <- WriteCompletionPipe(path) }() + + data, err := pipe.Await() + if err != nil { + t.Fatalf("Await: %v", err) + } + if err := <-errCh; err != nil { + t.Fatalf("WriteCompletionPipe: %v", err) + } + + if string(data) != EraseCompleteMessage { + t.Errorf("payload = %q, want %q", string(data), EraseCompleteMessage) + } +} + +// The remover infers "the scanner is disabled" from this error, so it has to +// keep satisfying os.IsNotExist on both platforms. +func TestWriteCompletionPipeAbsentPeerIsNotExist(t *testing.T) { + path := filepath.Join(shortTempDir(t), "no-such-peer") + + err := WriteCompletionPipe(path) + if err == nil { + t.Fatal("expected an error writing to an endpoint nobody published") + } + if !os.IsNotExist(err) { + t.Errorf("os.IsNotExist(%v) = false, want true", err) + } +} + +func TestCompletionPipeCloseIsIdempotentlySafe(t *testing.T) { + path := filepath.Join(shortTempDir(t), "closed") + + pipe, err := CreateCompletionPipe(path) + if err != nil { + t.Fatalf("CreateCompletionPipe: %v", err) + } + + // the scanner defers Close and the collector also closes explicitly + if err := pipe.Close(); err != nil { + t.Errorf("first Close: %v", err) + } + if err := pipe.Close(); err != nil { + t.Errorf("second Close: %v", err) + } +} diff --git a/pkg/utils/handoff_unix.go b/pkg/utils/handoff_unix.go new file mode 100644 index 0000000000..49ae8dd35e --- /dev/null +++ b/pkg/utils/handoff_unix.go @@ -0,0 +1,159 @@ +//go:build !windows + +package utils + +import ( + "context" + "encoding/json" + "io" + "os" + "time" + + "github.com/eraser-dev/eraser/api/unversioned" +) + +// The collector, scanner and remover hand images off to each other through +// endpoints in a shared volume. On Unix those endpoints are FIFOs; see +// handoff_windows.go for the socket-based equivalent. +// +// Three properties are load-bearing and must survive any reimplementation. +// Connecting blocks until the peer is on the other end, which is what makes +// arbitrary container start order safe. The reader sees EOF when the writer +// finishes, which is what frames a message. And the absence of an endpoint is +// itself meaningful: the remover infers that the scanner is disabled from +// ENOENT on the scanner's completion endpoint. + +// CompletionPipe is an endpoint a peer can observe before anything is read from +// it. The scanner creates one early precisely so the remover can tell a scanner +// is present. +type CompletionPipe struct { + path string +} + +// CreateCompletionPipe publishes the endpoint without waiting for a peer. +func CreateCompletionPipe(path string) (*CompletionPipe, error) { + if err := mkfifo(path, PipeMode); err != nil { + return nil, err + } + return &CompletionPipe{path: path}, nil +} + +// Await blocks until a peer signals completion. The payload is returned +// unvalidated so callers keep their existing handling of unexpected content. +func (p *CompletionPipe) Await() ([]byte, error) { + //nolint:gosec // G304: Opening pipe file is intended functionality + file, err := os.OpenFile(p.path, os.O_RDONLY, 0) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(file) + if closeErr := file.Close(); closeErr != nil && err == nil { + err = closeErr + } + if err != nil { + return nil, err + } + + return data, nil +} + +// Close releases this process's hold on the endpoint. The FIFO itself is left +// in place: the remover decides whether a scanner exists by whether the +// scanner's endpoint is on disk, so unlinking here would make a live scanner +// look absent. The socket implementation cannot keep the endpoint after Close +// because the listener owns it, which is the one lifecycle difference between +// the two. +func (p *CompletionPipe) Close() error { + return nil +} + +// WriteImagesPipe publishes the endpoint and blocks until the reader connects. +func WriteImagesPipe(path string, images []unversioned.Image) error { + data, err := json.Marshal(images) + if err != nil { + return err + } + + if err := mkfifo(path, PipeMode); err != nil { + return err + } + + //nolint:gosec // G304: Opening pipe file is intended functionality + file, err := os.OpenFile(path, os.O_WRONLY, 0) + if err != nil { + return err + } + + _, err = file.Write(data) + if closeErr := file.Close(); closeErr != nil && err == nil { + err = closeErr + } + + return err +} + +// ReadImagesPipe waits for the endpoint to appear, then reads until the writer +// finishes. It returns ctx.Err() if the context is canceled while waiting. +func ReadImagesPipe(ctx context.Context, path string) ([]unversioned.Image, error) { + timer := time.NewTimer(time.Second) + if !timer.Stop() { + <-timer.C + } + defer timer.Stop() + + var f *os.File + for { + var err error + + //nolint:gosec // G304: Opening pipe file is intended functionality + f, err = os.OpenFile(path, os.O_RDONLY, 0) + if err == nil { + break + } + if !os.IsNotExist(err) { + return nil, err + } + + timer.Reset(time.Second) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-timer.C: + continue + } + } + + data, err := io.ReadAll(f) + if closeErr := f.Close(); closeErr != nil && err == nil { + err = closeErr + } + if err != nil { + return nil, err + } + + images := []unversioned.Image{} + if err := json.Unmarshal(data, &images); err != nil { + return nil, err + } + + return images, nil +} + +// WriteCompletionPipe signals a peer that this stage is done. The returned error +// satisfies os.IsNotExist when the peer never published the endpoint, which is +// how an absent scanner is detected. +func WriteCompletionPipe(path string) error { + //nolint:gosec // G304: Opening pipe file is intended functionality + file, err := os.OpenFile(path, os.O_WRONLY, 0) + if err != nil { + return err + } + + _, err = file.WriteString(EraseCompleteMessage) + if closeErr := file.Close(); closeErr != nil && err == nil { + err = closeErr + } + + return err +} diff --git a/pkg/utils/handoff_windows.go b/pkg/utils/handoff_windows.go new file mode 100644 index 0000000000..0323da79dc --- /dev/null +++ b/pkg/utils/handoff_windows.go @@ -0,0 +1,195 @@ +//go:build windows + +package utils + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net" + "os" + "time" + + "github.com/eraser-dev/eraser/api/unversioned" +) + +// Windows has no filesystem FIFO, so the handoff runs over Unix domain sockets, +// which Windows has supported since Server 2019. A socket preserves the three +// properties the FIFO implementation relies on: Accept blocks until the peer +// dials, the reader sees EOF when the peer closes, and the endpoint is a file, +// so its absence still tells the remover that no scanner is present. +// +// Responsibility is inverted relative to Unix. With a FIFO the writer creates +// the endpoint and the reader polls for it; with a socket the reader listens and +// the writer dials with retry. + +// maxSocketPath is the longest usable pathname. The sun_path field is 108 +// bytes and has to hold a terminating NUL, so 107 is the real ceiling. +// Exceeding it fails deep inside the syscall with an opaque error, so it is +// checked up front. +const maxSocketPath = 107 + +// CompletionPipe is an endpoint a peer can observe before anything is read from +// it. The scanner creates one early precisely so the remover can tell a scanner +// is present, which means the listener has to outlive its creation. +type CompletionPipe struct { + path string + l net.Listener +} + +// CreateCompletionPipe publishes the endpoint without waiting for a peer. +func CreateCompletionPipe(path string) (*CompletionPipe, error) { + l, err := listen(path) + if err != nil { + return nil, err + } + return &CompletionPipe{path: path, l: l}, nil +} + +// Await blocks until a peer signals completion. The payload is returned +// unvalidated so callers keep their existing handling of unexpected content. +func (p *CompletionPipe) Await() ([]byte, error) { + conn, err := p.l.Accept() + if err != nil { + return nil, err + } + defer func() { _ = conn.Close() }() + + return io.ReadAll(conn) +} + +// Close releases the endpoint, which also unpublishes it. Callers both defer +// this and close explicitly, so repeat calls report success rather than +// net.ErrClosed. +func (p *CompletionPipe) Close() error { + if p.l == nil { + return nil + } + l := p.l + p.l = nil + return l.Close() +} + +// WriteImagesPipe blocks until the reader is listening, then sends the list. +// The unbounded retry mirrors the Unix implementation, where opening a FIFO for +// writing blocks until a reader arrives. +func WriteImagesPipe(path string, images []unversioned.Image) error { + data, err := json.Marshal(images) + if err != nil { + return err + } + + conn, err := dialForever(path) + if err != nil { + return err + } + + if _, err := conn.Write(data); err != nil { + _ = conn.Close() + return err + } + + // closing is what signals end-of-message to the reader + return conn.Close() +} + +// ReadImagesPipe publishes the endpoint and waits for the writer to connect and +// finish. It returns ctx.Err() if the context is canceled while waiting. +func ReadImagesPipe(ctx context.Context, path string) ([]unversioned.Image, error) { + l, err := listen(path) + if err != nil { + return nil, err + } + defer func() { _ = l.Close() }() + + // Accept has no context form; closing the listener is what unblocks it + done := make(chan struct{}) + defer close(done) + go func() { + select { + case <-ctx.Done(): + _ = l.Close() + case <-done: + } + }() + + conn, err := l.Accept() + if err != nil { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } + return nil, err + } + defer func() { _ = conn.Close() }() + + data, err := io.ReadAll(conn) + if err != nil { + return nil, err + } + + images := []unversioned.Image{} + if err := json.Unmarshal(data, &images); err != nil { + return nil, err + } + + return images, nil +} + +// WriteCompletionPipe signals a peer that this stage is done. The returned error +// satisfies os.IsNotExist when the peer never published the endpoint, which is +// how an absent scanner is detected. +func WriteCompletionPipe(path string) error { + // Dialing a socket that is not there reports connection-refused on Windows + // rather than ENOENT, so the filesystem is the only reliable way to tell + // "never published" from "published but gone". + if _, err := os.Stat(path); err != nil { + return err + } + + conn, err := net.Dial("unix", path) + if err != nil { + return err + } + + if _, err := conn.Write([]byte(EraseCompleteMessage)); err != nil { + _ = conn.Close() + return err + } + + return conn.Close() +} + +func listen(path string) (net.Listener, error) { + if len(path) > maxSocketPath { + return nil, fmt.Errorf("socket path %q is %d bytes, over the %d byte limit", path, len(path), maxSocketPath) + } + + // a socket left behind by a previous run would fail the bind + if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + + return net.Listen("unix", path) +} + +// dialForever waits for the reader to start listening. Errors are not +// classified: Windows reports a missing socket as connection-refused, so there +// is no reliable "not yet" error to match on. Retrying unconditionally mirrors +// the Unix implementation, where opening a FIFO for writing blocks until a +// reader arrives. +func dialForever(path string) (net.Conn, error) { + if len(path) > maxSocketPath { + return nil, fmt.Errorf("socket path %q is %d bytes, over the %d byte limit", path, len(path), maxSocketPath) + } + + for { + conn, err := net.Dial("unix", path) + if err == nil { + return conn, nil + } + time.Sleep(time.Second) + } +} diff --git a/pkg/utils/platform_windows_test.go b/pkg/utils/platform_windows_test.go index 19e5693df5..4b4a911adb 100644 --- a/pkg/utils/platform_windows_test.go +++ b/pkg/utils/platform_windows_test.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "os" + "path/filepath" + "strings" "testing" "github.com/Microsoft/go-winio" @@ -55,6 +57,28 @@ func TestGetAddressAndDialer(t *testing.T) { } } +// The guard exists to replace an opaque syscall failure with a clear message, +// so the boundary it enforces has to be the real one. +func TestSocketPathLimitBoundary(t *testing.T) { + dir := shortTempDir(t) + + pathOfLen := func(n int) string { + return filepath.Join(dir, strings.Repeat("a", n-len(dir)-1)) + } + + atLimit := pathOfLen(maxSocketPath) + l, err := listen(atLimit) + if err != nil { + t.Fatalf("listen(%d-byte path) = %v, want success", len(atLimit), err) + } + _ = l.Close() + + overLimit := pathOfLen(maxSocketPath + 1) + if _, err := listen(overLimit); err == nil { + t.Errorf("listen(%d-byte path) = nil, want the guard to reject it", len(overLimit)) + } +} + func TestMkfifoUnsupported(t *testing.T) { if err := mkfifo("ignored", PipeMode); !errors.Is(err, ErrFifoUnsupported) { t.Errorf("mkfifo on windows = %v, want ErrFifoUnsupported", err) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index be8fb7f158..f3d4521738 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,12 +5,10 @@ import ( "encoding/json" "errors" "fmt" - "io" "net" "net/url" "os" "strings" - "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -329,68 +327,16 @@ func readConfigMap(path string) ([]string, error) { return images, nil } +// ReadCollectScanPipe is the scanner-facing spelling of ReadImagesPipe, kept +// because custom scanners may call it directly. func ReadCollectScanPipe(ctx context.Context) ([]unversioned.Image, error) { - timer := time.NewTimer(time.Second) - if !timer.Stop() { - <-timer.C - } - defer timer.Stop() - - var f *os.File - for { - var err error - - f, err = os.OpenFile(CollectScanPath, os.O_RDONLY, 0) - if err == nil { - break - } - if !os.IsNotExist(err) { - return nil, err - } - - timer.Reset(time.Second) - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-timer.C: - continue - } - } - - // json data is list of []eraserv1.Image - data, err := io.ReadAll(f) - if err != nil { - return nil, err - } - - allImages := []unversioned.Image{} - if err = json.Unmarshal(data, &allImages); err != nil { - return nil, err - } - - return allImages, nil + return ReadImagesPipe(ctx, CollectScanPath) } +// WriteScanErasePipe is the scanner-facing spelling of WriteImagesPipe, kept +// because custom scanners may call it directly. func WriteScanErasePipe(vulnerableImages []unversioned.Image) error { - data, err := json.Marshal(vulnerableImages) - if err != nil { - return err - } - - if err = mkfifo(ScanErasePath, PipeMode); err != nil { - return err - } - - file, err := os.OpenFile(ScanErasePath, os.O_WRONLY, 0) - if err != nil { - return err - } - - if _, err := file.Write(data); err != nil { - return err - } - - return file.Close() + return WriteImagesPipe(ScanErasePath, vulnerableImages) } func ProcessRepoDigests(repoDigests []string) ([]string, []error) {