├── .gitignore ├── LICENSE ├── README.md ├── gopistrano.go └── shell.go /.gitignore: -------------------------------------------------------------------------------- 1 | Gopfile 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alan Chavez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ======= 2 | gopistrano 3 | ========== 4 | 5 | Automatic Deployment Tool in Golang 6 | 7 | ## Requirements 8 | 9 | * GoLang >= 1.3.3 10 | 11 | ## Installation 12 | 13 | Run this if you want to install gopistrano binary 14 | 15 | ``` go 16 | go install github.com/alanchavez88/gopistrano 17 | ``` 18 | 19 | That will compile and install gopistrano in your $GOPATH 20 | 21 | To deploy a project, you need to create a Gopfile. A Gopfile is just a configuration file in plain-text that will contain the credentials to SSH into your server, and the path of the directory where you want to deploy your project to. 22 | 23 | This is a sample Gopfile 24 | ``` 25 | username = yourusername 26 | password = yourpassword 27 | # private_key = /home/user/.ssh/id_rsa 28 | hostname = example.com 29 | port = 22 30 | repository = https://github.com/alanchavez88/theHarvester.git 31 | keep_releases = 5 32 | path = /home7/alanchav/gopistrano 33 | use_sudo = false 34 | webserver_user = nobody 35 | 36 | ``` 37 | The file above will clone the git repository above into the path specified in the Gopfile. 38 | 39 | Currently gopistrano only supports git, other version controls will be added in the future. 40 | 41 | It also only supports username and password authentication, the next update will provide authenticate via PEM files and SSH Keys 42 | 43 | To deploy you have to run 44 | 45 | ``` sh 46 | gopistrano deploy:setup 47 | 48 | ``` 49 | 50 | and then: 51 | 52 | ``` sh 53 | gopistrano deploy 54 | ``` 55 | 56 | ## Support 57 | 58 | Need help with Gopistrano? Shoot me an email at alan@alanchavez.com 59 | -------------------------------------------------------------------------------- /gopistrano.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/alanchavez88/goconf" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | var ( 15 | user, 16 | pass, 17 | hostname, 18 | port, 19 | repository, 20 | path, 21 | releases, 22 | shared, 23 | utils, 24 | privateKey, 25 | keepReleases string 26 | ) 27 | 28 | type deploy struct { 29 | cl *ssh.Client 30 | } 31 | 32 | func init() { 33 | var err error 34 | c, err := conf.ReadConfigFile("Gopfile") 35 | 36 | if err != nil { 37 | fmt.Println(err.Error()) 38 | os.Exit(1) 39 | } 40 | 41 | user, err = c.GetString("", "username") 42 | pass, err = c.GetString("", "password") 43 | privateKey, err = c.GetString("", "private_key") 44 | hostname, err = c.GetString("", "hostname") 45 | repository, err = c.GetString("", "repository") 46 | port, err = c.GetString("", "port") 47 | path, err = c.GetString("", "path") 48 | releases = path + "/releases" 49 | shared = path + "/shared" 50 | utils = path + "/utils" 51 | 52 | keepReleases, err = c.GetString("", "keep_releases") 53 | 54 | // just log whichever we get; let the user re-run the program to see all errors.. 55 | if err != nil { 56 | fmt.Println(err.Error()) 57 | os.Exit(1) 58 | } 59 | } 60 | 61 | func main() { 62 | flag.Parse() 63 | 64 | action := flag.Arg(0) 65 | 66 | if action == "" { 67 | fmt.Println("Error: use gopistrano deploy or gopistrano deploy:setup") 68 | return 69 | } 70 | 71 | deploy, err := newDeploy() 72 | 73 | // Do panic if the dial fails 74 | if err != nil { 75 | fmt.Println("Failed to start: " + err.Error()) 76 | return 77 | } 78 | 79 | switch strings.ToLower(action) { 80 | case "deploy:setup": 81 | err = deploy.Setup() 82 | case "deploy": 83 | err = deploy.Run() 84 | default: 85 | fmt.Println("Invalid command!") 86 | } 87 | 88 | if err != nil { 89 | fmt.Println(err.Error()) 90 | } 91 | } 92 | 93 | // returns a new deployment 94 | func newDeploy() (d *deploy, err error) { 95 | if pass != "" && user != "" { 96 | cfg := &ssh.ClientConfig{ 97 | User: user, 98 | Auth: []ssh.AuthMethod{ 99 | ssh.Password(pass), 100 | }, 101 | } 102 | 103 | fmt.Println("SSH-ing into " + hostname) 104 | cl, err := ssh.Dial("tcp", hostname+":"+port, cfg) 105 | 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | d = &deploy{cl: cl} 111 | } else if privateKey != "" && user != "" { 112 | cfg := &ssh.ClientConfig{ 113 | User: user, 114 | Auth: []ssh.AuthMethod{ 115 | publicKeyFile(privateKey), 116 | }, 117 | } 118 | 119 | fmt.Println("SSH-ing into " + hostname + " with private key " + privateKey) 120 | cl, err := ssh.Dial("tcp", hostname+":"+port, cfg) 121 | 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | d = &deploy{cl: cl} 127 | } else { 128 | panic("Unable to authenticate with server") 129 | } 130 | return 131 | } 132 | 133 | func publicKeyFile(file string) ssh.AuthMethod { 134 | buffer, err := ioutil.ReadFile(file) 135 | 136 | if err != nil { 137 | return nil 138 | } 139 | 140 | key, err := ssh.ParsePrivateKey(buffer) 141 | 142 | if err != nil { 143 | return nil 144 | } 145 | 146 | return ssh.PublicKeys(key) 147 | } 148 | 149 | // runs the deployment script remotely 150 | func (d *deploy) Run() error { 151 | deployCmd := "if [ ! -d " + releases + " ]; then exit 1; fi &&" + 152 | "if [ ! -d " + shared + " ]; then exit 1; fi &&" + 153 | "if [ ! -d " + utils + " ]; then exit 1; fi &&" + 154 | "if [ ! -f " + utils + "/deploy.sh ]; then exit 1; fi &&" + 155 | "" + utils + "/deploy.sh " + path + " " + repository + " " + keepReleases 156 | 157 | if err := d.runCmd(deployCmd); err != nil { 158 | return err 159 | } 160 | 161 | fmt.Println("Project Deployed!") 162 | return nil 163 | } 164 | 165 | // sets up directories for deployment a la capistrano 166 | func (d *deploy) Setup() error { 167 | cdPathCmd := "if [ ! -d " + releases + " ]; then mkdir -p " + releases + "; fi &&" + 168 | "if [ ! -d " + shared + " ]; then mkdir -p " + shared + "; fi &&" + 169 | "if [ ! -d " + utils + " ]; then mkdir -p " + utils + "; fi &&" + 170 | "chmod g+w " + releases + " " + shared + " " + path + " " + utils 171 | 172 | if err := d.runCmd(cdPathCmd); err != nil { 173 | return err 174 | } 175 | 176 | fmt.Println("Running scp connection") 177 | 178 | cpy := `echo -n '` + string(deployment_script) + `' > ` + utils + `/deploy.sh ; chmod +x ` + utils + `/deploy.sh` 179 | 180 | if err := d.runCmd(cpy); err != nil { 181 | return err 182 | } 183 | 184 | fmt.Println("Cool Beans! Gopistrano created the structure correctly!") 185 | return nil 186 | } 187 | 188 | // basic ssh cmd runner 189 | func (d *deploy) runCmd(cmd string) (err error) { 190 | session, err := d.cl.NewSession() 191 | if err != nil { 192 | return err 193 | } 194 | 195 | //this *does* return an error (EOF of some sort), but I guess we don't care? 196 | //the ssh lib needs to send it and must return it or something 197 | defer session.Close() 198 | 199 | //send through to main stdout, stderr 200 | session.Stdout = os.Stdout 201 | session.Stderr = os.Stderr 202 | 203 | return session.Run(cmd) 204 | } 205 | -------------------------------------------------------------------------------- /shell.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var deployment_script string = `#!/bin/bash 4 | # comment line below if you want quiet output 5 | #set -x 6 | 7 | DEPLOYMENT_PATH=$1 8 | REPOSITORY=$2 9 | KEEP_RELEASES=$3 10 | # variable init 11 | CUR_TIMESTAMP="$(date +'%Y%m%d%H%M%S')" 12 | 13 | # update code base with remote_cache strategy 14 | if [ -d "$DEPLOYMENT_PATH/shared/cached-copy" ] 15 | then 16 | cd "$DEPLOYMENT_PATH/shared/cached-copy" 17 | git fetch -q origin 18 | git fetch --tags -q origin 19 | git rev-list --max-count=1 HEAD | xargs git reset -q --hard 20 | git clean -q -d -x -f; 21 | else 22 | git clone -q $REPOSITORY "$DEPLOYMENT_PATH/shared/cached-copy" 23 | cd "$DEPLOYMENT_PATH/shared/cached-copy" 24 | git rev-list --max-count=1 HEAD | xargs git checkout -q -b deploy 25 | fi 26 | cp -RPp "$DEPLOYMENT_PATH/shared/cached-copy" "$DEPLOYMENT_PATH/releases/$CUR_TIMESTAMP" 27 | git rev-list --max-count=1 HEAD > "$DEPLOYMENT_PATH/releases/$CUR_TIMESTAMP/REVISION" 28 | chmod -R g+w "$DEPLOYMENT_PATH/releases/$CUR_TIMESTAMP" 29 | 30 | rm -f "$DEPLOYMENT_PATH/current" && ln -s "$DEPLOYMENT_PATH/releases/$CUR_TIMESTAMP" "$DEPLOYMENT_PATH/current" 31 | ls -1dt "$DEPLOYMENT_PATH/releases" | tail -n +$KEEP_RELEASES | xargs rm -rf 32 | ` 33 | --------------------------------------------------------------------------------