github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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/version" 21 22 "github.com/juju/juju/juju/names" 23 jujuversion "github.com/juju/juju/version" 24 ) 25 26 // Archive writes the executable files found in the given directory in 27 // gzipped tar format to w. 28 func Archive(w io.Writer, dir string) error { 29 entries, err := ioutil.ReadDir(dir) 30 if err != nil { 31 return err 32 } 33 34 gzw := gzip.NewWriter(w) 35 defer closeErrorCheck(&err, gzw) 36 37 tarw := tar.NewWriter(gzw) 38 defer closeErrorCheck(&err, tarw) 39 40 for _, ent := range entries { 41 h := tarHeader(ent) 42 logger.Debugf("adding entry: %#v", h) 43 // ignore local umask 44 if isExecutable(ent) { 45 h.Mode = 0755 46 } else { 47 h.Mode = 0644 48 } 49 err := tarw.WriteHeader(h) 50 if err != nil { 51 return err 52 } 53 fileName := filepath.Join(dir, ent.Name()) 54 if err := copyFile(tarw, fileName); err != nil { 55 return err 56 } 57 } 58 return nil 59 } 60 61 // archiveAndSHA256 calls Archive with the provided arguments, 62 // and returns a hex-encoded SHA256 hash of the resulting 63 // archive. 64 func archiveAndSHA256(w io.Writer, dir string) (sha256hash string, err error) { 65 h := sha256.New() 66 if err := Archive(io.MultiWriter(h, w), dir); err != nil { 67 return "", err 68 } 69 return fmt.Sprintf("%x", h.Sum(nil)), err 70 } 71 72 // copyFile writes the contents of the given file to w. 73 func copyFile(w io.Writer, file string) error { 74 f, err := os.Open(file) 75 if err != nil { 76 return err 77 } 78 defer f.Close() 79 _, err = io.Copy(w, f) 80 return err 81 } 82 83 // tarHeader returns a tar file header given the file's stat 84 // information. 85 func tarHeader(i os.FileInfo) *tar.Header { 86 return &tar.Header{ 87 Typeflag: tar.TypeReg, 88 Name: i.Name(), 89 Size: i.Size(), 90 Mode: int64(i.Mode() & 0777), 91 ModTime: i.ModTime(), 92 AccessTime: i.ModTime(), 93 ChangeTime: i.ModTime(), 94 Uname: "ubuntu", 95 Gname: "ubuntu", 96 } 97 } 98 99 // isExecutable returns whether the given info 100 // represents a regular file executable by (at least) the user. 101 func isExecutable(i os.FileInfo) bool { 102 return i.Mode()&(0100|os.ModeType) == 0100 103 } 104 105 // closeErrorCheck means that we can ensure that 106 // Close errors do not get lost even when we defer them, 107 func closeErrorCheck(errp *error, c io.Closer) { 108 err := c.Close() 109 if *errp == nil { 110 *errp = err 111 } 112 } 113 114 func setenv(env []string, val string) []string { 115 prefix := val[0 : strings.Index(val, "=")+1] 116 for i, eval := range env { 117 if strings.HasPrefix(eval, prefix) { 118 env[i] = val 119 return env 120 } 121 } 122 return append(env, val) 123 } 124 125 func findExecutable(execFile string) (string, error) { 126 logger.Debugf("looking for: %s", execFile) 127 if filepath.IsAbs(execFile) { 128 return execFile, nil 129 } 130 131 dir, file := filepath.Split(execFile) 132 133 // Now we have two possibilities: 134 // file == path indicating that the PATH was searched 135 // dir != "" indicating that it is a relative path 136 137 if dir == "" { 138 path := os.Getenv("PATH") 139 for _, name := range filepath.SplitList(path) { 140 result := filepath.Join(name, file) 141 // Use exec.LookPath() to check if the file exists and is executable` 142 f, err := exec.LookPath(result) 143 if err == nil { 144 return f, nil 145 } 146 } 147 148 return "", fmt.Errorf("could not find %q in the path", file) 149 } 150 cwd, err := os.Getwd() 151 if err != nil { 152 return "", err 153 } 154 return filepath.Clean(filepath.Join(cwd, execFile)), nil 155 } 156 157 func copyExistingJujud(dir string) error { 158 // Assume that the user is running juju. 159 jujuLocation, err := findExecutable(os.Args[0]) 160 if err != nil { 161 logger.Infof("%v", err) 162 return err 163 } 164 jujudLocation := filepath.Join(filepath.Dir(jujuLocation), names.Jujud) 165 logger.Debugf("checking: %s", jujudLocation) 166 info, err := os.Stat(jujudLocation) 167 if err != nil { 168 logger.Infof("couldn't find existing jujud") 169 return err 170 } 171 logger.Infof("Found agent binary to upload (%s)", jujudLocation) 172 // TODO(thumper): break this out into a util function. 173 // copy the file into the dir. 174 source, err := os.Open(jujudLocation) 175 if err != nil { 176 logger.Infof("open source failed: %v", err) 177 return err 178 } 179 defer source.Close() 180 target := filepath.Join(dir, names.Jujud) 181 logger.Infof("target: %v", target) 182 destination, err := os.OpenFile(target, os.O_RDWR|os.O_TRUNC|os.O_CREATE, info.Mode()) 183 if err != nil { 184 logger.Infof("open destination failed: %v", err) 185 return err 186 } 187 defer destination.Close() 188 _, err = io.Copy(destination, source) 189 if err != nil { 190 return err 191 } 192 return nil 193 } 194 195 func buildJujud(dir string) error { 196 logger.Infof("building jujud") 197 cmds := [][]string{ 198 {"go", "build", "-gccgoflags=-static-libgo", "-o", filepath.Join(dir, names.Jujud), "github.com/juju/juju/cmd/jujud"}, 199 } 200 for _, args := range cmds { 201 cmd := exec.Command(args[0], args[1:]...) 202 out, err := cmd.CombinedOutput() 203 if err != nil { 204 return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out) 205 } 206 } 207 return nil 208 } 209 210 func packageLocalTools(toolsDir string, buildAgent bool) error { 211 if !buildAgent { 212 if err := copyExistingJujud(toolsDir); err != nil { 213 return errors.New("no prepackaged agent available and no jujud binary can be found") 214 } 215 return nil 216 } 217 logger.Infof("Building agent binary to upload (%s)", jujuversion.Current.String()) 218 if err := buildJujud(toolsDir); err != nil { 219 return errors.Annotate(err, "cannot build jujud agent binary from source") 220 } 221 return nil 222 } 223 224 // BundleToolsFunc is a function which can bundle all the current juju tools 225 // in gzipped tar format to the given writer. 226 type BundleToolsFunc func(build bool, w io.Writer, forceVersion *version.Number) (version.Binary, string, error) 227 228 // Override for testing. 229 var BundleTools BundleToolsFunc = bundleTools 230 231 // bundleTools bundles all the current juju tools in gzipped tar 232 // format to the given writer. 233 // If forceVersion is not nil, a FORCE-VERSION file is included in 234 // the tools bundle so it will lie about its current version number. 235 func bundleTools(build bool, w io.Writer, forceVersion *version.Number) (tvers version.Binary, sha256Hash string, err error) { 236 dir, err := ioutil.TempDir("", "juju-tools") 237 if err != nil { 238 return version.Binary{}, "", err 239 } 240 defer os.RemoveAll(dir) 241 if err := packageLocalTools(dir, build); err != nil { 242 return version.Binary{}, "", err 243 } 244 245 // Extract the version number that the jujud binary was built with. 246 // This is used to check compatibility with the version of the client 247 // being used to bootstrap. 248 tvers, err = getVersionFromJujud(dir) 249 if err != nil { 250 return version.Binary{}, "", errors.Trace(err) 251 } 252 253 if forceVersion != nil { 254 logger.Debugf("forcing version to %s", forceVersion) 255 if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil { 256 return version.Binary{}, "", err 257 } 258 } 259 260 sha256hash, err := archiveAndSHA256(w, dir) 261 if err != nil { 262 return version.Binary{}, "", err 263 } 264 return tvers, sha256hash, err 265 } 266 267 var execCommand = exec.Command 268 269 func getVersionFromJujud(dir string) (version.Binary, error) { 270 path := filepath.Join(dir, names.Jujud) 271 cmd := execCommand(path, "version") 272 var stdout, stderr bytes.Buffer 273 cmd.Stdout = &stdout 274 cmd.Stderr = &stderr 275 276 if err := cmd.Run(); err != nil { 277 return version.Binary{}, errors.Errorf("cannot get version from %q: %v; %s", path, err, stderr.String()+stdout.String()) 278 } 279 tvs := strings.TrimSpace(stdout.String()) 280 tvers, err := version.ParseBinary(tvs) 281 if err != nil { 282 return version.Binary{}, errors.Errorf("invalid version %q printed by jujud", tvs) 283 } 284 return tvers, nil 285 }