github.com/jdhenke/godel@v0.0.0-20161213181855-abeb3861bf0d/cmd/godel/install.go (about) 1 // Copyright 2016 Palantir Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package godel 16 17 import ( 18 "crypto/sha256" 19 "encoding/hex" 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net/http" 24 "os" 25 "os/exec" 26 "path" 27 "strings" 28 29 "github.com/nmiyake/archiver" 30 "github.com/nmiyake/pkg/dirs" 31 "github.com/palantir/pkg/specdir" 32 "github.com/pkg/errors" 33 "gopkg.in/cheggaaa/pb.v1" 34 35 "github.com/palantir/godel/layout" 36 "github.com/palantir/godel/properties" 37 ) 38 39 // Copies and installs the gödel package from the provided PkgSrc. If the PkgSrc includes a checksum, this 40 // function will check to see if a TGZ file for the version as already been downloaded and if the checksum matches. If 41 // it does, that file will be used. Otherwise, the TGZ will be downloaded from the specified location and the downloaded 42 // TGZ will be verified against the checksum. If the checksum is empty, no verification will occur. If the install 43 // succeeds, the following files will be created: 44 // "{{layout.GödelHomePath()}}/downloads/{{layout.AppName}}-{{version}}.tgz" and 45 // "{{layout.GödelHomePath()}}/dists/{{layout.AppName}}-{{version}}". If the downloaded distribution matches a version 46 // that already exists in the distribution directory and a download occurs, the existing distribution will be 47 // overwritten by the newly downloaded one. Returns the version of the distribution that was installed. 48 func install(src PkgSrc, stdout io.Writer) (string, error) { 49 gödelHomeSpecDir, err := layout.GödelHomeSpecDir(specdir.Create) 50 if err != nil { 51 return "", errors.Wrapf(err, "failed to create SpecDir for gödel home") 52 } 53 gödelHome := gödelHomeSpecDir.Root() 54 55 downloadsDir := gödelHomeSpecDir.Path(layout.DownloadsDir) 56 tgzFilePath, err := getPkg(src, downloadsDir, stdout) 57 if err != nil { 58 return "", err 59 } 60 61 tgzVersion, err := verifyPackageTgz(tgzFilePath) 62 if err != nil { 63 return "", errors.Wrapf(err, "downloaded file %s is not a valid %s package", tgzFilePath, layout.AppName) 64 } 65 66 // create temporary directory in gödel home in which downloaded tgz is expanded. If verification is successful, 67 // the expanded directory will be moved to the destination. 68 tmpDir, cleanup, err := dirs.TempDir(gödelHome, "") 69 defer cleanup() 70 if err != nil { 71 return "", errors.Wrapf(err, "failed to create temporary directory rooted at %s", gödelHome) 72 } 73 74 if err := archiver.UntarGz(tgzFilePath, tmpDir); err != nil { 75 return "", errors.Wrapf(err, "failed to extract archive %s to %s", tgzFilePath, tmpDir) 76 } 77 78 expandedGödelDir := path.Join(tmpDir, layout.AppName+"-"+tgzVersion) 79 expandedGödelApp, err := layout.AppSpecDir(expandedGödelDir, tgzVersion) 80 if err != nil { 81 return "", errors.Wrapf(err, "extracted archive layout did not match expected gödel layout") 82 } 83 84 version, err := getExecutableVersion(expandedGödelApp) 85 if err != nil { 86 return "", errors.Wrapf(err, "failed to get version of downloaded gödel package") 87 } 88 89 if version != tgzVersion { 90 return "", errors.Errorf("version reported by executable does not match version specified by tgz: expected %s, was %s", tgzVersion, version) 91 } 92 93 gödelDist, err := layout.GödelDistLayout(version, specdir.Create) 94 if err != nil { 95 return "", errors.Wrapf(err, "failed to create distribution directory") 96 } 97 gödelDirDestPath := gödelDist.Path(layout.AppDir) 98 99 // delete destination directory if it already exists 100 if _, err := os.Stat(gödelDirDestPath); !os.IsNotExist(err) { 101 if err != nil { 102 return "", errors.Wrapf(err, "failed to stat %s", gödelDirDestPath) 103 } 104 105 if err := os.RemoveAll(gödelDirDestPath); err != nil { 106 return "", errors.Wrapf(err, "failed to remove %s", gödelDirDestPath) 107 } 108 } 109 110 if err := os.Rename(expandedGödelDir, gödelDirDestPath); err != nil { 111 return "", errors.Wrapf(err, "failed to rename %s to %s", expandedGödelDir, gödelDirDestPath) 112 } 113 114 return version, nil 115 } 116 117 // GetDistPkgInfo returns the distribution URL and checksum (if it exists) from the configuration file in the provided 118 // directory. Returns an error if the URL cannot be read. 119 func GetDistPkgInfo(configDir string) (PkgWithChecksum, error) { 120 propsFilePath := path.Join(configDir, fmt.Sprintf("%v.properties", layout.AppName)) 121 props, err := properties.Read(propsFilePath) 122 if err != nil { 123 return PkgWithChecksum{}, errors.Wrapf(err, "failed to read properties file %s", propsFilePath) 124 } 125 url, err := properties.Get(props, properties.URL) 126 if err != nil { 127 return PkgWithChecksum{}, errors.Wrapf(err, "failed to get URL") 128 } 129 checksum, _ := properties.Get(props, properties.Checksum) 130 return PkgWithChecksum{ 131 Pkg: url, 132 Checksum: checksum, 133 }, nil 134 } 135 136 // getExecutableVersion gets the version of gödel contained in the provided root gödel directory. Invokes the executable 137 // for the current platform with the "--version" flag and returns the version determined by that output. 138 func getExecutableVersion(gödelApp specdir.SpecDir) (string, error) { 139 executablePath := gödelApp.Path(layout.AppExecutable) 140 cmd := exec.Command(executablePath, "version") 141 output, err := cmd.Output() 142 if err != nil { 143 return "", errors.Wrapf(err, "failed to execute command %v: %s", cmd.Args, string(output)) 144 } 145 146 outputString := strings.TrimSpace(string(output)) 147 parts := strings.Split(outputString, " ") 148 if len(parts) != 3 { 149 return "", errors.Errorf(`expected output %s to have 3 parts when split by " ", but was %v`, outputString, parts) 150 } 151 152 return parts[2], nil 153 } 154 155 // getPkg gets the source package from the specified source and copies it to a new file in the specified directory 156 // (which must already exist). Returns the path to the downloaded file. 157 func getPkg(src PkgSrc, destDir string, stdout io.Writer) (rPkg string, rErr error) { 158 expectedChecksum := src.checksum() 159 160 if destDirInfo, err := os.Stat(destDir); err != nil { 161 if os.IsNotExist(err) { 162 return "", errors.Wrapf(err, "destination directory %s does not exist", destDir) 163 } 164 return "", errors.WithStack(err) 165 } else if !destDirInfo.IsDir() { 166 return "", errors.Errorf("destination path %s exists, but is not a directory", destDir) 167 } 168 169 destFilePath := path.Join(destDir, src.name()) 170 if info, err := os.Stat(destFilePath); err == nil { 171 if info.IsDir() { 172 return "", errors.Errorf("destination path %s already exists and is a directory", destFilePath) 173 } 174 if expectedChecksum != "" { 175 // if tgz already exists at destination and checksum is known, verify checksum of existing tgz. 176 // If it matches, use existing file. 177 checksum, err := sha256Checksum(destFilePath) 178 if err != nil { 179 // if checksum computation fails, print error but continue execution 180 fmt.Fprintf(stdout, "Failed to compute checksum of %s: %v\n", destFilePath, err) 181 } else if checksum == expectedChecksum { 182 return destFilePath, nil 183 } 184 } 185 } 186 187 // create new file for package (overwrite any existing file) 188 destFile, err := os.Create(destFilePath) 189 if err != nil { 190 return "", errors.Wrapf(err, "failed to create file %s", destFilePath) 191 } 192 defer func() { 193 if err := destFile.Close(); err != nil && rErr == nil { 194 rErr = errors.Wrapf(err, "failed to close file %s in defer", destFilePath) 195 } 196 }() 197 198 r, size, err := src.getPkg() 199 if err != nil { 200 return "", err 201 } 202 defer func() { 203 if err := r.Close(); err != nil && rErr == nil { 204 rErr = errors.Wrapf(err, "failed to close reader for %s in defer", src.path()) 205 } 206 }() 207 208 h := sha256.New() 209 w := io.MultiWriter(h, destFile) 210 211 fmt.Fprintf(stdout, "Getting package from %v...\n", src.path()) 212 if err := copyWithProgress(w, r, size, stdout); err != nil { 213 return "", errors.Wrapf(err, "failed to copy package %s to %s", src.path(), destFilePath) 214 } 215 216 // verify checksum if provided 217 if expectedChecksum != "" { 218 actualChecksum := hex.EncodeToString(h.Sum(nil)) 219 if expectedChecksum != actualChecksum { 220 return "", errors.Errorf("SHA-256 checksum of downloaded package did not match expected checksum: expected %s, was %s", expectedChecksum, actualChecksum) 221 } 222 } 223 224 return destFilePath, nil 225 } 226 227 func copyWithProgress(w io.Writer, r io.Reader, dataLen int64, stdout io.Writer) error { 228 bar := pb.New64(dataLen).SetUnits(pb.U_BYTES) 229 bar.SetMaxWidth(120) 230 bar.Output = stdout 231 bar.Start() 232 defer func() { 233 bar.Finish() 234 }() 235 mw := io.MultiWriter(w, bar) 236 _, err := io.Copy(mw, r) 237 return err 238 } 239 240 type PkgSrc interface { 241 // returns a reader that can be used to read the package and the size of the package. Reader will be open and 242 // ready for reads -- the caller is responsible for closing the reader when done. 243 getPkg() (io.ReadCloser, int64, error) 244 // returns the name of this package. 245 name() string 246 // returns the path to this package. 247 path() string 248 // returns the expected SHA-256 checksum for the package. If this function returns an empty string, then a 249 // checksum will not be performed. 250 checksum() string 251 } 252 253 type PkgWithChecksum struct { 254 Pkg string 255 Checksum string 256 } 257 258 func (p PkgWithChecksum) ToPkgSrc() PkgSrc { 259 if strings.HasPrefix(p.Pkg, "http://") || strings.HasPrefix(p.Pkg, "https://") { 260 return remotePkg(p) 261 } 262 return localPkg(p) 263 } 264 265 type remotePkg PkgWithChecksum 266 267 func (p remotePkg) getPkg() (io.ReadCloser, int64, error) { 268 url := p.Pkg 269 response, err := http.Get(url) 270 if err != nil { 271 return nil, 0, errors.Wrapf(err, "get call for url %s failed", url) 272 } 273 if response.StatusCode >= 400 { 274 return nil, 0, errors.Errorf("request for URL %s returned status code %d", url, response.StatusCode) 275 } 276 return response.Body, response.ContentLength, nil 277 } 278 279 func (p remotePkg) name() string { 280 return p.Pkg[strings.LastIndex(p.Pkg, "/")+1:] 281 } 282 283 func (p remotePkg) path() string { 284 return p.Pkg 285 } 286 287 func (p remotePkg) checksum() string { 288 return p.Checksum 289 } 290 291 type localPkg PkgWithChecksum 292 293 func (p localPkg) getPkg() (io.ReadCloser, int64, error) { 294 pathToLocalTgz := p.Pkg 295 localTgzFileInfo, err := os.Stat(pathToLocalTgz) 296 if err != nil { 297 if os.IsNotExist(err) { 298 return nil, 0, errors.Errorf("%s does not exist", pathToLocalTgz) 299 } 300 return nil, 0, errors.WithStack(err) 301 } else if localTgzFileInfo.IsDir() { 302 return nil, 0, errors.Errorf("%s is a directory", pathToLocalTgz) 303 } 304 srcTgzFile, err := os.Open(pathToLocalTgz) 305 if err != nil { 306 return nil, 0, errors.Wrapf(err, "failed to open %s", pathToLocalTgz) 307 } 308 return srcTgzFile, localTgzFileInfo.Size(), nil 309 } 310 311 func (p localPkg) name() string { 312 return path.Base(p.Pkg) 313 } 314 315 func (p localPkg) path() string { 316 return p.Pkg 317 } 318 319 func (p localPkg) checksum() string { 320 return p.Checksum 321 } 322 323 func sha256Checksum(filename string) (string, error) { 324 bytes, err := ioutil.ReadFile(filename) 325 if err != nil { 326 return "", errors.Wrapf(err, "failed to read file %s", filename) 327 } 328 sha256Checksum := sha256.Sum256(bytes) 329 return hex.EncodeToString(sha256Checksum[:]), nil 330 }