diff --git a/Makefile b/Makefile index 977bc46e..bbfb5541 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ test: go test -v github.com/drone/drone/pkg/database/testing go test -v github.com/drone/drone/pkg/mail go test -v github.com/drone/drone/pkg/model + go test -v github.com/drone/drone/pkg/plugin/deploy go test -v github.com/drone/drone/pkg/queue install: diff --git a/pkg/handler/repos.go b/pkg/handler/repos.go index fe3336c0..8d728814 100644 --- a/pkg/handler/repos.go +++ b/pkg/handler/repos.go @@ -125,7 +125,7 @@ func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error { // create the github key, or update if one already exists _, err := client.RepoKeys.CreateUpdate(owner, name, repo.PublicKey, keyName) if err != nil { - return fmt.Errorf("Unable to add Private Key to your GitHub repository") + return fmt.Errorf("Unable to add Public Key to your GitHub repository") } } else { diff --git a/pkg/plugin/deploy/deployment.go b/pkg/plugin/deploy/deployment.go index 0181586c..e45b3a94 100644 --- a/pkg/plugin/deploy/deployment.go +++ b/pkg/plugin/deploy/deployment.go @@ -16,6 +16,7 @@ type Deploy struct { Heroku *Heroku `yaml:"heroku,omitempty"` Nodejitsu *Nodejitsu `yaml:"nodejitsu,omitempty"` Openshift *Openshift `yaml:"openshift,omitempty"` + SSH *SSH `yaml:"ssh,omitempty"` } func (d *Deploy) Write(f *buildfile.Buildfile) { @@ -43,4 +44,7 @@ func (d *Deploy) Write(f *buildfile.Buildfile) { if d.Openshift != nil { d.Openshift.Write(f) } + if d.SSH != nil { + d.SSH.Write(f) + } } diff --git a/pkg/plugin/deploy/ssh.go b/pkg/plugin/deploy/ssh.go index b2b65d9c..51b1da71 100644 --- a/pkg/plugin/deploy/ssh.go +++ b/pkg/plugin/deploy/ssh.go @@ -1 +1,96 @@ package deploy + +import ( + "fmt" + "strconv" + "strings" + + "github.com/drone/drone/pkg/build/buildfile" +) + +// SSH struct holds configuration data for deployment +// via ssh, deployment done by scp-ing file(s) listed +// in artifacts to the target host, and then run cmd +// remotely. +// It is assumed that the target host already +// add this repo public key in the host's `authorized_hosts` +// file. And the private key is already copied to `.ssh/id_rsa` +// inside the build container. No further check will be done. +type SSH struct { + + // Target is the deployment host in this format + // user@hostname:/full/path + // + // PORT may be omitted if its default to port 22. + Target string `yaml:"target,omitempty"` + + // Artifacts is a list of files/dirs to be deployed + // to the target host. If artifacts list more than one file + // it will be compressed into a single tar.gz file. + // if artifacts contain: + // - GITARCHIVE + // + // other file listed in artifacts will be ignored, instead, we will + // create git archive from the current revision and deploy that file + // alone. + // If you need to deploy the git archive along with some other files, + // please use build script to create the git archive, and then list + // the archive name here with the other files. + Artifacts []string `yaml:"artifacts,omitempty"` + + // Cmd is a single command executed at target host after the artifacts + // is deployed. + Cmd string `yaml:"cmd,omitempty"` +} + +// Write down the buildfile +func (s *SSH) Write(f *buildfile.Buildfile) { + host := strings.SplitN(s.Target, " ", 2) + if len(host) == 1 { + host = append(host, "22") + } + if _, err := strconv.Atoi(host[1]); err != nil { + host[1] = "22" + } + + // Is artifact created? + artifact := false + + for _, a := range s.Artifacts { + if a == "GITARCHIVE" { + artifact = createGitArchive(f) + break + } + } + + if len(s.Artifacts) > 1 && !artifact { + artifact = compress(f, s.Artifacts) + } else if len(s.Artifacts) == 1 { + f.WriteCmdSilent(fmt.Sprintf("ARTIFACT=%s", s.Artifacts[0])) + artifact = true + } + + if artifact { + scpCmd := "scp -o StrictHostKeyChecking=no -P %s ${ARTIFACT} %s" + f.WriteCmd(fmt.Sprintf(scpCmd, host[1], host[0])) + } + + if len(s.Cmd) > 0 { + sshCmd := "ssh -o StrictHostKeyChecking=no -p %s %s %s" + f.WriteCmd(fmt.Sprintf(sshCmd, host[1], strings.SplitN(host[0], ":", 2)[0], s.Cmd)) + } +} + +func createGitArchive(f *buildfile.Buildfile) bool { + f.WriteCmdSilent("COMMIT=$(git rev-parse HEAD)") + f.WriteCmdSilent("ARTIFACT=${PWD##*/}-${COMMIT}.tar.gz") + f.WriteCmdSilent("git archive --format=tar.gz --prefix=${PWD##*/}/ ${COMMIT} > ${ARTIFACT}") + return true +} + +func compress(f *buildfile.Buildfile, files []string) bool { + cmd := "tar -cf ${ARTIFACT} %s" + f.WriteCmdSilent("ARTIFACT=${PWD##*/}.tar.gz") + f.WriteCmdSilent(fmt.Sprintf(cmd, strings.Join(files, " "))) + return true +} diff --git a/pkg/plugin/deploy/ssh_test.go b/pkg/plugin/deploy/ssh_test.go new file mode 100644 index 00000000..e3f4fc18 --- /dev/null +++ b/pkg/plugin/deploy/ssh_test.go @@ -0,0 +1,125 @@ +package deploy + +import ( + "strings" + "testing" + + "github.com/drone/drone/pkg/build/buildfile" + + "launchpad.net/goyaml" +) + +// emulate Build struct +type build struct { + Deploy *Deploy `yaml:"deploy,omitempty"` +} + +var sampleYml = ` +deploy: + ssh: + target: user@test.example.com + cmd: /opt/bin/redeploy.sh +` + +var sampleYml1 = ` +deploy: + ssh: + target: user@test.example.com:/srv/app/location 2212 + artifacts: + - build.result + cmd: /opt/bin/redeploy.sh +` + +var sampleYml2 = ` +deploy: + ssh: + target: user@test.example.com:/srv/app/location 2212 + artifacts: + - build.result + - config/file + cmd: /opt/bin/redeploy.sh +` + +var sampleYml3 = ` +deploy: + ssh: + target: user@test.example.com:/srv/app/location 2212 + artifacts: + - GITARCHIVE + cmd: /opt/bin/redeploy.sh +` + +func setUp(input string) (string, error) { + var buildStruct build + err := goyaml.Unmarshal([]byte(input), &buildStruct) + if err != nil { + return "", err + } + bf := buildfile.New() + buildStruct.Deploy.Write(bf) + return bf.String(), err +} + +func TestSSHNoArtifact(t *testing.T) { + bscr, err := setUp(sampleYml) + if err != nil { + t.Fatalf("Can't unmarshal deploy script: %s", err) + } + + if strings.Contains(bscr, `scp`) { + t.Error("Expect script not to contains scp command") + } + + if !strings.Contains(bscr, "ssh -o StrictHostKeyChecking=no -p 22 user@test.example.com /opt/bin/redeploy.sh") { + t.Error("Expect script to contains ssh command") + } +} + +func TestSSHOneArtifact(t *testing.T) { + bscr, err := setUp(sampleYml1) + if err != nil { + t.Fatalf("Can't unmarshal deploy script: %s", err) + } + + if !strings.Contains(bscr, "ARTIFACT=build.result") { + t.Errorf("Expect script to contains artifact") + } + + if !strings.Contains(bscr, "scp -o StrictHostKeyChecking=no -P 2212 ${ARTIFACT} user@test.example.com:/srv/app/location") { + t.Errorf("Expect script to contains scp command, got:\n%s", bscr) + } +} + +func TestSSHMultiArtifact(t *testing.T) { + bscr, err := setUp(sampleYml2) + if err != nil { + t.Fatalf("Can't unmarshal deploy script: %s", err) + } + + if !strings.Contains(bscr, "ARTIFACT=${PWD##*/}.tar.gz") { + t.Errorf("Expect script to contains artifact") + } + + if !strings.Contains(bscr, "tar -cf ${ARTIFACT} build.result config/file") { + t.Errorf("Expect script to contains tar command. got:\n", bscr) + } +} + +func TestSSHGitArchive(t *testing.T) { + bscr, err := setUp(sampleYml3) + if err != nil { + t.Fatalf("Can't unmarshal deploy script: %s", err) + } + + if !strings.Contains(bscr, "COMMIT=$(git rev-parse HEAD)") { + t.Errorf("Expect script to contains commit ref") + } + + if !strings.Contains(bscr, "ARTIFACT=${PWD##*/}-${COMMIT}.tar.gz") { + t.Errorf("Expect script to contains artifact") + } + + if !strings.Contains(bscr, "git archive --format=tar.gz --prefix=${PWD##*/}/ ${COMMIT} > ${ARTIFACT}") { + t.Errorf("Expect script to run git archive") + } +}