builder.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. package dockerfile
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "runtime"
  8. "strings"
  9. "time"
  10. "github.com/docker/docker/api/types"
  11. "github.com/docker/docker/api/types/backend"
  12. "github.com/docker/docker/api/types/container"
  13. "github.com/docker/docker/builder"
  14. "github.com/docker/docker/builder/dockerfile/command"
  15. "github.com/docker/docker/builder/dockerfile/parser"
  16. "github.com/docker/docker/builder/fscache"
  17. "github.com/docker/docker/builder/remotecontext"
  18. "github.com/docker/docker/pkg/idtools"
  19. "github.com/docker/docker/pkg/streamformatter"
  20. "github.com/docker/docker/pkg/stringid"
  21. "github.com/docker/docker/pkg/system"
  22. "github.com/moby/buildkit/session"
  23. "github.com/pkg/errors"
  24. "github.com/sirupsen/logrus"
  25. "golang.org/x/net/context"
  26. "golang.org/x/sync/syncmap"
  27. )
  28. var validCommitCommands = map[string]bool{
  29. "cmd": true,
  30. "entrypoint": true,
  31. "healthcheck": true,
  32. "env": true,
  33. "expose": true,
  34. "label": true,
  35. "onbuild": true,
  36. "user": true,
  37. "volume": true,
  38. "workdir": true,
  39. }
  40. // SessionGetter is object used to get access to a session by uuid
  41. type SessionGetter interface {
  42. Get(ctx context.Context, uuid string) (session.Caller, error)
  43. }
  44. // BuildManager is shared across all Builder objects
  45. type BuildManager struct {
  46. idMappings *idtools.IDMappings
  47. backend builder.Backend
  48. pathCache pathCache // TODO: make this persistent
  49. sg SessionGetter
  50. fsCache *fscache.FSCache
  51. }
  52. // NewBuildManager creates a BuildManager
  53. func NewBuildManager(b builder.Backend, sg SessionGetter, fsCache *fscache.FSCache, idMappings *idtools.IDMappings) (*BuildManager, error) {
  54. bm := &BuildManager{
  55. backend: b,
  56. pathCache: &syncmap.Map{},
  57. sg: sg,
  58. idMappings: idMappings,
  59. fsCache: fsCache,
  60. }
  61. if err := fsCache.RegisterTransport(remotecontext.ClientSessionRemote, NewClientSessionTransport()); err != nil {
  62. return nil, err
  63. }
  64. return bm, nil
  65. }
  66. // Build starts a new build from a BuildConfig
  67. func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
  68. buildsTriggered.Inc()
  69. if config.Options.Dockerfile == "" {
  70. config.Options.Dockerfile = builder.DefaultDockerfileName
  71. }
  72. source, dockerfile, err := remotecontext.Detect(config)
  73. if err != nil {
  74. return nil, err
  75. }
  76. defer func() {
  77. if source != nil {
  78. if err := source.Close(); err != nil {
  79. logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
  80. }
  81. }
  82. }()
  83. // TODO @jhowardmsft LCOW support - this will require rework to allow both linux and Windows simultaneously.
  84. // This is an interim solution to hardcode to linux if LCOW is turned on.
  85. if dockerfile.Platform == "" {
  86. dockerfile.Platform = runtime.GOOS
  87. if dockerfile.Platform == "windows" && system.LCOWSupported() {
  88. dockerfile.Platform = "linux"
  89. }
  90. }
  91. ctx, cancel := context.WithCancel(ctx)
  92. defer cancel()
  93. if src, err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil {
  94. return nil, err
  95. } else if src != nil {
  96. source = src
  97. }
  98. builderOptions := builderOptions{
  99. Options: config.Options,
  100. ProgressWriter: config.ProgressWriter,
  101. Backend: bm.backend,
  102. PathCache: bm.pathCache,
  103. IDMappings: bm.idMappings,
  104. Platform: dockerfile.Platform,
  105. }
  106. return newBuilder(ctx, builderOptions).build(source, dockerfile)
  107. }
  108. func (bm *BuildManager) initializeClientSession(ctx context.Context, cancel func(), options *types.ImageBuildOptions) (builder.Source, error) {
  109. if options.SessionID == "" || bm.sg == nil {
  110. return nil, nil
  111. }
  112. logrus.Debug("client is session enabled")
  113. ctx, cancelCtx := context.WithTimeout(ctx, sessionConnectTimeout)
  114. defer cancelCtx()
  115. c, err := bm.sg.Get(ctx, options.SessionID)
  116. if err != nil {
  117. return nil, err
  118. }
  119. go func() {
  120. <-c.Context().Done()
  121. cancel()
  122. }()
  123. if options.RemoteContext == remotecontext.ClientSessionRemote {
  124. st := time.Now()
  125. csi, err := NewClientSessionSourceIdentifier(ctx, bm.sg, options.SessionID)
  126. if err != nil {
  127. return nil, err
  128. }
  129. src, err := bm.fsCache.SyncFrom(ctx, csi)
  130. if err != nil {
  131. return nil, err
  132. }
  133. logrus.Debugf("sync-time: %v", time.Since(st))
  134. return src, nil
  135. }
  136. return nil, nil
  137. }
  138. // builderOptions are the dependencies required by the builder
  139. type builderOptions struct {
  140. Options *types.ImageBuildOptions
  141. Backend builder.Backend
  142. ProgressWriter backend.ProgressWriter
  143. PathCache pathCache
  144. IDMappings *idtools.IDMappings
  145. Platform string
  146. }
  147. // Builder is a Dockerfile builder
  148. // It implements the builder.Backend interface.
  149. type Builder struct {
  150. options *types.ImageBuildOptions
  151. Stdout io.Writer
  152. Stderr io.Writer
  153. Aux *streamformatter.AuxFormatter
  154. Output io.Writer
  155. docker builder.Backend
  156. clientCtx context.Context
  157. idMappings *idtools.IDMappings
  158. buildStages *buildStages
  159. disableCommit bool
  160. buildArgs *buildArgs
  161. imageSources *imageSources
  162. pathCache pathCache
  163. containerManager *containerManager
  164. imageProber ImageProber
  165. // TODO @jhowardmft LCOW Support. This will be moved to options at a later
  166. // stage, however that cannot be done now as it affects the public API
  167. // if it were.
  168. platform string
  169. }
  170. // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
  171. // TODO @jhowardmsft LCOW support: Eventually platform can be moved into the builder
  172. // options, however, that would be an API change as it shares types.ImageBuildOptions.
  173. func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
  174. config := options.Options
  175. if config == nil {
  176. config = new(types.ImageBuildOptions)
  177. }
  178. // @jhowardmsft LCOW Support. For the time being, this is interim. Eventually
  179. // will be moved to types.ImageBuildOptions, but it can't for now as that would
  180. // be an API change.
  181. if options.Platform == "" {
  182. options.Platform = runtime.GOOS
  183. }
  184. if options.Platform == "windows" && system.LCOWSupported() {
  185. options.Platform = "linux"
  186. }
  187. b := &Builder{
  188. clientCtx: clientCtx,
  189. options: config,
  190. Stdout: options.ProgressWriter.StdoutFormatter,
  191. Stderr: options.ProgressWriter.StderrFormatter,
  192. Aux: options.ProgressWriter.AuxFormatter,
  193. Output: options.ProgressWriter.Output,
  194. docker: options.Backend,
  195. idMappings: options.IDMappings,
  196. buildArgs: newBuildArgs(config.BuildArgs),
  197. buildStages: newBuildStages(),
  198. imageSources: newImageSources(clientCtx, options),
  199. pathCache: options.PathCache,
  200. imageProber: newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache),
  201. containerManager: newContainerManager(options.Backend),
  202. platform: options.Platform,
  203. }
  204. return b
  205. }
  206. // Build runs the Dockerfile builder by parsing the Dockerfile and executing
  207. // the instructions from the file.
  208. func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
  209. defer b.imageSources.Unmount()
  210. addNodesForLabelOption(dockerfile.AST, b.options.Labels)
  211. if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
  212. buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
  213. return nil, validationError{err}
  214. }
  215. dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
  216. if err != nil {
  217. return nil, err
  218. }
  219. if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
  220. buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
  221. return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
  222. }
  223. dockerfile.PrintWarnings(b.Stderr)
  224. b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr)
  225. if dispatchState.imageID == "" {
  226. buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
  227. return nil, errors.New("No image was generated. Is your Dockerfile empty?")
  228. }
  229. return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
  230. }
  231. func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
  232. if aux == nil || state.imageID == "" {
  233. return nil
  234. }
  235. return aux.Emit(types.BuildResult{ID: state.imageID})
  236. }
  237. func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
  238. shlex := NewShellLex(dockerfile.EscapeToken)
  239. state := newDispatchState()
  240. total := len(dockerfile.AST.Children)
  241. var err error
  242. for i, n := range dockerfile.AST.Children {
  243. select {
  244. case <-b.clientCtx.Done():
  245. logrus.Debug("Builder: build cancelled!")
  246. fmt.Fprint(b.Stdout, "Build cancelled")
  247. buildsFailed.WithValues(metricsBuildCanceled).Inc()
  248. return nil, errors.New("Build cancelled")
  249. default:
  250. // Not cancelled yet, keep going...
  251. }
  252. // If this is a FROM and we have a previous image then
  253. // emit an aux message for that image since it is the
  254. // end of the previous stage
  255. if n.Value == command.From {
  256. if err := emitImageID(b.Aux, state); err != nil {
  257. return nil, err
  258. }
  259. }
  260. if n.Value == command.From && state.isCurrentStage(b.options.Target) {
  261. break
  262. }
  263. opts := dispatchOptions{
  264. state: state,
  265. stepMsg: formatStep(i, total),
  266. node: n,
  267. shlex: shlex,
  268. source: source,
  269. }
  270. if state, err = b.dispatch(opts); err != nil {
  271. if b.options.ForceRemove {
  272. b.containerManager.RemoveAll(b.Stdout)
  273. }
  274. return nil, err
  275. }
  276. fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
  277. if b.options.Remove {
  278. b.containerManager.RemoveAll(b.Stdout)
  279. }
  280. }
  281. // Emit a final aux message for the final image
  282. if err := emitImageID(b.Aux, state); err != nil {
  283. return nil, err
  284. }
  285. return state, nil
  286. }
  287. func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
  288. if len(labels) == 0 {
  289. return
  290. }
  291. node := parser.NodeFromLabels(labels)
  292. dockerfile.Children = append(dockerfile.Children, node)
  293. }
  294. // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
  295. // It will:
  296. // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
  297. // - Do build by calling builder.dispatch() to call all entries' handling routines
  298. //
  299. // BuildFromConfig is used by the /commit endpoint, with the changes
  300. // coming from the query parameter of the same name.
  301. //
  302. // TODO: Remove?
  303. func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
  304. if len(changes) == 0 {
  305. return config, nil
  306. }
  307. b := newBuilder(context.Background(), builderOptions{
  308. Options: &types.ImageBuildOptions{NoCache: true},
  309. })
  310. dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
  311. if err != nil {
  312. return nil, validationError{err}
  313. }
  314. // TODO @jhowardmsft LCOW support. For now, if LCOW enabled, switch to linux.
  315. // Also explicitly set the platform. Ultimately this will be in the builder
  316. // options, but we can't do that yet as it would change the API.
  317. if dockerfile.Platform == "" {
  318. dockerfile.Platform = runtime.GOOS
  319. }
  320. if dockerfile.Platform == "windows" && system.LCOWSupported() {
  321. dockerfile.Platform = "linux"
  322. }
  323. b.platform = dockerfile.Platform
  324. // ensure that the commands are valid
  325. for _, n := range dockerfile.AST.Children {
  326. if !validCommitCommands[n.Value] {
  327. return nil, validationError{errors.Errorf("%s is not a valid change command", n.Value)}
  328. }
  329. }
  330. b.Stdout = ioutil.Discard
  331. b.Stderr = ioutil.Discard
  332. b.disableCommit = true
  333. if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
  334. return nil, validationError{err}
  335. }
  336. dispatchState := newDispatchState()
  337. dispatchState.runConfig = config
  338. return dispatchFromDockerfile(b, dockerfile, dispatchState, nil)
  339. }
  340. func checkDispatchDockerfile(dockerfile *parser.Node) error {
  341. for _, n := range dockerfile.Children {
  342. if err := checkDispatch(n); err != nil {
  343. return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine)
  344. }
  345. }
  346. return nil
  347. }
  348. func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
  349. shlex := NewShellLex(result.EscapeToken)
  350. ast := result.AST
  351. total := len(ast.Children)
  352. for i, n := range ast.Children {
  353. opts := dispatchOptions{
  354. state: dispatchState,
  355. stepMsg: formatStep(i, total),
  356. node: n,
  357. shlex: shlex,
  358. source: source,
  359. }
  360. if _, err := b.dispatch(opts); err != nil {
  361. return nil, err
  362. }
  363. }
  364. return dispatchState.runConfig, nil
  365. }