├── .gitignore ├── LICENSE ├── README.md └── s3update.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Heetch 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # s3update 2 | 3 | __Enable your Golang applications to self update with S3. Requires Go 1.8+__ 4 | 5 | This package enables our internal tools to be updated when new commits to their master branch are pushed to Github. 6 | 7 | Latest binaries are hosted on S3 under a specific bucket along its current version. When ran locally, the binary will 8 | fetch the version and if its local version is older than the remote one, the new binary will get fetched and will exit, 9 | stating to the user that it got updated and need to be ran again. 10 | 11 | In our case, we're only shipping Linux and Darwin, targeting amd64 platform. 12 | 13 | Bucket will have the following structure: 14 | 15 | ``` 16 | mybucket/ 17 | mytool/ 18 | VERSION 19 | mytool-linux-amd64 20 | mytool-darwin-amd64 21 | ``` 22 | 23 | ## Usage 24 | 25 | Updates are easier to deal with when done through a continuous integration platform. We're using CircleCI but the following 26 | excerpt can easily be adapted to whichever solution being used. 27 | 28 | ### CircleCI 29 | 30 | [xgo](https://github.com/karalabe/xgo) is being used to easily cross-compile code. 31 | 32 | Adding the following at the end of the build script will push the binaries and its version to S3. 33 | 34 | ```sh 35 | xgo --targets="linux/amd64,darwin/amd64" -ldflags="-X main.Version=$CIRCLE_BUILD_NUM" . 36 | 37 | if [[ "$CIRCLE_BRANCH" = "master" ]]; then 38 | aws s3 cp mytool-darwin-10.6-amd64 s3://mybucket/mytool/mytool-darwin-amd64 --acl authenticated-read 39 | aws s3 cp mytool-linux-amd64 s3://mybucket/mytool/mytool-linux-amd64 --acl authenticated-read 40 | echo -n $CIRCLE_BUILD_NUM > VERSION && aws s3 cp VERSION s3://mybucket/mytool/VERSION --acl authenticated-read 41 | fi 42 | ``` 43 | 44 | ### Example 45 | 46 | ```go 47 | package main 48 | 49 | import ( 50 | "github.com/heetch/s3update" 51 | ) 52 | 53 | var ( 54 | // This gets set during the compilation. See below. 55 | Version = "" 56 | ) 57 | 58 | func main() { 59 | err := s3update.AutoUpdate(s3update.Updater{ 60 | CurrentVersion: Version, 61 | S3Bucket: "mybucket", 62 | S3Region: "eu-west-1", 63 | S3ReleaseKey: "mytool/mytool-{{OS}}-{{ARCH}}", 64 | S3VersionKey: "mytool/VERSION", 65 | }) 66 | 67 | if err != nil { 68 | // ... 69 | } 70 | 71 | ... 72 | } 73 | ``` 74 | 75 | Binary must be compiled with a flag specifying the new version: 76 | 77 | ```sh 78 | go build -ldflags "-X main.Version=111" main.go 79 | ``` 80 | 81 | ## Contributions 82 | 83 | Any contribution is welcomed! 84 | 85 | - Open an issue if you want to discuss bugs/features 86 | - Open Pull Requests if you want to contribute to the code 87 | 88 | ## Copyright 89 | 90 | Copyright © 2016 Heetch 91 | 92 | See the [LICENSE](https://github.com/heetch/s3update/blob/master/LICENSE) (MIT) file for more details. 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /s3update.go: -------------------------------------------------------------------------------- 1 | package s3update 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/aws/aws-sdk-go/aws" 15 | "github.com/aws/aws-sdk-go/aws/credentials" 16 | "github.com/aws/aws-sdk-go/aws/session" 17 | "github.com/aws/aws-sdk-go/service/s3" 18 | "github.com/mitchellh/ioprogress" 19 | ) 20 | 21 | type Updater struct { 22 | // CurrentVersion represents the current binary version. 23 | // This is generally set at the compilation time with -ldflags "-X main.Version=42" 24 | // See the README for additional information 25 | CurrentVersion string 26 | // S3Bucket represents the S3 bucket containing the different files used by s3update. 27 | S3Bucket string 28 | // S3Region represents the S3 region you want to work in. 29 | S3Region string 30 | // S3ReleaseKey represents the raw key on S3 to download new versions. 31 | // The value can be something like `cli/releases/cli-{{OS}}-{{ARCH}}` 32 | S3ReleaseKey string 33 | // S3VersionKey represents the key on S3 to download the current version 34 | S3VersionKey string 35 | // AWSCredentials represents the config to use to connect to s3 36 | AWSCredentials *credentials.Credentials 37 | } 38 | 39 | // validate ensures every required fields is correctly set. Otherwise and error is returned. 40 | func (u Updater) validate() error { 41 | if u.CurrentVersion == "" { 42 | return fmt.Errorf("no version set") 43 | } 44 | 45 | if u.S3Bucket == "" { 46 | return fmt.Errorf("no bucket set") 47 | } 48 | 49 | if u.S3Region == "" { 50 | return fmt.Errorf("no s3 region") 51 | } 52 | 53 | if u.S3ReleaseKey == "" { 54 | return fmt.Errorf("no s3ReleaseKey set") 55 | } 56 | 57 | if u.S3VersionKey == "" { 58 | return fmt.Errorf("no s3VersionKey set") 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // AutoUpdate runs synchronously a verification to ensure the binary is up-to-date. 65 | // If a new version gets released, the download will happen automatically 66 | // It's possible to bypass this mechanism by setting the S3UPDATE_DISABLED environment variable. 67 | func AutoUpdate(u Updater) error { 68 | if os.Getenv("S3UPDATE_DISABLED") != "" { 69 | fmt.Println("s3update: autoupdate disabled") 70 | return nil 71 | } 72 | 73 | if err := u.validate(); err != nil { 74 | fmt.Printf("s3update: %s - skipping auto update\n", err.Error()) 75 | return err 76 | } 77 | 78 | return runAutoUpdate(u) 79 | } 80 | 81 | // generateS3ReleaseKey dynamically builds the S3 key depending on the os and architecture. 82 | func generateS3ReleaseKey(path string) string { 83 | path = strings.Replace(path, "{{OS}}", runtime.GOOS, -1) 84 | path = strings.Replace(path, "{{ARCH}}", runtime.GOARCH, -1) 85 | 86 | return path 87 | } 88 | 89 | func runAutoUpdate(u Updater) error { 90 | localVersion, err := strconv.ParseInt(u.CurrentVersion, 10, 64) 91 | if err != nil || localVersion == 0 { 92 | return fmt.Errorf("invalid local version") 93 | } 94 | 95 | svc := s3.New(session.New(), &aws.Config{ 96 | Region: aws.String(u.S3Region), 97 | Credentials: u.AWSCredentials, 98 | }) 99 | 100 | resp, err := svc.GetObject(&s3.GetObjectInput{Bucket: aws.String(u.S3Bucket), Key: aws.String(u.S3VersionKey)}) 101 | if err != nil { 102 | return err 103 | } 104 | defer resp.Body.Close() 105 | 106 | b, err := ioutil.ReadAll(resp.Body) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | remoteVersion, err := strconv.ParseInt(string(b), 10, 64) 112 | if err != nil || remoteVersion == 0 { 113 | return fmt.Errorf("invalid remote version") 114 | } 115 | 116 | fmt.Printf("s3update: Local Version %d - Remote Version: %d\n", localVersion, remoteVersion) 117 | if localVersion < remoteVersion { 118 | fmt.Printf("s3update: version outdated ... \n") 119 | s3Key := generateS3ReleaseKey(u.S3ReleaseKey) 120 | resp, err := svc.GetObject(&s3.GetObjectInput{Bucket: aws.String(u.S3Bucket), Key: aws.String(s3Key)}) 121 | if err != nil { 122 | return err 123 | } 124 | defer resp.Body.Close() 125 | progressR := &ioprogress.Reader{ 126 | Reader: resp.Body, 127 | Size: *resp.ContentLength, 128 | DrawInterval: 500 * time.Millisecond, 129 | DrawFunc: ioprogress.DrawTerminalf(os.Stdout, func(progress, total int64) string { 130 | bar := ioprogress.DrawTextFormatBar(40) 131 | return fmt.Sprintf("%s %20s", bar(progress, total), ioprogress.DrawTextFormatBytes(progress, total)) 132 | }), 133 | } 134 | 135 | dest, err := os.Executable() 136 | if err != nil { 137 | return err 138 | } 139 | 140 | // Move the old version to a backup path that we can recover from 141 | // in case the upgrade fails 142 | destBackup := dest + ".bak" 143 | if _, err := os.Stat(dest); err == nil { 144 | os.Rename(dest, destBackup) 145 | } 146 | 147 | // Use the same flags that ioutil.WriteFile uses 148 | f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) 149 | if err != nil { 150 | os.Rename(destBackup, dest) 151 | return err 152 | } 153 | defer f.Close() 154 | 155 | fmt.Printf("s3update: downloading new version to %s\n", dest) 156 | if _, err := io.Copy(f, progressR); err != nil { 157 | os.Rename(destBackup, dest) 158 | return err 159 | } 160 | // The file must be closed already so we can execute it in the next step 161 | f.Close() 162 | 163 | // Removing backup 164 | os.Remove(destBackup) 165 | 166 | fmt.Printf("s3update: updated with success to version %d\nRestarting application\n", remoteVersion) 167 | 168 | // The update completed, we can now restart the application without requiring any user action. 169 | if err := syscall.Exec(dest, os.Args, os.Environ()); err != nil { 170 | return err 171 | } 172 | 173 | os.Exit(0) 174 | } 175 | 176 | return nil 177 | } 178 | --------------------------------------------------------------------------------