github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/environs/tools/build.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package tools 5 6 import ( 7 "archive/tar" 8 "bytes" 9 "compress/gzip" 10 "crypto/sha256" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "strings" 18 19 "github.com/juju/errors" 20 "github.com/juju/os/series" 21 "github.com/juju/utils/arch" 22 "github.com/juju/version" 23 24 "github.com/juju/juju/juju/names" 25 jujuversion "github.com/juju/juju/version" 26 ) 27 28 // Archive writes the executable files found in the given directory in 29 // gzipped tar format to w. 30 func Archive(w io.Writer, dir string) error { 31 entries, err := ioutil.ReadDir(dir) 32 if err != nil { 33 return err 34 } 35 36 gzw := gzip.NewWriter(w) 37 defer closeErrorCheck(&err, gzw) 38 39 tarw := tar.NewWriter(gzw) 40 defer closeErrorCheck(&err, tarw) 41 42 for _, ent := range entries { 43 h := tarHeader(ent) 44 logger.Debugf("adding entry: %#v", h) 45 // ignore local umask 46 if isExecutable(ent) { 47 h.Mode = 0755 48 } else { 49 h.Mode = 0644 50 } 51 err := tarw.WriteHeader(h) 52 if err != nil { 53 return err 54 } 55 fileName := filepath.Join(dir, ent.Name()) 56 if err := copyFile(tarw, fileName); err != nil { 57 return err 58 } 59 } 60 return nil 61 } 62 63 // archiveAndSHA256 calls Archive with the provided arguments, 64 // and returns a hex-encoded SHA256 hash of the resulting 65 // archive. 66 func archiveAndSHA256(w io.Writer, dir string) (sha256hash string, err error) { 67 h := sha256.New() 68 if err := Archive(io.MultiWriter(h, w), dir); err != nil { 69 return "", err 70 } 71 return fmt.Sprintf("%x", h.Sum(nil)), err 72 } 73 74 // copyFile writes the contents of the given file to w. 75 func copyFile(w io.Writer, file string) error { 76 f, err := os.Open(file) 77 if err != nil { 78 return err 79 } 80 defer f.Close() 81 _, err = io.Copy(w, f) 82 return err 83 } 84 85 // tarHeader returns a tar file header given the file's stat 86 // information. 87 func tarHeader(i os.FileInfo) *tar.Header { 88 return &tar.Header{ 89 Typeflag: tar.TypeReg, 90 Name: i.Name(), 91 Size: i.Size(), 92 Mode: int64(i.Mode() & 0777), 93 ModTime: i.ModTime(), 94 AccessTime: i.ModTime(), 95 ChangeTime: i.ModTime(), 96 Uname: "ubuntu", 97 Gname: "ubuntu", 98 } 99 } 100 101 // isExecutable returns whether the given info 102 // represents a regular file executable by (at least) the user. 103 func isExecutable(i os.FileInfo) bool { 104 return i.Mode()&(0100|os.ModeType) == 0100 105 } 106 107 // closeErrorCheck means that we can ensure that 108 // Close errors do not get lost even when we defer them, 109 func closeErrorCheck(errp *error, c io.Closer) { 110 err := c.Close() 111 if *errp == nil { 112 *errp = err 113 } 114 } 115 116 func findExecutable(execFile string) (string, error) { 117 logger.Debugf("looking for: %s", execFile) 118 if filepath.IsAbs(execFile) { 119 return execFile, nil 120 } 121 122 dir, file := filepath.Split(execFile) 123 124 // Now we have two possibilities: 125 // file == path indicating that the PATH was searched 126 // dir != "" indicating that it is a relative path 127 128 if dir == "" { 129 path := os.Getenv("PATH") 130 for _, name := range filepath.SplitList(path) { 131 result := filepath.Join(name, file) 132 // Use exec.LookPath() to check if the file exists and is executable` 133 f, err := exec.LookPath(result) 134 if err == nil { 135 return f, nil 136 } 137 } 138 139 return "", fmt.Errorf("could not find %q in the path", file) 140 } 141 cwd, err := os.Getwd() 142 if err != nil { 143 return "", err 144 } 145 return filepath.Clean(filepath.Join(cwd, execFile)), nil 146 } 147 148 func copyFileWithMode(from, to string, mode os.FileMode) error { 149 source, err := os.Open(from) 150 if err != nil { 151 logger.Infof("open source failed: %v", err) 152 return err 153 } 154 defer source.Close() 155 destination, err := os.OpenFile(to, os.O_RDWR|os.O_TRUNC|os.O_CREATE, mode) 156 if err != nil { 157 logger.Infof("open destination failed: %v", err) 158 return err 159 } 160 defer destination.Close() 161 _, err = io.Copy(destination, source) 162 if err != nil { 163 return err 164 } 165 return nil 166 } 167 168 // ExistingJujudLocation returns the directory to 169 // a jujud executable in the path. 170 func ExistingJujudLocation() (string, error) { 171 jujuLocation, err := findExecutable(os.Args[0]) 172 if err != nil { 173 logger.Infof("%v", err) 174 return "", err 175 } 176 jujuDir := filepath.Dir(jujuLocation) 177 return jujuDir, nil 178 } 179 180 // VersionFileFallbackDir is the other location we'll check for a 181 // juju-versions file if it's not alongside the binary (for example if 182 // Juju was installed from a .deb). (Exposed so we can override it in 183 // tests.) 184 var VersionFileFallbackDir = "/usr/lib/juju" 185 186 func copyExistingJujud(dir string) error { 187 // Assume that the user is running juju. 188 jujuDir, err := ExistingJujudLocation() 189 if err != nil { 190 logger.Infof("couldn't find existing jujud: %v", err) 191 return errors.Trace(err) 192 } 193 jujudLocation := filepath.Join(jujuDir, names.Jujud) 194 logger.Debugf("checking: %s", jujudLocation) 195 info, err := os.Stat(jujudLocation) 196 if err != nil { 197 logger.Infof("couldn't find existing jujud: %v", err) 198 return errors.Trace(err) 199 } 200 logger.Infof("Found agent binary to upload (%s)", jujudLocation) 201 target := filepath.Join(dir, names.Jujud) 202 logger.Infof("target: %v", target) 203 err = copyFileWithMode(jujudLocation, target, info.Mode()) 204 if err != nil { 205 return errors.Trace(err) 206 } 207 // If there's a version file beside the jujud binary or in the 208 // fallback location, include that. 209 versionTarget := filepath.Join(dir, names.JujudVersions) 210 211 versionPaths := []string{ 212 filepath.Join(jujuDir, names.JujudVersions), 213 filepath.Join(VersionFileFallbackDir, names.JujudVersions), 214 } 215 for _, versionPath := range versionPaths { 216 info, err = os.Stat(versionPath) 217 if os.IsNotExist(err) { 218 continue 219 } else if err != nil { 220 return errors.Trace(err) 221 } 222 logger.Infof("including versions file %q", versionPath) 223 return errors.Trace(copyFileWithMode(versionPath, versionTarget, info.Mode())) 224 } 225 return nil 226 } 227 228 func buildJujud(dir string) error { 229 logger.Infof("building jujud") 230 cmds := [][]string{ 231 {"go", "build", "-gccgoflags=-static-libgo", "-o", filepath.Join(dir, names.Jujud), "github.com/juju/juju/cmd/jujud"}, 232 } 233 for _, args := range cmds { 234 cmd := exec.Command(args[0], args[1:]...) 235 out, err := cmd.CombinedOutput() 236 if err != nil { 237 return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out) 238 } 239 } 240 return nil 241 } 242 243 func packageLocalTools(toolsDir string, buildAgent bool) error { 244 if !buildAgent { 245 if err := copyExistingJujud(toolsDir); err != nil { 246 return errors.New("no prepackaged agent available and no jujud binary can be found") 247 } 248 return nil 249 } 250 logger.Infof("Building agent binary to upload (%s)", jujuversion.Current.String()) 251 if err := buildJujud(toolsDir); err != nil { 252 return errors.Annotate(err, "cannot build jujud agent binary from source") 253 } 254 return nil 255 } 256 257 // BundleToolsFunc is a function which can bundle all the current juju tools 258 // in gzipped tar format to the given writer. 259 type BundleToolsFunc func(build bool, w io.Writer, forceVersion *version.Number) (version.Binary, bool, string, error) 260 261 // Override for testing. 262 var BundleTools BundleToolsFunc = bundleTools 263 264 // bundleTools bundles all the current juju tools in gzipped tar 265 // format to the given writer. If forceVersion is not nil and the 266 // file isn't an official build, a FORCE-VERSION file is included in 267 // the tools bundle so it will lie about its current version number. 268 func bundleTools(build bool, w io.Writer, forceVersion *version.Number) (_ version.Binary, official bool, sha256hash string, _ error) { 269 dir, err := ioutil.TempDir("", "juju-tools") 270 if err != nil { 271 return version.Binary{}, false, "", err 272 } 273 defer os.RemoveAll(dir) 274 if err := packageLocalTools(dir, build); err != nil { 275 return version.Binary{}, false, "", err 276 } 277 278 tvers, official, err := JujudVersion(dir) 279 if err != nil { 280 return version.Binary{}, false, "", errors.Trace(err) 281 } 282 if official { 283 logger.Debugf("using official version %s", tvers) 284 } else if forceVersion != nil { 285 logger.Debugf("forcing version to %s", forceVersion) 286 if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil { 287 return version.Binary{}, false, "", err 288 } 289 } 290 291 sha256hash, err = archiveAndSHA256(w, dir) 292 if err != nil { 293 return version.Binary{}, false, "", err 294 } 295 return tvers, official, sha256hash, err 296 } 297 298 var execCommand = exec.Command 299 300 func getVersionFromJujud(dir string) (version.Binary, error) { 301 path := filepath.Join(dir, names.Jujud) 302 cmd := execCommand(path, "version") 303 var stdout, stderr bytes.Buffer 304 cmd.Stdout = &stdout 305 cmd.Stderr = &stderr 306 307 if err := cmd.Run(); err != nil { 308 return version.Binary{}, errors.Errorf("cannot get version from %q: %v; %s", path, err, stderr.String()+stdout.String()) 309 } 310 tvs := strings.TrimSpace(stdout.String()) 311 tvers, err := version.ParseBinary(tvs) 312 if err != nil { 313 return version.Binary{}, errors.Errorf("invalid version %q printed by jujud", tvs) 314 } 315 return tvers, nil 316 } 317 318 // JujudVersion returns the Jujud version at the specified location, 319 // and whether it is an official binary. 320 func JujudVersion(dir string) (version.Binary, bool, error) { 321 tvers, err := getVersionFromFile(dir) 322 official := err == nil 323 if err != nil && !errors.IsNotFound(err) && !isNoMatchingToolsChecksum(err) { 324 return version.Binary{}, false, errors.Trace(err) 325 } 326 if errors.IsNotFound(err) || isNoMatchingToolsChecksum(err) { 327 // No signature file found. 328 // Extract the version number that the jujud binary was built with. 329 // This is used to check compatibility with the version of the client 330 // being used to bootstrap. 331 tvers, err = getVersionFromJujud(dir) 332 if err != nil { 333 return version.Binary{}, false, errors.Trace(err) 334 } 335 } 336 return tvers, official, nil 337 } 338 339 type noMatchingToolsChecksum struct { 340 versionPath string 341 jujudPath string 342 } 343 344 func (e *noMatchingToolsChecksum) Error() string { 345 return fmt.Sprintf("no SHA256 in version file %q matches binary %q", e.versionPath, e.jujudPath) 346 } 347 348 func isNoMatchingToolsChecksum(err error) bool { 349 _, ok := err.(*noMatchingToolsChecksum) 350 return ok 351 } 352 353 func getVersionFromFile(dir string) (version.Binary, error) { 354 versionPath := filepath.Join(dir, names.JujudVersions) 355 sigFile, err := os.Open(versionPath) 356 if os.IsNotExist(err) { 357 return version.Binary{}, errors.NotFoundf("version file %q", versionPath) 358 } else if err != nil { 359 return version.Binary{}, errors.Trace(err) 360 } 361 defer sigFile.Close() 362 363 versions, err := ParseVersions(sigFile) 364 if err != nil { 365 return version.Binary{}, errors.Trace(err) 366 } 367 368 // Find the binary by hash. 369 jujudPath := filepath.Join(dir, names.Jujud) 370 jujudFile, err := os.Open(jujudPath) 371 if err != nil { 372 return version.Binary{}, errors.Trace(err) 373 } 374 defer jujudFile.Close() 375 matching, err := versions.VersionsMatching(jujudFile) 376 if err != nil { 377 return version.Binary{}, errors.Trace(err) 378 } 379 if len(matching) == 0 { 380 return version.Binary{}, &noMatchingToolsChecksum{versionPath, jujudPath} 381 } 382 return selectBinary(matching) 383 } 384 385 func selectBinary(versions []string) (version.Binary, error) { 386 thisArch := arch.HostArch() 387 thisSeries, err := series.HostSeries() 388 if err != nil { 389 return version.Binary{}, errors.Trace(err) 390 } 391 var current version.Binary 392 for _, ver := range versions { 393 current, err = version.ParseBinary(ver) 394 if err != nil { 395 return version.Binary{}, errors.Trace(err) 396 } 397 if current.Series == thisSeries && current.Arch == thisArch { 398 return current, nil 399 } 400 } 401 // There's no version matching our series/arch, but the signature 402 // still matches the binary for all versions passed in, so just 403 // punt. 404 return current, nil 405 }