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  }