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 }