github.com/moby/docker@v26.1.3+incompatible/builder/remotecontext/detect.go (about)

     1  package remotecontext // import "github.com/docker/docker/builder/remotecontext"
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  
    13  	"github.com/containerd/continuity/driver"
    14  	"github.com/containerd/log"
    15  	"github.com/docker/docker/api/types/backend"
    16  	"github.com/docker/docker/builder"
    17  	"github.com/docker/docker/builder/remotecontext/urlutil"
    18  	"github.com/docker/docker/errdefs"
    19  	"github.com/moby/buildkit/frontend/dockerfile/parser"
    20  	"github.com/moby/patternmatcher"
    21  	"github.com/moby/patternmatcher/ignorefile"
    22  	"github.com/moby/sys/symlink"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  // ClientSessionRemote is identifier for client-session context transport
    27  const ClientSessionRemote = "client-session"
    28  
    29  // Detect returns a context and dockerfile from remote location or local
    30  // archive.
    31  func Detect(config backend.BuildConfig) (remote builder.Source, dockerfile *parser.Result, err error) {
    32  	remoteURL := config.Options.RemoteContext
    33  	dockerfilePath := config.Options.Dockerfile
    34  
    35  	switch {
    36  	case remoteURL == "":
    37  		remote, dockerfile, err = newArchiveRemote(config.Source, dockerfilePath)
    38  	case remoteURL == ClientSessionRemote:
    39  		return nil, nil, errdefs.InvalidParameter(errors.New("experimental session with v1 builder is no longer supported, use builder version v2 (BuildKit) instead"))
    40  	case urlutil.IsGitURL(remoteURL):
    41  		remote, dockerfile, err = newGitRemote(remoteURL, dockerfilePath)
    42  	case urlutil.IsURL(remoteURL):
    43  		remote, dockerfile, err = newURLRemote(remoteURL, dockerfilePath, config.ProgressWriter.ProgressReaderFunc)
    44  	default:
    45  		err = fmt.Errorf("remoteURL (%s) could not be recognized as URL", remoteURL)
    46  	}
    47  	return
    48  }
    49  
    50  func newArchiveRemote(rc io.ReadCloser, dockerfilePath string) (builder.Source, *parser.Result, error) {
    51  	defer rc.Close()
    52  	c, err := FromArchive(rc)
    53  	if err != nil {
    54  		return nil, nil, err
    55  	}
    56  
    57  	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
    58  }
    59  
    60  func withDockerfileFromContext(c modifiableContext, dockerfilePath string) (builder.Source, *parser.Result, error) {
    61  	df, err := openAt(c, dockerfilePath)
    62  	if err != nil {
    63  		if errors.Is(err, os.ErrNotExist) {
    64  			if dockerfilePath == builder.DefaultDockerfileName {
    65  				lowercase := strings.ToLower(dockerfilePath)
    66  				if _, err := StatAt(c, lowercase); err == nil {
    67  					return withDockerfileFromContext(c, lowercase)
    68  				}
    69  			}
    70  			return nil, nil, errors.Errorf("Cannot locate specified Dockerfile: %s", dockerfilePath) // backwards compatible error
    71  		}
    72  		c.Close()
    73  		return nil, nil, err
    74  	}
    75  
    76  	res, err := readAndParseDockerfile(dockerfilePath, df)
    77  	if err != nil {
    78  		return nil, nil, err
    79  	}
    80  
    81  	df.Close()
    82  
    83  	if err := removeDockerfile(c, dockerfilePath); err != nil {
    84  		c.Close()
    85  		return nil, nil, err
    86  	}
    87  
    88  	return c, res, nil
    89  }
    90  
    91  func newGitRemote(gitURL string, dockerfilePath string) (builder.Source, *parser.Result, error) {
    92  	c, err := MakeGitContext(gitURL) // TODO: change this to NewLazySource
    93  	if err != nil {
    94  		return nil, nil, err
    95  	}
    96  	return withDockerfileFromContext(c.(modifiableContext), dockerfilePath)
    97  }
    98  
    99  func newURLRemote(url string, dockerfilePath string, progressReader func(in io.ReadCloser) io.ReadCloser) (builder.Source, *parser.Result, error) {
   100  	contentType, content, err := downloadRemote(url)
   101  	if err != nil {
   102  		return nil, nil, err
   103  	}
   104  	defer content.Close()
   105  
   106  	switch contentType {
   107  	case mimeTypeTextPlain:
   108  		res, err := parser.Parse(progressReader(content))
   109  		return nil, res, errdefs.InvalidParameter(err)
   110  	default:
   111  		source, err := FromArchive(progressReader(content))
   112  		if err != nil {
   113  			return nil, nil, err
   114  		}
   115  		return withDockerfileFromContext(source.(modifiableContext), dockerfilePath)
   116  	}
   117  }
   118  
   119  func removeDockerfile(c modifiableContext, filesToRemove ...string) error {
   120  	f, err := openAt(c, ".dockerignore")
   121  	// Note that a missing .dockerignore file isn't treated as an error
   122  	switch {
   123  	case os.IsNotExist(err):
   124  		return nil
   125  	case err != nil:
   126  		return err
   127  	}
   128  	excludes, err := ignorefile.ReadAll(f)
   129  	if err != nil {
   130  		f.Close()
   131  		return errors.Wrap(err, "error reading .dockerignore")
   132  	}
   133  	f.Close()
   134  	filesToRemove = append([]string{".dockerignore"}, filesToRemove...)
   135  	for _, fileToRemove := range filesToRemove {
   136  		if rm, _ := patternmatcher.MatchesOrParentMatches(fileToRemove, excludes); rm {
   137  			if err := c.Remove(fileToRemove); err != nil {
   138  				log.G(context.TODO()).Errorf("failed to remove %s: %v", fileToRemove, err)
   139  			}
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  func readAndParseDockerfile(name string, rc io.Reader) (*parser.Result, error) {
   146  	br := bufio.NewReader(rc)
   147  	if _, err := br.Peek(1); err != nil {
   148  		if err == io.EOF {
   149  			return nil, errdefs.InvalidParameter(errors.Errorf("the Dockerfile (%s) cannot be empty", name))
   150  		}
   151  		return nil, errors.Wrap(err, "unexpected error reading Dockerfile")
   152  	}
   153  
   154  	dockerfile, err := parser.Parse(br)
   155  	if err != nil {
   156  		return nil, errdefs.InvalidParameter(errors.Wrapf(err, "failed to parse %s", name))
   157  	}
   158  
   159  	return dockerfile, nil
   160  }
   161  
   162  func openAt(remote builder.Source, path string) (driver.File, error) {
   163  	fullPath, err := FullPath(remote, path)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	return os.Open(fullPath)
   168  }
   169  
   170  // StatAt is a helper for calling Stat on a path from a source
   171  func StatAt(remote builder.Source, path string) (os.FileInfo, error) {
   172  	fullPath, err := FullPath(remote, path)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	return os.Stat(fullPath)
   177  }
   178  
   179  // FullPath is a helper for getting a full path for a path from a source
   180  func FullPath(remote builder.Source, path string) (string, error) {
   181  	remoteRoot := remote.Root()
   182  	fullPath, err := symlink.FollowSymlinkInScope(filepath.Join(remoteRoot, path), remoteRoot)
   183  	if err != nil {
   184  		if runtime.GOOS == "windows" {
   185  			return "", fmt.Errorf("failed to resolve scoped path %s (%s): %s. Possible cause is a forbidden path outside the build context", path, fullPath, err)
   186  		}
   187  		return "", fmt.Errorf("forbidden path outside the build context: %s (%s)", path, fullPath) // backwards compat with old error
   188  	}
   189  	return fullPath, nil
   190  }