builder.go 11 KB

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