diff --git a/pkg/commands/options/filestuff.go b/pkg/commands/options/filestuff.go index 2820aa5468..cba70b563a 100644 --- a/pkg/commands/options/filestuff.go +++ b/pkg/commands/options/filestuff.go @@ -59,6 +59,11 @@ func EnumerateFiles(fo *FilenameOptions) chan string { // processing (unless we are in recursive mode). If we decide to process // the directory, and we're in watch mode, then we set up a watch on the // directory. + symlink, err := ResolveSymlink(path, fi) + if err != nil { + return err + } + if fi.IsDir() { if path != paths && !fo.Recursive { return filepath.SkipDir @@ -67,6 +72,10 @@ func EnumerateFiles(fo *FilenameOptions) chan string { return nil } + if symlink != "" { + files <- symlink + } + // Don't check extension if the filepath was passed explicitly if path != paths { switch filepath.Ext(path) { @@ -87,3 +96,13 @@ func EnumerateFiles(fo *FilenameOptions) chan string { }() return files } + +func ResolveSymlink(path string, fi os.FileInfo) (symlink string, err error) { + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + symlink, err = os.Readlink(path) // TODO: Handle error here + if !filepath.IsAbs(symlink) { // Output of os.Readlink is OS-dependent... + symlink = filepath.Join(filepath.Dir(path), symlink) + } + } + return +} diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 6c053e2fbe..aed8575537 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -405,7 +405,25 @@ func resolveFile( if f == "-" { b, err = ioutil.ReadAll(os.Stdin) } else { - b, err = ioutil.ReadFile(f) + fi, err := os.Lstat(f) + if err != nil { + return nil, fmt.Errorf("unable to find file %s selector: %wß", f, err) + } + + symlink, err := options.ResolveSymlink(f, fi) + if err != nil { + return nil, fmt.Errorf("unable to resolve symlink of file %s selector: %w", f, err) + } + + if symlink != "" { + b, err = ioutil.ReadFile(symlink) + } else { + b, err = ioutil.ReadFile(f) + } + + if err != nil { + return nil, fmt.Errorf("unable to read file %s: %w", f, err) + } } if err != nil { return nil, err diff --git a/pkg/commands/resolver_test.go b/pkg/commands/resolver_test.go index 21d2d5d610..f2c1bb3e8b 100644 --- a/pkg/commands/resolver_test.go +++ b/pkg/commands/resolver_test.go @@ -25,10 +25,15 @@ import ( "io/ioutil" "log" "net/http/httptest" + "os" "path" + "path/filepath" + "runtime" "strings" "testing" + "gopkg.in/yaml.v3" + "github.com/docker/docker/api/types" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -42,7 +47,6 @@ import ( "github.com/google/ko/pkg/build" "github.com/google/ko/pkg/commands/options" kotesting "github.com/google/ko/pkg/internal/testing" - "gopkg.in/yaml.v3" ) var ( @@ -65,6 +69,12 @@ var ( errImageTag = fmt.Errorf("ImageTag() error") ) +func RootDir() string { + _, b, _, _ := runtime.Caller(0) + d := path.Join(path.Dir(b), "..") + return filepath.Dir(d) +} + type erroringClient struct { daemon.Client } @@ -131,6 +141,62 @@ func TestResolveMultiDocumentYAMLs(t *testing.T) { } } +func TestResolveSymlink(t *testing.T) { + testRef := "github.com/google/ko/test" + refs := []string{testRef} + hashes := []v1.Hash{fooHash} + + f := filepath.Join(RootDir(), "test", "test.yaml") + inputYAML, err := os.ReadFile(f) + if err != nil { + t.Fatalf("Readfile(%s) = %v", f, err) + } + + m := map[string]v1.Hash{ + testRef: fooHash, + } + + base := mustRepository("gcr.io/multi-pass") + outYAML, err := resolveFile( + context.Background(), + filepath.Join(RootDir(), "test", "symlink.yaml"), + kotesting.NewFixedBuild(map[string]build.Result{ + testRef: foo, + }), + kotesting.NewFixedPublish(base, m), + &options.SelectorOptions{}) + + if err != nil { + t.Fatalf("ImageReferences(%v) = %v", string(inputYAML), err) + } + + rd := bytes.NewReader(outYAML) + decoder := yaml.NewDecoder(rd) + var outStructured []string + for { + var output map[string]interface{} + if err := decoder.Decode(&output); err == nil { + outStructured = append(outStructured, output["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["image"].(string)) + } else if errors.Is(err, io.EOF) { + break + } else { + t.Errorf("yaml.Unmarshal(%v) = %v", string(outYAML), err) + } + } + + expectedStructured := []string{ + kotesting.ComputeDigest(base, refs[0], hashes[0]), + } + + if want, got := len(expectedStructured), len(outStructured); want != got { + t.Errorf("resolveFile(%v) = %v, want %v", string(inputYAML), got, want) + } + + if diff := cmp.Diff(expectedStructured, outStructured, cmpopts.EquateEmpty()); diff != "" { + t.Errorf("resolveFile(%v); (-want +got) = %v", string(inputYAML), diff) + } +} + func TestResolveMultiDocumentYAMLsWithSelector(t *testing.T) { passesSelector := `apiVersion: something/v1 kind: Foo diff --git a/test/symlink.yaml b/test/symlink.yaml new file mode 120000 index 0000000000..c0943836b3 --- /dev/null +++ b/test/symlink.yaml @@ -0,0 +1 @@ +test.yaml \ No newline at end of file