builder.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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, options.SessionID)
  128. if err != nil {
  129. return nil, err
  130. }
  131. src, err := bm.fsCache.SyncFrom(ctx, csi)
  132. if err != nil {
  133. return nil, err
  134. }
  135. logrus.Debugf("sync-time: %v", time.Since(st))
  136. return src, nil
  137. }
  138. return nil, nil
  139. }
  140. // builderOptions are the dependencies required by the builder
  141. type builderOptions struct {
  142. Options *types.ImageBuildOptions
  143. Backend builder.Backend
  144. ProgressWriter backend.ProgressWriter
  145. PathCache pathCache
  146. Archiver *archive.Archiver
  147. Platform string
  148. }
  149. // Builder is a Dockerfile builder
  150. // It implements the builder.Backend interface.
  151. type Builder struct {
  152. options *types.ImageBuildOptions
  153. Stdout io.Writer
  154. Stderr io.Writer
  155. Aux *streamformatter.AuxFormatter
  156. Output io.Writer
  157. docker builder.Backend
  158. clientCtx context.Context
  159. archiver *archive.Archiver
  160. buildStages *buildStages
  161. disableCommit bool
  162. buildArgs *buildArgs
  163. imageSources *imageSources
  164. pathCache pathCache
  165. containerManager *containerManager
  166. imageProber ImageProber
  167. // TODO @jhowardmft LCOW Support. This will be moved to options at a later
  168. // stage, however that cannot be done now as it affects the public API
  169. // if it were.
  170. platform string
  171. }
  172. // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
  173. // TODO @jhowardmsft LCOW support: Eventually platform can be moved into the builder
  174. // options, however, that would be an API change as it shares types.ImageBuildOptions.
  175. func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
  176. config := options.Options
  177. if config == nil {
  178. config = new(types.ImageBuildOptions)
  179. }
  180. // @jhowardmsft LCOW Support. For the time being, this is interim. Eventually
  181. // will be moved to types.ImageBuildOptions, but it can't for now as that would
  182. // be an API change.
  183. if options.Platform == "" {
  184. options.Platform = runtime.GOOS
  185. }
  186. if options.Platform == "windows" && system.LCOWSupported() {
  187. options.Platform = "linux"
  188. }
  189. b := &Builder{
  190. clientCtx: clientCtx,
  191. options: config,
  192. Stdout: options.ProgressWriter.StdoutFormatter,
  193. Stderr: options.ProgressWriter.StderrFormatter,
  194. Aux: options.ProgressWriter.AuxFormatter,
  195. Output: options.ProgressWriter.Output,
  196. docker: options.Backend,
  197. archiver: options.Archiver,
  198. buildArgs: newBuildArgs(config.BuildArgs),
  199. buildStages: newBuildStages(),
  200. imageSources: newImageSources(clientCtx, options),
  201. pathCache: options.PathCache,
  202. imageProber: newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache),
  203. containerManager: newContainerManager(options.Backend),
  204. platform: options.Platform,
  205. }
  206. return b
  207. }
  208. // Build runs the Dockerfile builder by parsing the Dockerfile and executing
  209. // the instructions from the file.
  210. func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
  211. defer b.imageSources.Unmount()
  212. addNodesForLabelOption(dockerfile.AST, b.options.Labels)
  213. if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
  214. buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
  215. return nil, err
  216. }
  217. dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
  218. if err != nil {
  219. return nil, err
  220. }
  221. if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
  222. buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
  223. return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
  224. }
  225. dockerfile.PrintWarnings(b.Stderr)
  226. b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr)
  227. if dispatchState.imageID == "" {
  228. buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
  229. return nil, errors.New("No image was generated. Is your Dockerfile empty?")
  230. }
  231. return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
  232. }
  233. func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
  234. if aux == nil || state.imageID == "" {
  235. return nil
  236. }
  237. return aux.Emit(types.BuildResult{ID: state.imageID})
  238. }
  239. func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
  240. shlex := NewShellLex(dockerfile.EscapeToken)
  241. state := newDispatchState()
  242. total := len(dockerfile.AST.Children)
  243. var err error
  244. for i, n := range dockerfile.AST.Children {
  245. select {
  246. case <-b.clientCtx.Done():
  247. logrus.Debug("Builder: build cancelled!")
  248. fmt.Fprint(b.Stdout, "Build cancelled")
  249. buildsFailed.WithValues(metricsBuildCanceled).Inc()
  250. return nil, errors.New("Build cancelled")
  251. default:
  252. // Not cancelled yet, keep going...
  253. }
  254. // If this is a FROM and we have a previous image then
  255. // emit an aux message for that image since it is the
  256. // end of the previous stage
  257. if n.Value == command.From {
  258. if err := emitImageID(b.Aux, state); err != nil {
  259. return nil, err
  260. }
  261. }
  262. if n.Value == command.From && state.isCurrentStage(b.options.Target) {
  263. break
  264. }
  265. opts := dispatchOptions{
  266. state: state,
  267. stepMsg: formatStep(i, total),
  268. node: n,
  269. shlex: shlex,
  270. source: source,
  271. }
  272. if state, err = b.dispatch(opts); err != nil {
  273. if b.options.ForceRemove {
  274. b.containerManager.RemoveAll(b.Stdout)
  275. }
  276. return nil, err
  277. }
  278. fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
  279. if b.options.Remove {
  280. b.containerManager.RemoveAll(b.Stdout)
  281. }
  282. }
  283. // Emit a final aux message for the final image
  284. if err := emitImageID(b.Aux, state); err != nil {
  285. return nil, err
  286. }
  287. return state, nil
  288. }
  289. func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
  290. if len(labels) == 0 {
  291. return
  292. }
  293. node := parser.NodeFromLabels(labels)
  294. dockerfile.Children = append(dockerfile.Children, node)
  295. }
  296. // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
  297. // It will:
  298. // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
  299. // - Do build by calling builder.dispatch() to call all entries' handling routines
  300. //
  301. // BuildFromConfig is used by the /commit endpoint, with the changes
  302. // coming from the query parameter of the same name.
  303. //
  304. // TODO: Remove?
  305. func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
  306. if len(changes) == 0 {
  307. return config, nil
  308. }
  309. b := newBuilder(context.Background(), builderOptions{
  310. Options: &types.ImageBuildOptions{NoCache: true},
  311. })
  312. dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
  313. if err != nil {
  314. return nil, err
  315. }
  316. // TODO @jhowardmsft LCOW support. For now, if LCOW enabled, switch to linux.
  317. // Also explicitly set the platform. Ultimately this will be in the builder
  318. // options, but we can't do that yet as it would change the API.
  319. if dockerfile.Platform == "" {
  320. dockerfile.Platform = runtime.GOOS
  321. }
  322. if dockerfile.Platform == "windows" && system.LCOWSupported() {
  323. dockerfile.Platform = "linux"
  324. }
  325. b.platform = dockerfile.Platform
  326. // ensure that the commands are valid
  327. for _, n := range dockerfile.AST.Children {
  328. if !validCommitCommands[n.Value] {
  329. return nil, fmt.Errorf("%s is not a valid change command", n.Value)
  330. }
  331. }
  332. b.Stdout = ioutil.Discard
  333. b.Stderr = ioutil.Discard
  334. b.disableCommit = true
  335. if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
  336. return nil, err
  337. }
  338. dispatchState := newDispatchState()
  339. dispatchState.runConfig = config
  340. return dispatchFromDockerfile(b, dockerfile, dispatchState, nil)
  341. }
  342. func checkDispatchDockerfile(dockerfile *parser.Node) error {
  343. for _, n := range dockerfile.Children {
  344. if err := checkDispatch(n); err != nil {
  345. return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine)
  346. }
  347. }
  348. return nil
  349. }
  350. func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
  351. shlex := NewShellLex(result.EscapeToken)
  352. ast := result.AST
  353. total := len(ast.Children)
  354. for i, n := range ast.Children {
  355. opts := dispatchOptions{
  356. state: dispatchState,
  357. stepMsg: formatStep(i, total),
  358. node: n,
  359. shlex: shlex,
  360. source: source,
  361. }
  362. if _, err := b.dispatch(opts); err != nil {
  363. return nil, err
  364. }
  365. }
  366. return dispatchState.runConfig, nil
  367. }