diff --git a/build/invoke.go b/build/invoke.go index 0b65fb08ca5e..72a0f095bb71 100644 --- a/build/invoke.go +++ b/build/invoke.go @@ -4,6 +4,9 @@ import ( "context" _ "crypto/sha256" // ensure digests can be computed "io" + "io/fs" + "os" + "path" "sync" "sync/atomic" "syscall" @@ -146,6 +149,57 @@ func (c *Container) Exec(ctx context.Context, cfg *InvokeConfig, stdin io.ReadCl return err } +func (c *Container) CanInvoke(ctx context.Context, cfg *InvokeConfig) error { + var cmd string + if len(cfg.Entrypoint) > 0 { + cmd = cfg.Entrypoint[0] + } else if len(cfg.Cmd) > 0 { + cmd = cfg.Cmd[0] + } + + if cmd == "" { + return errors.New("no command specified") + } + + const symlinkResolutionLimit = 40 + for range symlinkResolutionLimit { + fpath, index, err := c.resultCtx.inferMountIndex(cmd, cfg) + if err != nil { + return err + } + + st, err := c.container.StatFile(ctx, gateway.StatContainerRequest{ + StatRequest: gateway.StatRequest{ + Path: fpath, + }, + MountIndex: index, + }) + if err != nil { + return errors.Wrapf(err, "stat error: %s", cmd) + } + + mode := fs.FileMode(st.Mode) + if mode&os.ModeSymlink != 0 { + // Follow the link. + if path.IsAbs(st.Linkname) { + cmd = st.Linkname + } else { + cmd = path.Join(path.Dir(fpath), st.Linkname) + } + continue + } + + if !mode.IsRegular() { + return errors.Errorf("%s: not a file", cmd) + } + if mode&0o111 == 0 { + return errors.Errorf("%s: not an executable", cmd) + } + return nil + } + return errors.Errorf("%s: reached symlink resolution limit", cmd) +} + func (c *Container) ReadFile(ctx context.Context, req gateway.ReadContainerRequest) ([]byte, error) { return c.container.ReadFile(ctx, req) } diff --git a/build/result.go b/build/result.go index b940fce0d342..ce83eaffc9f7 100644 --- a/build/result.go +++ b/build/result.go @@ -7,7 +7,7 @@ import ( "encoding/json" "io" iofs "io/fs" - "path/filepath" + "path" "slices" "strings" "sync" @@ -19,7 +19,6 @@ import ( ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" - "github.com/tonistiigi/fsutil/types" ) // NewResultHandle stores a gateway client, gateway reference, and the error from @@ -81,38 +80,37 @@ func (r *ResultHandle) NewContainer(ctx context.Context, cfg *InvokeConfig) (gat return r.gwClient.NewContainer(ctx, req) } -func (r *ResultHandle) StatFile(ctx context.Context, fpath string, cfg *InvokeConfig) (*types.Stat, error) { +func (r *ResultHandle) inferMountIndex(fpath string, cfg *InvokeConfig) (string, int, error) { containerCfg, err := r.getContainerConfig(cfg) if err != nil { - return nil, err + return "", 0, err + } + + type mountCandidate struct { + gateway.Mount + Index int } - candidateMounts := make([]gateway.Mount, 0, len(containerCfg.Mounts)) - for _, m := range containerCfg.Mounts { + candidateMounts := make([]mountCandidate, 0, len(containerCfg.Mounts)) + for i, m := range containerCfg.Mounts { if strings.HasPrefix(fpath, m.Dest) { - candidateMounts = append(candidateMounts, m) + candidateMounts = append(candidateMounts, mountCandidate{ + Mount: m, + Index: i, + }) } } if len(candidateMounts) == 0 { - return nil, iofs.ErrNotExist + return "", 0, iofs.ErrNotExist } - slices.SortFunc(candidateMounts, func(a, b gateway.Mount) int { + slices.SortFunc(candidateMounts, func(a, b mountCandidate) int { return cmp.Compare(len(a.Dest), len(b.Dest)) }) m := candidateMounts[len(candidateMounts)-1] - relpath, err := filepath.Rel(m.Dest, fpath) - if err != nil { - return nil, err - } - - if m.Ref == nil { - return nil, iofs.ErrNotExist - } - - req := gateway.StatRequest{Path: filepath.ToSlash(relpath)} - return m.Ref.StatFile(ctx, req) + relpath := strings.TrimPrefix(fpath, m.Dest) + return path.Join("/", relpath), m.Index, nil } func (r *ResultHandle) getContainerConfig(cfg *InvokeConfig) (containerCfg gateway.NewContainerRequest, _ error) { diff --git a/dap/debug_shell.go b/dap/debug_shell.go index 5ebb1c930bc5..2bfe84635da7 100644 --- a/dap/debug_shell.go +++ b/dap/debug_shell.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "io/fs" "net" "os" "path/filepath" @@ -192,14 +191,6 @@ func (s *shell) attach(ctx context.Context, f dap.StackFrame, rCtx *build.Result } }() - // Check if the entrypoint is executable. If it isn't, don't bother - // trying to invoke. - if reason, ok := s.canInvoke(ctx, rCtx, cfg); !ok { - writeLineF(in.Stdout, "Build container is not executable. (reason: %s)", reason) - <-ctx.Done() - return context.Cause(ctx) - } - if err := s.sem.Acquire(ctx, 1); err != nil { return err } @@ -211,6 +202,14 @@ func (s *shell) attach(ctx context.Context, f dap.StackFrame, rCtx *build.Result } defer ctr.Cancel() + // Check if the entrypoint is executable. If it isn't, don't bother + // trying to invoke. + if err := ctr.CanInvoke(ctx, cfg); err != nil { + writeLineF(in.Stdout, "Build container is not executable. (reason: %s)", err) + <-ctx.Done() + return context.Cause(ctx) + } + writeLineF(in.Stdout, "Running %s in build container from line %d.", strings.Join(append(cfg.Entrypoint, cfg.Cmd...), " "), f.Line, @@ -231,33 +230,6 @@ func (s *shell) attach(ctx context.Context, f dap.StackFrame, rCtx *build.Result return nil } -func (s *shell) canInvoke(ctx context.Context, rCtx *build.ResultHandle, cfg *build.InvokeConfig) (reason string, ok bool) { - var cmd string - if len(cfg.Entrypoint) > 0 { - cmd = cfg.Entrypoint[0] - } else if len(cfg.Cmd) > 0 { - cmd = cfg.Cmd[0] - } - - if cmd == "" { - return "no command specified", false - } - - st, err := rCtx.StatFile(ctx, cmd, cfg) - if err != nil { - return fmt.Sprintf("stat error: %s", err), false - } - - mode := fs.FileMode(st.Mode) - if !mode.IsRegular() { - return fmt.Sprintf("%s: not a file", cmd), false - } - if mode&0111 == 0 { - return fmt.Sprintf("%s: not an executable", cmd), false - } - return "", true -} - // SendRunInTerminalRequest will send the request to the client to attach to // the socket path that was created by Init. This is intended to be run // from the adapter and interact directly with the client.