@@ -5,11 +5,15 @@ import (
55 "errors"
66 "fmt"
77 "io/fs"
8+ "os"
9+ "os/exec"
10+ "path/filepath"
811)
912
1013// AllFilesystemChecks returns a list of filesystem checks to be performed on the image filesystem.
1114func AllFilesystemChecks () []FilesystemCheck {
1215 return []FilesystemCheck {
16+ FilesystemHasGoExecutables ("bin/opm" ),
1317 FilesystemHasDirectories (
1418 "configs" ,
1519 "tmp/cache" ,
@@ -21,7 +25,7 @@ func AllFilesystemChecks() []FilesystemCheck {
2125func FilesystemHasDirectories (paths ... string ) FilesystemCheck {
2226 return FilesystemCheck {
2327 Name : "FilesystemHasDirectories" + fmt .Sprintf (":(%q) " , paths ),
24- Fn : func (ctx context.Context , imageFS fs.FS ) error {
28+ Fn : func (ctx context.Context , imageFS fs.FS , _ string ) error {
2529 var errs []error
2630 for _ , path := range paths {
2731 stat , err := fs .Stat (imageFS , path )
@@ -38,3 +42,80 @@ func FilesystemHasDirectories(paths ...string) FilesystemCheck {
3842 },
3943 }
4044}
45+
46+ // FilesystemHasGoExecutables checks that the given paths exist inside the extracted image filesystem.
47+ // It supports regular files and symlinks:
48+ // - If it's a regular file, we just check that it exists.
49+ // - If it's a symlink, we check that the link target exists.
50+ //
51+ // This is useful for verifying container images where binaries may be symlinks,
52+ // In our image builds (see:
53+ // https://github.com/openshift/operator-framework-olm/blob/082d59a819afc43b24e9ca23c531bdfc35418722/operator-registry.Dockerfile#L16-L19),
54+ // the actual binaries are in /bin/registry/*, and /bin/* just has symlinks that point to them.
55+ func FilesystemHasGoExecutables (paths ... string ) FilesystemCheck {
56+ return FilesystemCheck {
57+ Name : "FilesystemHasGoExecutables" + fmt .Sprintf (":(%q)" , paths ),
58+
59+ // We use the `os` package instead of fs.FS because fs.FS does not support Lstat or Readlink,
60+ // which are needed to detect and resolve symlinks correctly.
61+ // Therefore, we are looking to real file paths under the unpacked image (in tmpDir/fs).
62+ Fn : func (_ context.Context , _ fs.FS , tmpDir string ) error {
63+ var errs []error
64+ root := filepath .Join (tmpDir , "fs" )
65+
66+ for _ , rel := range paths {
67+ fullPath := filepath .Join (root , rel )
68+
69+ binaryPath , err := resolveImageSymlink (root , fullPath )
70+ if err != nil {
71+ errs = append (errs , fmt .Errorf ("path %q: %w" , rel , err ))
72+ continue
73+ }
74+
75+ out , err := exec .Command ("go" , "version" , "-m" , binaryPath ).CombinedOutput ()
76+ if err != nil {
77+ errs = append (errs , fmt .Errorf ("not a Go binary or unreadable metadata for %q: %v\n %s" , rel , err , out ))
78+ continue
79+ }
80+ }
81+
82+ return errors .Join (errs ... )
83+ },
84+ }
85+ }
86+
87+ // resolveImageSymlink resolve the symlink target.
88+ // Example 1: if the target is absolute like "/bin/registry/opm",
89+ // that means it should exist under the image root at tmpDir/fs/bin/registry/opm.
90+ // Example 2: if the target is relative like "registry/opm" or "../registry/opm",
91+ // resolve it relative to the symlink’s own directory.
92+ func resolveImageSymlink (root , fullPath string ) (string , error ) {
93+ fi , err := os .Lstat (fullPath )
94+ if err != nil {
95+ return "" , fmt .Errorf ("error stating file: %w" , err )
96+ }
97+
98+ if fi .Mode ()& os .ModeSymlink == 0 {
99+ return fullPath , nil
100+ }
101+
102+ target , err := os .Readlink (fullPath )
103+ if err != nil {
104+ return "" , fmt .Errorf ("unreadable symlink: %w" , err )
105+ }
106+
107+ var resolved string
108+ if filepath .IsAbs (target ) {
109+ // remove leading "/" and resolve from root
110+ resolved = filepath .Join (root , target [1 :])
111+ } else {
112+ // If it's not a symlink, we just check that the file exists.
113+ resolved = filepath .Join (filepath .Dir (fullPath ), target )
114+ }
115+
116+ if _ , err := os .Stat (resolved ); err != nil {
117+ return "" , fmt .Errorf ("symlink points to missing target %q: %w" , resolved , err )
118+ }
119+
120+ return resolved , nil
121+ }
0 commit comments