builder.go 12 KB

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