diff --git a/buildcontext/resolver.go b/buildcontext/resolver.go index 530656b7ac..18642f8bb0 100644 --- a/buildcontext/resolver.go +++ b/buildcontext/resolver.go @@ -3,6 +3,7 @@ package buildcontext import ( "context" "fmt" + "os" "path/filepath" "strings" @@ -137,6 +138,71 @@ func (r *Resolver) ExpandWildcard( return ret, nil } +// resolveLocalRootEarthfile rewrites a Earthfile reference for the root of a project. +func resolveLocalRootEarthfile(ref domain.Reference) domain.Reference { + if ref.IsRemote() || + ref.IsImportReference() || + filepath.Clean(ref.GetLocalPath()) != "." || + strings.HasPrefix(ref.GetName(), DockerfileMetaTarget) { + return ref + } + + cwd, err := os.Getwd() + if err != nil { + return ref + } + + curr := cwd + for { + if !hasEarthfile(curr) { + parent := filepath.Dir(curr) + if parent == curr { + break + } + + curr = parent + + continue + } + + relPath, err := filepath.Rel(cwd, curr) + if err != nil { + return withLocalPath(ref, curr) + } + + relPath = filepath.ToSlash(relPath) + if !strings.HasPrefix(relPath, ".") && !filepath.IsAbs(relPath) { + relPath = "./" + relPath + } + + return withLocalPath(ref, relPath) + } + + return ref +} + +func hasEarthfile(dir string) bool { + fi, err := os.Stat(filepath.Join(dir, Earthfile)) + if err != nil { + return false + } + + return !fi.IsDir() +} + +func withLocalPath(ref domain.Reference, newLocalPath string) domain.Reference { + switch r := ref.(type) { + case domain.Target: + r.LocalPath = newLocalPath + return r + case domain.Command: + r.LocalPath = newLocalPath + return r + default: + return ref + } +} + // Resolve returns resolved context data for a given earth reference. If the reference is a target, // then the context will include a build context and possibly additional local directories. func (r *Resolver) Resolve( @@ -146,6 +212,8 @@ func (r *Resolver) Resolve( return nil, errors.Errorf("cannot resolve non-dereferenced import ref %s", ref.String()) } + ref = resolveLocalRootEarthfile(ref) + var ( d *Data err error diff --git a/buildcontext/resolver_test.go b/buildcontext/resolver_test.go new file mode 100644 index 0000000000..8fc67b4fc9 --- /dev/null +++ b/buildcontext/resolver_test.go @@ -0,0 +1,88 @@ +package buildcontext + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/EarthBuild/earthbuild/domain" +) + +//nolint:paralleltest +func TestResolveLocalRootEarthfile(t *testing.T) { + t.Run("finds Earthfile in grandparent directory (../../)", func(t *testing.T) { + tmpDir := t.TempDir() + + err := os.WriteFile(filepath.Join(tmpDir, "Earthfile"), []byte("VERSION 0.8"), 0o600) + require.NoError(t, err) + + subSubDir := filepath.Join(tmpDir, "sub", "subsub") + + err = os.MkdirAll(subSubDir, 0o700) + require.NoError(t, err) + + t.Chdir(subSubDir) + + ref, err := domain.ParseTarget("+foo") + require.NoError(t, err) + + resolved := resolveLocalRootEarthfile(ref) + + require.Equal(t, "../..", resolved.GetLocalPath()) + }) + + t.Run("finds Earthfile in current directory (.)", func(t *testing.T) { + tmpDir := t.TempDir() + + err := os.WriteFile(filepath.Join(tmpDir, "Earthfile"), []byte("VERSION 0.8"), 0o600) + require.NoError(t, err) + + t.Chdir(tmpDir) + + ref, err := domain.ParseTarget("+foo") + require.NoError(t, err) + + resolved := resolveLocalRootEarthfile(ref) + + require.Equal(t, ".", resolved.GetLocalPath()) + }) + + t.Run("stops at nearest ancestor Earthfile", func(t *testing.T) { + tmpDir := t.TempDir() + + err := os.WriteFile(filepath.Join(tmpDir, "Earthfile"), []byte("VERSION 0.8"), 0o600) + require.NoError(t, err) + + withOwnDir := filepath.Join(tmpDir, "sub", "with_own_earthfile") + + err = os.MkdirAll(withOwnDir, 0o700) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(withOwnDir, "Earthfile"), []byte("VERSION 0.8"), 0o600) + require.NoError(t, err) + + t.Chdir(withOwnDir) + + ref, err := domain.ParseTarget("+foo") + require.NoError(t, err) + + resolved := resolveLocalRootEarthfile(ref) + + require.Equal(t, ".", resolved.GetLocalPath()) + }) + + t.Run("returns original reference when no Earthfile exists in ancestors", func(t *testing.T) { + tmpDir := t.TempDir() + + t.Chdir(tmpDir) + + ref, err := domain.ParseTarget("+foo") + require.NoError(t, err) + + resolved := resolveLocalRootEarthfile(ref) + + require.Equal(t, ".", resolved.GetLocalPath()) + }) +}