// Exec executes a build with the given payload and options. If the // build fails, an *Error is returned. func Exec(payload Payload, opt Options, outw, errw io.Writer) error { var sec *secure.Secure if payload.Keys != nil && len(payload.YamlEnc) != 0 { var err error sec, err = secure.Parse(payload.YamlEnc, payload.Keys.Private) if err != nil { return fmt.Errorf("decrypting encrypted secrets: %s", err) } log.Debugln("Successfully decrypted secrets") } // TODO This block of code (and the above block) need to be cleaned // up and written in a manner that facilitates better unit testing. if sec != nil { verified := shasum.Check(payload.Yaml, sec.Checksum) // the checksum should be invalidated if the repository is // public, and the build is a pull request, and the checksum // value was not provided. if payload.Build.Event == plugin.EventPull && !payload.Repo.IsPrivate && len(sec.Checksum) == 0 { verified = false } switch { case verified && payload.Build.Event == plugin.EventPull: log.Debugln("Injected secrets into Yaml safely") var err error payload.Yaml, err = inject.InjectSafe(payload.Yaml, sec.Environment.Map()) if err != nil { return fmt.Errorf("injecting yaml secrets: %s", err) } case verified: log.Debugln("Injected secrets into Yaml") payload.Yaml = inject.Inject(payload.Yaml, sec.Environment.Map()) case !verified: // if we can't validate the Yaml file we don't inject // secrets, and therefore shouldn't bother running the // deploy and notify tests. opt.Deploy = false opt.Notify = false log.Errorln("Unable to validate Yaml checksum. This means that secrets will not be injected. In addition the Deploy and Notify steps will not be executed. To resolve this please regenerate the secrets file.", sec.Checksum) } } // injects the matrix configuration parameters // into the yaml prior to parsing. injectParams := map[string]string{ "COMMIT_SHORT": payload.Build.Commit, // DEPRECATED "COMMIT": payload.Build.Commit, "BRANCH": payload.Build.Branch, "BUILD_NUMBER": strconv.Itoa(payload.Build.Number), } if payload.Build.Event == plugin.EventTag { injectParams["TAG"] = strings.TrimPrefix(payload.Build.Ref, "refs/tags/") } payload.Yaml = inject.Inject(payload.Yaml, payload.Job.Environment) payload.Yaml = inject.Inject(payload.Yaml, injectParams) // safely inject global variables var globals = map[string]string{} for _, s := range payload.System.Globals { parts := strings.SplitN(s, "=", 2) if len(parts) != 2 { continue } globals[parts[0]] = parts[1] } if payload.Repo.IsPrivate { payload.Yaml = inject.Inject(payload.Yaml, globals) } else { payload.Yaml, _ = inject.InjectSafe(payload.Yaml, globals) } // extracts the clone path from the yaml. If // the clone path doesn't exist it uses a path // derrived from the repository uri. payload.Workspace = &plugin.Workspace{Keys: payload.Keys, Netrc: payload.Netrc} payload.Workspace.Path = path.Parse(payload.Yaml, payload.Repo.Link) payload.Workspace.Root = "/drone/src" log.Debugf("Using workspace %s", payload.Workspace.Path) rules := []parser.RuleFunc{ parser.ImageName, parser.ImageMatchFunc(payload.System.Plugins), parser.ImagePullFunc(opt.Force), parser.SanitizeFunc(payload.Repo.IsTrusted), //&& !plugin.PullRequest(payload.Build) parser.CacheFunc(payload.Repo.FullName), parser.DebugFunc(yaml.ParseDebugString(payload.Yaml)), parser.Escalate, parser.HttpProxy, parser.DefaultNotifyFilter, } if len(opt.Mount) != 0 { log.Debugf("Mounting %s as workspace %s", opt.Mount, payload.Workspace.Path, ) rules = append(rules, parser.MountFunc( opt.Mount, payload.Workspace.Path, )) } tree, err := parser.Parse(payload.Yaml, rules) if err != nil { // TODO(sqs): There was a comment here saying "print error // messages in debug mode only". Is this because of security // (e.g., the decrypted YAML secrets could leak in the error // message)? If so, don't return the err here; instead, return // a simple error message such as "error parsing yaml". return err } r := runner.Load(tree) client, err := dockerclient.NewDockerClient("unix:///var/run/docker.sock", nil) if err != nil { return err } // // creates a wrapper Docker client that uses an ambassador // // container to create a pod-like environment. controller, err := docker.NewClient(client) if err != nil { return fmt.Errorf("creating docker ambassador container: %s", err) } defer controller.Destroy() // watch for sigkill (timeout or cancel build) killc := make(chan os.Signal, 1) signal.Notify(killc, syscall.SIGINT, syscall.SIGTERM) go func() { <-killc log.Println("Cancel request received, killing process") controller.Destroy() // possibe race here. implement lock on the other end os.Exit(130) // cancel is treated like ctrl+c }() go func() { var timeout = payload.Repo.Timeout if timeout == 0 { timeout = 60 } <-time.After(time.Duration(timeout) * time.Minute) log.Println("Timeout request received, killing process") controller.Destroy() // possibe race here. implement lock on the other end os.Exit(128) // cancel is treated like ctrl+c }() state := &runner.State{ Client: controller, Stdout: outw, Stderr: errw, Repo: payload.Repo, Build: payload.Build, BuildLast: payload.BuildLast, Job: payload.Job, System: payload.System, Workspace: payload.Workspace, } if opt.Cache { log.Debugln("Running Cache step") err = r.RunNode(state, parser.NodeCache) if err != nil { log.Debugln(err) } } if opt.Clone { log.Debugln("Running Clone step") err = r.RunNode(state, parser.NodeClone) if err != nil { log.Debugln(err) } } if opt.Build && !state.Failed() { log.Debugln("Running Build and Compose steps") err = r.RunNode(state, parser.NodeCompose|parser.NodeBuild) if err != nil { log.Debugln(err) } } if opt.Deploy && !state.Failed() { log.Debugln("Running Publish and Deploy steps") err = r.RunNode(state, parser.NodePublish|parser.NodeDeploy) if err != nil { log.Debugln(err) } } // if the build is not failed, at this point // we can mark as successful if !state.Failed() { state.Job.Status = plugin.StateSuccess state.Build.Status = plugin.StateSuccess } if opt.Cache { log.Debugln("Running post-Build Cache steps") err = r.RunNode(state, parser.NodeCache) if err != nil { log.Debugln(err) } } if opt.Notify { log.Debugln("Running Notify steps") err = r.RunNode(state, parser.NodeNotify) if err != nil { log.Debugln(err) } } if state.Failed() { controller.Destroy() return &Error{ExitCode: state.ExitCode()} } return nil }
func main() { // parses command line flags flag.BoolVar(&cache, "cache", false, "") flag.BoolVar(&clone, "clone", false, "") flag.BoolVar(&build, "build", false, "") flag.BoolVar(&deploy, "deploy", false, "") flag.BoolVar(¬ify, "notify", false, "") flag.BoolVar(&debug, "debug", false, "") flag.BoolVar(&force, "pull", false, "") flag.StringVar(&mount, "mount", "", "") flag.Parse() // unmarshal the json payload via stdin or // via the command line args (whichever was used) plugin.MustUnmarshal(&payload) // configure the default log format and // log levels if yaml.ParseDebugString(payload.Yaml) { log.SetLevel(log.DebugLevel) } log.SetFormatter(new(formatter)) var sec *secure.Secure if payload.Keys != nil && len(payload.YamlEnc) != 0 { var err error sec, err = secure.Parse(payload.YamlEnc, payload.Keys.Private) if err != nil { log.Debugln("Unable to decrypt encrypted secrets", err) } else { log.Debugln("Successfully decrypted secrets") } } // TODO This block of code (and the above block) need to be cleaned // up and written in a manner that facilitates better unit testing. if sec != nil { verified := shasum.Check(payload.Yaml, sec.Checksum) // the checksum should be invalidated if the repository is // public, and the build is a pull request, and the checksum // value was not provided. if payload.Build.Event == plugin.EventPull && !payload.Repo.IsPrivate && len(sec.Checksum) == 0 { verified = false } switch { case verified && payload.Build.Event == plugin.EventPull: log.Debugln("Injected secrets into Yaml safely") var err error payload.Yaml, err = inject.InjectSafe(payload.Yaml, sec.Environment.Map()) if err != nil { fmt.Println("Error injecting Yaml secrets") os.Exit(1) } case verified: log.Debugln("Injected secrets into Yaml") payload.Yaml = inject.Inject(payload.Yaml, sec.Environment.Map()) case !verified: // if we can't validate the Yaml file we don't inject // secrets, and therefore shouldn't bother running the // deploy and notify tests. deploy = false notify = false fmt.Println("Unable to validate Yaml checksum.", sec.Checksum) } } // injects the matrix configuration parameters // into the yaml prior to parsing. injectParams := map[string]string{ "COMMIT_SHORT": payload.Build.Commit[:7], "COMMIT": payload.Build.Commit, "BRANCH": payload.Build.Branch, "BUILD_NUMBER": strconv.Itoa(payload.Build.Number), } if payload.Build.Event == plugin.EventTag { injectParams["TAG"] = strings.TrimPrefix(payload.Build.Ref, "refs/tags/") } payload.Yaml = inject.Inject(payload.Yaml, payload.Job.Environment) payload.Yaml = inject.Inject(payload.Yaml, injectParams) // safely inject global variables var globals = map[string]string{} for _, s := range payload.System.Globals { parts := strings.SplitN(s, "=", 2) if len(parts) != 2 { continue } globals[parts[0]] = parts[1] } payload.Yaml, _ = inject.InjectSafe(payload.Yaml, globals) // extracts the clone path from the yaml. If // the clone path doesn't exist it uses a path // derrived from the repository uri. payload.Workspace = &plugin.Workspace{Keys: payload.Keys, Netrc: payload.Netrc} payload.Workspace.Path = path.Parse(payload.Yaml, payload.Repo.Link) payload.Workspace.Root = "/drone/src" log.Debugf("Using workspace %s", payload.Workspace.Path) rules := []parser.RuleFunc{ parser.ImageName, parser.ImageMatchFunc(payload.System.Plugins), parser.ImagePullFunc(force), parser.SanitizeFunc(payload.Repo.IsTrusted), //&& !plugin.PullRequest(payload.Build) parser.CacheFunc(payload.Repo.FullName), parser.Escalate, parser.HttpProxy, parser.DefaultNotifyFilter, } if len(mount) != 0 { log.Debugf("Mounting %s as workspace %s", mount, payload.Workspace.Path, ) rules = append(rules, parser.MountFunc( mount, payload.Workspace.Path, )) } tree, err := parser.Parse(payload.Yaml, rules) if err != nil { log.Debugln(err) // print error messages in debug mode only log.Fatalln("Error parsing the .drone.yml") os.Exit(1) } r := runner.Load(tree) client, err := dockerclient.NewDockerClient("unix:///var/run/docker.sock", nil) if err != nil { log.Debugln(err) log.Fatalln("Error creating the docker client.") os.Exit(1) } // // creates a wrapper Docker client that uses an ambassador // // container to create a pod-like environment. controller, err := docker.NewClient(client) if err != nil { log.Debugln(err) log.Fatalln("Error creating the docker ambassador.") os.Exit(1) } defer controller.Destroy() // watch for sigkill (timeout or cancel build) killc := make(chan os.Signal, 1) signal.Notify(killc, syscall.SIGINT, syscall.SIGTERM) go func() { <-killc log.Println("Cancel request received, killing process") controller.Destroy() // possibe race here. implement lock on the other end os.Exit(130) // cancel is treated like ctrl+c }() go func() { var timeout = payload.Repo.Timeout if timeout == 0 { timeout = 60 } <-time.After(time.Duration(timeout) * time.Minute) log.Println("Timeout request received, killing process") controller.Destroy() // possibe race here. implement lock on the other end os.Exit(128) // cancel is treated like ctrl+c }() state := &runner.State{ Client: controller, Stdout: os.Stdout, Stderr: os.Stdout, Repo: payload.Repo, Build: payload.Build, BuildLast: payload.BuildLast, Job: payload.Job, System: payload.System, Workspace: payload.Workspace, } if cache { log.Debugln("Running Cache step") err = r.RunNode(state, parser.NodeCache) if err != nil { log.Debugln(err) } } if clone { log.Debugln("Running Clone step") err = r.RunNode(state, parser.NodeClone) if err != nil { log.Debugln(err) } } if build && !state.Failed() { log.Debugln("Running Build and Compose steps") err = r.RunNode(state, parser.NodeCompose|parser.NodeBuild) if err != nil { log.Debugln(err) } } if deploy && !state.Failed() { log.Debugln("Running Publish and Deploy steps") err = r.RunNode(state, parser.NodePublish|parser.NodeDeploy) if err != nil { log.Debugln(err) } } // if the build is not failed, at this point // we can mark as successful if !state.Failed() { state.Job.Status = plugin.StateSuccess state.Build.Status = plugin.StateSuccess } if cache { log.Debugln("Running post-Build Cache steps") err = r.RunNode(state, parser.NodeCache) if err != nil { log.Debugln(err) } } if notify { log.Debugln("Running Notify steps") err = r.RunNode(state, parser.NodeNotify) if err != nil { log.Debugln(err) } } if state.Failed() { controller.Destroy() os.Exit(state.ExitCode()) } }