github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/agent/tools/toolsdir.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 "compress/gzip" 9 "crypto/sha256" 10 "encoding/json" 11 "fmt" 12 "io" 13 "os" 14 "path" 15 "strings" 16 17 "github.com/juju/errors" 18 "github.com/juju/utils/v3/symlink" 19 "github.com/juju/version/v2" 20 21 coretools "github.com/juju/juju/tools" 22 ) 23 24 const ( 25 dirPerm = 0755 26 filePerm = 0644 27 toolsFile = "downloaded-tools.txt" 28 ) 29 30 // SharedToolsDir returns the directory that is used to 31 // store binaries for the given version of the juju tools 32 // within the dataDir directory. 33 func SharedToolsDir(dataDir string, vers version.Binary) string { 34 return path.Join(dataDir, "tools", vers.String()) 35 } 36 37 // ToolsDir returns the directory that is used/ to store binaries for 38 // the tools used by the given agent within the given dataDir directory. 39 // Conventionally it is a symbolic link to the actual tools directory. 40 func ToolsDir(dataDir, agentName string) string { 41 //TODO(perrito666) ToolsDir and any other *Dir needs to take the 42 // agent series to use the right path, in this case, if filepath 43 // is used it ends up creating a bogus toolsdir when the client 44 // is in windows. 45 return path.Join(dataDir, "tools", agentName) 46 } 47 48 // UnpackTools reads a set of juju tools in gzipped tar-archive 49 // format and unpacks them into the appropriate tools directory 50 // within dataDir. If a valid tools directory already exists, 51 // UnpackTools returns without error. 52 func UnpackTools(dataDir string, tools *coretools.Tools, r io.Reader) (err error) { 53 // Unpack the gzip file and compute the checksum. 54 sha256hash := sha256.New() 55 zr, err := gzip.NewReader(io.TeeReader(r, sha256hash)) 56 if err != nil { 57 return err 58 } 59 defer zr.Close() 60 f, err := os.CreateTemp(os.TempDir(), "tools-tar") 61 if err != nil { 62 return err 63 } 64 defer func() { 65 _ = f.Close() 66 _ = os.Remove(f.Name()) 67 }() 68 69 _, err = io.Copy(f, zr) 70 if err != nil { 71 return err 72 } 73 74 gzipSHA256 := fmt.Sprintf("%x", sha256hash.Sum(nil)) 75 if tools.SHA256 != gzipSHA256 { 76 return fmt.Errorf("tarball sha256 mismatch, expected %s, got %s", tools.SHA256, gzipSHA256) 77 } 78 79 // Make a temporary directory in the tools directory, 80 // first ensuring that the tools directory exists. 81 toolsDir := path.Join(dataDir, "tools") 82 err = os.MkdirAll(toolsDir, dirPerm) 83 if err != nil { 84 return err 85 } 86 dir, err := os.MkdirTemp(toolsDir, "unpacking-") 87 if err != nil { 88 return err 89 } 90 defer removeAll(dir) 91 92 // Checksum matches, now reset the file and untar it. 93 _, err = f.Seek(0, 0) 94 if err != nil { 95 return err 96 } 97 tr := tar.NewReader(f) 98 for { 99 hdr, err := tr.Next() 100 if err == io.EOF { 101 break 102 } 103 if err != nil { 104 return err 105 } 106 if strings.ContainsAny(hdr.Name, "/\\") { 107 return fmt.Errorf("bad name %q in agent binary archive", hdr.Name) 108 } 109 if hdr.Typeflag != tar.TypeReg { 110 return fmt.Errorf("bad file type %c in file %q in agent binary archive", hdr.Typeflag, hdr.Name) 111 } 112 name := path.Join(dir, hdr.Name) 113 if err := writeFile(name, os.FileMode(hdr.Mode&0777), tr); err != nil { 114 return errors.Annotatef(err, "tar extract %q failed", name) 115 } 116 } 117 if err = WriteToolsMetadataData(dir, tools); err != nil { 118 return err 119 } 120 121 // The tempdir is created with 0700, so we need to make it more 122 // accessible for juju-exec. 123 err = os.Chmod(dir, dirPerm) 124 if err != nil { 125 return err 126 } 127 128 err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version)) 129 // If we've failed to rename the directory, it may be because 130 // the directory already exists - if ReadTools succeeds, we 131 // assume all's ok. 132 if err != nil { 133 if _, err := ReadTools(dataDir, tools.Version); err == nil { 134 return nil 135 } 136 } 137 return err 138 } 139 140 func removeAll(dir string) { 141 err := os.RemoveAll(dir) 142 if err == nil || os.IsNotExist(err) { 143 return 144 } 145 logger.Errorf("cannot remove %q: %v", dir, err) 146 } 147 148 func writeFile(name string, mode os.FileMode, r io.Reader) error { 149 f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) 150 if err != nil { 151 return err 152 } 153 defer f.Close() 154 _, err = io.Copy(f, r) 155 return err 156 } 157 158 // ReadTools checks that the tools information for the given version exists 159 // in the dataDir directory, and returns a Tools instance. 160 // The tools information is json encoded in a text file, "downloaded-tools.txt". 161 func ReadTools(dataDir string, vers version.Binary) (*coretools.Tools, error) { 162 dir := SharedToolsDir(dataDir, vers) 163 toolsData, err := os.ReadFile(path.Join(dir, toolsFile)) 164 if err != nil { 165 return nil, fmt.Errorf("cannot read agent metadata in directory %v: %v", dir, err) 166 } 167 var tools coretools.Tools 168 if err := json.Unmarshal(toolsData, &tools); err != nil { 169 return nil, fmt.Errorf("invalid agent metadata in directory %q: %v", dir, err) 170 } 171 return &tools, nil 172 } 173 174 // ChangeAgentTools atomically replaces the agent-specific symlink 175 // under dataDir so it points to the previously unpacked 176 // version vers. It returns the new tools read. 177 func ChangeAgentTools(dataDir string, agentName string, vers version.Binary) (*coretools.Tools, error) { 178 tools, err := ReadTools(dataDir, vers) 179 if err != nil { 180 return nil, err 181 } 182 // build absolute path to toolsDir. Windows implementation of symlink 183 // will check for the existence of the source file and error if it does 184 // not exists. This is a limitation of junction points (symlinks) on NTFS 185 toolPath := SharedToolsDir(dataDir, tools.Version) 186 toolsDir := ToolsDir(dataDir, agentName) 187 188 err = symlink.Replace(toolsDir, toolPath) 189 if err != nil { 190 return nil, fmt.Errorf("cannot replace tools directory: %s", err) 191 } 192 return tools, nil 193 } 194 195 // WriteToolsMetadataData writes the tools metadata file to the given directory. 196 func WriteToolsMetadataData(dir string, tools *coretools.Tools) error { 197 toolsMetadataData, err := json.Marshal(tools) 198 if err != nil { 199 return err 200 } 201 return os.WriteFile(path.Join(dir, toolsFile), toolsMetadataData, filePerm) 202 }