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