PageRenderTime 116ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/pkg/build/build.go

https://github.com/Arkan/drone
Go | 556 lines | 306 code | 96 blank | 154 comment | 80 complexity | 41e5d975af53e1a0f41aefcc4ba1e365 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, BSD-3-Clause, LGPL-3.0, Apache-2.0, BSD-2-Clause
  1. package build
  2. import (
  3. "fmt"
  4. "io"
  5. "io/ioutil"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "strings"
  10. "time"
  11. "github.com/drone/drone/pkg/build/buildfile"
  12. "github.com/drone/drone/pkg/build/docker"
  13. "github.com/drone/drone/pkg/build/dockerfile"
  14. "github.com/drone/drone/pkg/build/log"
  15. "github.com/drone/drone/pkg/build/proxy"
  16. "github.com/drone/drone/pkg/build/repo"
  17. "github.com/drone/drone/pkg/build/script"
  18. )
  19. // BuildState stores information about a build
  20. // process including the Exit status and various
  21. // Runtime statistics (coming soon).
  22. type BuildState struct {
  23. Started int64
  24. Finished int64
  25. ExitCode int
  26. // we may eventually include detailed resource
  27. // usage statistics, including including CPU time,
  28. // Max RAM, Max Swap, Disk space, and more.
  29. }
  30. func New(dockerClient *docker.Client) *Builder {
  31. return &Builder{
  32. dockerClient: dockerClient,
  33. }
  34. }
  35. // Builder represents a build process being prepared
  36. // to run.
  37. type Builder struct {
  38. // Image specifies the Docker Image that will be
  39. // used to virtualize the Build process.
  40. Build *script.Build
  41. // Source specifies the Repository path of the code
  42. // that we are testing.
  43. //
  44. // The source repository may be a local repository
  45. // on the current filesystem, or a remote repository
  46. // on GitHub, Bitbucket, etc.
  47. Repo *repo.Repo
  48. // Key is an identify file, such as an RSA private key, that
  49. // will be copied into the environments ~/.ssh/id_rsa file.
  50. Key []byte
  51. // Timeout is the maximum amount of to will wait for a process
  52. // to exit. The default is no timeout.
  53. Timeout time.Duration
  54. // Privileged indicates the build should be executed in privileged
  55. // mode. The default is false.
  56. Privileged bool
  57. // Stdout specifies the builds's standard output.
  58. //
  59. // If stdout is nil, Run connects the corresponding file descriptor
  60. // to the null device (os.DevNull).
  61. Stdout io.Writer
  62. // BuildState contains information about an exited build,
  63. // available after a call to Run.
  64. BuildState *BuildState
  65. // Docker image that was created for
  66. // this build.
  67. image *docker.Image
  68. // Docker container was that created
  69. // for this build.
  70. container *docker.Run
  71. // Docker containers created for the
  72. // specified services and linked to
  73. // this build.
  74. services []*docker.Container
  75. dockerClient *docker.Client
  76. }
  77. func (b *Builder) Run() error {
  78. // teardown will remove the Image and stop and
  79. // remove the service containers after the
  80. // build is done running.
  81. defer b.teardown()
  82. // setup will create the Image and supporting
  83. // service containers.
  84. if err := b.setup(); err != nil {
  85. return err
  86. }
  87. // make sure build state is not nil
  88. b.BuildState = &BuildState{}
  89. b.BuildState.ExitCode = 0
  90. b.BuildState.Started = time.Now().UTC().Unix()
  91. c := make(chan error, 1)
  92. go func() {
  93. c <- b.run()
  94. }()
  95. // wait for either a) the job to complete or b) the job to timeout
  96. select {
  97. case err := <-c:
  98. return err
  99. case <-time.After(b.Timeout):
  100. log.Errf("time limit exceeded for build %s", b.Build.Name)
  101. b.BuildState.ExitCode = 124
  102. b.BuildState.Finished = time.Now().UTC().Unix()
  103. return nil
  104. }
  105. }
  106. func (b *Builder) setup() error {
  107. // temp directory to store all files required
  108. // to generate the Docker image.
  109. dir, err := ioutil.TempDir("", "drone-")
  110. if err != nil {
  111. return err
  112. }
  113. // clean up after our mess.
  114. defer os.RemoveAll(dir)
  115. // make sure the image isn't empty. this would be bad
  116. if len(b.Build.Image) == 0 {
  117. log.Err("Fatal Error, No Docker Image specified")
  118. return fmt.Errorf("Error: missing Docker image")
  119. }
  120. // if we're using an alias for the build name we
  121. // should substitute it now
  122. if alias, ok := builders[b.Build.Image]; ok {
  123. b.Build.Image = alias.Tag
  124. }
  125. // if this is a local repository we should symlink
  126. // to the source code in our temp directory
  127. if b.Repo.IsLocal() {
  128. // this is where we used to use symlinks. We should
  129. // talk to the docker team about this, since copying
  130. // the entire repository is slow :(
  131. //
  132. // see https://github.com/dotcloud/docker/pull/3567
  133. //src := filepath.Join(dir, "src")
  134. //err = os.Symlink(b.Repo.Path, src)
  135. //if err != nil {
  136. // return err
  137. //}
  138. src := filepath.Join(dir, "src")
  139. cmd := exec.Command("cp", "-a", b.Repo.Path, src)
  140. if err := cmd.Run(); err != nil {
  141. return err
  142. }
  143. }
  144. // start all services required for the build
  145. // that will get linked to the container.
  146. for _, service := range b.Build.Services {
  147. // Parse the name of the Docker image
  148. // And then construct a fully qualified image name
  149. owner, name, tag := parseImageName(service)
  150. cname := fmt.Sprintf("%s/%s:%s", owner, name, tag)
  151. // Get the image info
  152. img, err := b.dockerClient.Images.Inspect(cname)
  153. if err != nil {
  154. // Get the image if it doesn't exist
  155. if err := b.dockerClient.Images.Pull(cname); err != nil {
  156. return fmt.Errorf("Error: Unable to pull image %s", cname)
  157. }
  158. img, err = b.dockerClient.Images.Inspect(cname)
  159. if err != nil {
  160. return fmt.Errorf("Error: Invalid or unknown image %s", cname)
  161. }
  162. }
  163. // debugging
  164. log.Infof("starting service container %s", cname)
  165. // Run the contianer
  166. run, err := b.dockerClient.Containers.RunDaemonPorts(cname, img.Config.ExposedPorts)
  167. if err != nil {
  168. return err
  169. }
  170. // Get the container info
  171. info, err := b.dockerClient.Containers.Inspect(run.ID)
  172. if err != nil {
  173. // on error kill the container since it hasn't yet been
  174. // added to the array and would therefore not get
  175. // removed in the defer statement.
  176. b.dockerClient.Containers.Stop(run.ID, 10)
  177. b.dockerClient.Containers.Remove(run.ID)
  178. return err
  179. }
  180. // Add the running service to the list
  181. b.services = append(b.services, info)
  182. }
  183. if err := b.writeIdentifyFile(dir); err != nil {
  184. return err
  185. }
  186. if err := b.writeBuildScript(dir); err != nil {
  187. return err
  188. }
  189. if err := b.writeProxyScript(dir); err != nil {
  190. return err
  191. }
  192. if err := b.writeDockerfile(dir); err != nil {
  193. return err
  194. }
  195. // debugging
  196. log.Info("creating build image")
  197. // check for build container (ie bradrydzewski/go:1.2)
  198. // and download if it doesn't already exist
  199. if _, err := b.dockerClient.Images.Inspect(b.Build.Image); err == docker.ErrNotFound {
  200. // download the image if it doesn't exist
  201. if err := b.dockerClient.Images.Pull(b.Build.Image); err != nil {
  202. return err
  203. }
  204. }
  205. // create the Docker image
  206. id := createUID()
  207. if err := b.dockerClient.Images.Build(id, dir); err != nil {
  208. return err
  209. }
  210. // debugging
  211. log.Infof("copying repository to %s", b.Repo.Dir)
  212. // get the image details
  213. b.image, err = b.dockerClient.Images.Inspect(id)
  214. if err != nil {
  215. // if we have problems with the image make sure
  216. // we remove it before we exit
  217. b.dockerClient.Images.Remove(id)
  218. return err
  219. }
  220. return nil
  221. }
  222. // teardown is a helper function that we can use to
  223. // stop and remove the build container, its supporting image,
  224. // and the supporting service containers.
  225. func (b *Builder) teardown() error {
  226. // stop and destroy the container
  227. if b.container != nil {
  228. // debugging
  229. log.Info("removing build container")
  230. // stop the container, ignore error message
  231. b.dockerClient.Containers.Stop(b.container.ID, 15)
  232. // remove the container, ignore error message
  233. if err := b.dockerClient.Containers.Remove(b.container.ID); err != nil {
  234. log.Errf("failed to delete build container %s", b.container.ID)
  235. }
  236. }
  237. // stop and destroy the container services
  238. for i, container := range b.services {
  239. // debugging
  240. log.Infof("removing service container %s", b.Build.Services[i])
  241. // stop the service container, ignore the error
  242. b.dockerClient.Containers.Stop(container.ID, 15)
  243. // remove the service container, ignore the error
  244. if err := b.dockerClient.Containers.Remove(container.ID); err != nil {
  245. log.Errf("failed to delete service container %s", container.ID)
  246. }
  247. }
  248. // destroy the underlying image
  249. if b.image != nil {
  250. // debugging
  251. log.Info("removing build image")
  252. if _, err := b.dockerClient.Images.Remove(b.image.ID); err != nil {
  253. log.Errf("failed to completely delete build image %s. %s", b.image.ID, err.Error())
  254. }
  255. }
  256. return nil
  257. }
  258. func (b *Builder) run() error {
  259. // create and run the container
  260. conf := docker.Config{
  261. Image: b.image.ID,
  262. AttachStdin: false,
  263. AttachStdout: true,
  264. AttachStderr: true,
  265. }
  266. // configure if Docker should run in privileged mode
  267. host := docker.HostConfig{
  268. Privileged: (b.Privileged && len(b.Repo.PR) == 0),
  269. }
  270. // debugging
  271. log.Noticef("starting build %s", b.Build.Name)
  272. // link service containers
  273. for i, service := range b.services {
  274. // convert name of the image to a slug
  275. _, name, _ := parseImageName(b.Build.Services[i])
  276. // link the service container to our
  277. // build container.
  278. host.Links = append(host.Links, service.Name[1:]+":"+name)
  279. }
  280. // where are temp files going to go?
  281. tmpPath := "/tmp/drone"
  282. if len(os.Getenv("DRONE_TMP")) > 0 {
  283. tmpPath = os.Getenv("DRONE_TMP")
  284. }
  285. log.Infof("temp directory is %s", tmpPath)
  286. if err := os.MkdirAll(tmpPath, 0777); err != nil {
  287. return fmt.Errorf("Failed to create temp directory at %s: %s", tmpPath, err)
  288. }
  289. // link cached volumes
  290. conf.Volumes = make(map[string]struct{})
  291. for _, volume := range b.Build.Cache {
  292. name := filepath.Clean(b.Repo.Name)
  293. branch := filepath.Clean(b.Repo.Branch)
  294. volume := filepath.Clean(volume)
  295. // with Docker, volumes must be an absolute path. If an absolute
  296. // path is not provided, then assume it is for the repository
  297. // working directory.
  298. if strings.HasPrefix(volume, "/") == false {
  299. volume = filepath.Join(b.Repo.Dir, volume)
  300. }
  301. // local cache path on the host machine
  302. // this path is going to be really long
  303. hostpath := filepath.Join(tmpPath, name, branch, volume)
  304. // check if the volume is created
  305. if _, err := os.Stat(hostpath); err != nil {
  306. // if does not exist then create
  307. os.MkdirAll(hostpath, 0777)
  308. }
  309. host.Binds = append(host.Binds, hostpath+":"+volume)
  310. conf.Volumes[volume] = struct{}{}
  311. // debugging
  312. log.Infof("mounting volume %s:%s", hostpath, volume)
  313. }
  314. // create the container from the image
  315. run, err := b.dockerClient.Containers.Create(&conf)
  316. if err != nil {
  317. return err
  318. }
  319. // cache instance of docker.Run
  320. b.container = run
  321. // attach to the container
  322. go func() {
  323. b.dockerClient.Containers.Attach(run.ID, &writer{b.Stdout})
  324. }()
  325. // start the container
  326. if err := b.dockerClient.Containers.Start(run.ID, &host); err != nil {
  327. b.BuildState.ExitCode = 1
  328. b.BuildState.Finished = time.Now().UTC().Unix()
  329. return err
  330. }
  331. // wait for the container to stop
  332. wait, err := b.dockerClient.Containers.Wait(run.ID)
  333. if err != nil {
  334. b.BuildState.ExitCode = 1
  335. b.BuildState.Finished = time.Now().UTC().Unix()
  336. return err
  337. }
  338. // set completion time
  339. b.BuildState.Finished = time.Now().UTC().Unix()
  340. // get the exit code if possible
  341. b.BuildState.ExitCode = wait.StatusCode
  342. return nil
  343. }
  344. // writeDockerfile is a helper function that generates a
  345. // Dockerfile and writes to the builds temporary directory
  346. // so that it can be used to create the Image.
  347. func (b *Builder) writeDockerfile(dir string) error {
  348. var dockerfile = dockerfile.New(b.Build.Image)
  349. dockerfile.WriteWorkdir(b.Repo.Dir)
  350. dockerfile.WriteAdd("drone", "/usr/local/bin/")
  351. // upload source code if repository is stored
  352. // on the host machine
  353. if b.Repo.IsRemote() == false {
  354. dockerfile.WriteAdd("src", filepath.Join(b.Repo.Dir))
  355. }
  356. switch {
  357. case strings.HasPrefix(b.Build.Image, "bradrydzewski/"),
  358. strings.HasPrefix(b.Build.Image, "drone/"):
  359. // the default user for all official Drone imnage
  360. // is the "ubuntu" user, since all build images
  361. // inherit from the ubuntu cloud ISO
  362. dockerfile.WriteUser("ubuntu")
  363. dockerfile.WriteEnv("HOME", "/home/ubuntu")
  364. dockerfile.WriteEnv("LANG", "en_US.UTF-8")
  365. dockerfile.WriteEnv("LANGUAGE", "en_US:en")
  366. dockerfile.WriteEnv("LOGNAME", "ubuntu")
  367. dockerfile.WriteEnv("TERM", "xterm")
  368. dockerfile.WriteEnv("SHELL", "/bin/bash")
  369. dockerfile.WriteAdd("id_rsa", "/home/ubuntu/.ssh/id_rsa")
  370. dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /home/ubuntu/.ssh")
  371. dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /var/cache/drone")
  372. dockerfile.WriteRun("sudo chown -R ubuntu:ubuntu /usr/local/bin/drone")
  373. dockerfile.WriteRun("sudo chmod 600 /home/ubuntu/.ssh/id_rsa")
  374. default:
  375. // all other images are assumed to use
  376. // the root user.
  377. dockerfile.WriteUser("root")
  378. dockerfile.WriteEnv("HOME", "/root")
  379. dockerfile.WriteEnv("LANG", "en_US.UTF-8")
  380. dockerfile.WriteEnv("LANGUAGE", "en_US:en")
  381. dockerfile.WriteEnv("LOGNAME", "root")
  382. dockerfile.WriteEnv("TERM", "xterm")
  383. dockerfile.WriteEnv("SHELL", "/bin/bash")
  384. dockerfile.WriteEnv("GOPATH", "/var/cache/drone")
  385. dockerfile.WriteAdd("id_rsa", "/root/.ssh/id_rsa")
  386. dockerfile.WriteRun("chmod 600 /root/.ssh/id_rsa")
  387. dockerfile.WriteRun("echo 'StrictHostKeyChecking no' > /root/.ssh/config")
  388. }
  389. dockerfile.WriteAdd("proxy.sh", "/etc/drone.d/")
  390. dockerfile.WriteEntrypoint("/bin/bash -e /usr/local/bin/drone")
  391. // write the Dockerfile to the temporary directory
  392. return ioutil.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile.Bytes(), 0700)
  393. }
  394. // writeBuildScript is a helper function that
  395. // will generate the build script file in the builder's
  396. // temp directory to be added to the Image.
  397. func (b *Builder) writeBuildScript(dir string) error {
  398. f := buildfile.New()
  399. // add environment variables about the build
  400. f.WriteEnv("CI", "true")
  401. f.WriteEnv("DRONE", "true")
  402. f.WriteEnv("DRONE_BRANCH", b.Repo.Branch)
  403. f.WriteEnv("DRONE_COMMIT", b.Repo.Commit)
  404. f.WriteEnv("DRONE_PR", b.Repo.PR)
  405. f.WriteEnv("DRONE_BUILD_DIR", b.Repo.Dir)
  406. // add environment variables for code coverage
  407. // systems, like coveralls.
  408. f.WriteEnv("CI_NAME", "DRONE")
  409. f.WriteEnv("CI_BUILD_NUMBER", b.Repo.Commit)
  410. f.WriteEnv("CI_BUILD_URL", "")
  411. f.WriteEnv("CI_BRANCH", b.Repo.Branch)
  412. f.WriteEnv("CI_PULL_REQUEST", b.Repo.PR)
  413. // add /etc/hosts entries
  414. for _, mapping := range b.Build.Hosts {
  415. f.WriteHost(mapping)
  416. }
  417. // if the repository is remote then we should
  418. // add the commands to the build script to
  419. // clone the repository
  420. if b.Repo.IsRemote() {
  421. for _, cmd := range b.Repo.Commands() {
  422. f.WriteCmd(cmd)
  423. }
  424. }
  425. // if the commit is for merging a pull request
  426. // we should only execute the build commands,
  427. // and omit the deploy and publish commands.
  428. if len(b.Repo.PR) == 0 {
  429. b.Build.Write(f, b.Repo)
  430. } else {
  431. // only write the build commands
  432. b.Build.WriteBuild(f)
  433. }
  434. scriptfilePath := filepath.Join(dir, "drone")
  435. return ioutil.WriteFile(scriptfilePath, f.Bytes(), 0700)
  436. }
  437. // writeProxyScript is a helper function that
  438. // will generate the proxy.sh file in the builder's
  439. // temp directory to be added to the Image.
  440. func (b *Builder) writeProxyScript(dir string) error {
  441. var proxyfile = proxy.Proxy{}
  442. // loop through services so that we can
  443. // map ip address to localhost
  444. for _, container := range b.services {
  445. // create an entry for each port
  446. for port := range container.NetworkSettings.Ports {
  447. proxyfile.Set(port.Port(), container.NetworkSettings.IPAddress)
  448. }
  449. }
  450. // write the proxyfile to the temp directory
  451. proxyfilePath := filepath.Join(dir, "proxy.sh")
  452. return ioutil.WriteFile(proxyfilePath, proxyfile.Bytes(), 0755)
  453. }
  454. // writeIdentifyFile is a helper function that
  455. // will generate the id_rsa file in the builder's
  456. // temp directory to be added to the Image.
  457. func (b *Builder) writeIdentifyFile(dir string) error {
  458. keyfilePath := filepath.Join(dir, "id_rsa")
  459. return ioutil.WriteFile(keyfilePath, b.Key, 0700)
  460. }