github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/tools.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/sha256"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/im7mortal/kmutex"
    19  	"github.com/juju/errors"
    20  	jujuhttp "github.com/juju/http/v2"
    21  	"github.com/juju/version/v2"
    22  
    23  	"github.com/juju/juju/apiserver/common"
    24  	"github.com/juju/juju/apiserver/httpcontext"
    25  	corebase "github.com/juju/juju/core/base"
    26  	"github.com/juju/juju/core/os/ostype"
    27  	"github.com/juju/juju/environs"
    28  	"github.com/juju/juju/environs/simplestreams"
    29  	envtools "github.com/juju/juju/environs/tools"
    30  	"github.com/juju/juju/rpc/params"
    31  	"github.com/juju/juju/state"
    32  	"github.com/juju/juju/state/binarystorage"
    33  	"github.com/juju/juju/state/stateenvirons"
    34  	"github.com/juju/juju/tools"
    35  )
    36  
    37  // toolsReadCloser wraps the ReadCloser for the tools blob
    38  // and the state StorageCloser.
    39  // It allows us to stream the tools binary from state,
    40  // closing them both at once when done.
    41  type toolsReadCloser struct {
    42  	f  io.ReadCloser
    43  	st binarystorage.StorageCloser
    44  }
    45  
    46  func (t *toolsReadCloser) Read(p []byte) (n int, err error) {
    47  	return t.f.Read(p)
    48  }
    49  
    50  func (t *toolsReadCloser) Close() error {
    51  	var err error
    52  	if err = t.f.Close(); err == nil {
    53  		return t.st.Close()
    54  	}
    55  	if err2 := t.st.Close(); err2 != nil {
    56  		err = errors.Wrap(err, err2)
    57  	}
    58  	return err
    59  }
    60  
    61  // toolsHandler handles tool upload through HTTPS in the API server.
    62  type toolsUploadHandler struct {
    63  	ctxt          httpContext
    64  	stateAuthFunc func(*http.Request) (*state.PooledState, error)
    65  }
    66  
    67  // toolsHandler handles tool download through HTTPS in the API server.
    68  type toolsDownloadHandler struct {
    69  	ctxt       httpContext
    70  	fetchMutex *kmutex.Kmutex
    71  }
    72  
    73  func newToolsDownloadHandler(httpCtxt httpContext) *toolsDownloadHandler {
    74  	return &toolsDownloadHandler{
    75  		ctxt:       httpCtxt,
    76  		fetchMutex: kmutex.New(),
    77  	}
    78  }
    79  
    80  func (h *toolsDownloadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    81  	st, err := h.ctxt.stateForRequestUnauthenticated(r)
    82  	if err != nil {
    83  		if err := sendError(w, err); err != nil {
    84  			logger.Errorf("%v", err)
    85  		}
    86  		return
    87  	}
    88  	defer st.Release()
    89  
    90  	switch r.Method {
    91  	case "GET":
    92  		reader, size, err := h.getToolsForRequest(r, st.State)
    93  		if err != nil {
    94  			logger.Errorf("GET(%s) failed: %v", r.URL, err)
    95  			if err := sendError(w, errors.NewBadRequest(err, "")); err != nil {
    96  				logger.Errorf("%v", err)
    97  			}
    98  			return
    99  		}
   100  		defer reader.Close()
   101  		if err := h.sendTools(w, reader, size); err != nil {
   102  			logger.Errorf("%v", err)
   103  		}
   104  	default:
   105  		if err := sendError(w, errors.MethodNotAllowedf("unsupported method: %q", r.Method)); err != nil {
   106  			logger.Errorf("%v", err)
   107  		}
   108  	}
   109  }
   110  
   111  func (h *toolsUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   112  	// Validate before authenticate because the authentication is dependent
   113  	// on the state connection that is determined during the validation.
   114  	st, err := h.stateAuthFunc(r)
   115  	if err != nil {
   116  		if err := sendError(w, err); err != nil {
   117  			logger.Errorf("%v", err)
   118  		}
   119  		return
   120  	}
   121  	defer st.Release()
   122  
   123  	switch r.Method {
   124  	case "POST":
   125  		// Add tools to storage.
   126  		agentTools, err := h.processPost(r, st.State)
   127  		if err != nil {
   128  			if err := sendError(w, err); err != nil {
   129  				logger.Errorf("%v", err)
   130  			}
   131  			return
   132  		}
   133  		if err := sendStatusAndJSON(w, http.StatusOK, &params.ToolsResult{
   134  			ToolsList: tools.List{agentTools},
   135  		}); err != nil {
   136  			logger.Errorf("%v", err)
   137  		}
   138  	default:
   139  		if err := sendError(w, errors.MethodNotAllowedf("unsupported method: %q", r.Method)); err != nil {
   140  			logger.Errorf("%v", err)
   141  		}
   142  	}
   143  }
   144  
   145  // getToolsForRequest retrieves the compressed agent binaries tarball from state
   146  // based on the input HTTP request.
   147  // It is returned with the size of the file as recorded in the stored metadata.
   148  func (h *toolsDownloadHandler) getToolsForRequest(r *http.Request, st *state.State) (_ io.ReadCloser, _ int64, err error) {
   149  	vers, err := version.ParseBinary(r.URL.Query().Get(":version"))
   150  	if err != nil {
   151  		return nil, 0, errors.Annotate(err, "error parsing version")
   152  	}
   153  	logger.Debugf("request for agent binaries: %s", vers)
   154  
   155  	storage, err := st.ToolsStorage()
   156  	if err != nil {
   157  		return nil, 0, errors.Annotate(err, "error getting storage for agent binaries")
   158  	}
   159  	defer func() {
   160  		if err != nil {
   161  			_ = storage.Close()
   162  		}
   163  	}()
   164  
   165  	// TODO(juju4) = remove this compatibility logic
   166  	// Looked for stored tools which are recorded for a series
   167  	// but which have the same os type as the wanted version.
   168  	// Alternatively, the request may have been for a specifc
   169  	// series and we need to use stored tools for the corresponding
   170  	// os type.
   171  	storageVers := vers
   172  	var osTypeName string
   173  	if vers.Number.Major == 2 && vers.Number.Minor <= 8 {
   174  		wantedOSType := vers.Release
   175  		if !ostype.IsValidOSTypeName(vers.Release) {
   176  			wantedOSType = corebase.DefaultOSTypeNameFromSeries(vers.Release)
   177  		}
   178  		vers.Release = wantedOSType
   179  
   180  		all, err := storage.AllMetadata()
   181  		if err != nil {
   182  			return nil, 0, errors.Trace(err)
   183  		}
   184  		var osMatchVersion *version.Binary
   185  		for _, m := range all {
   186  			metaVers, err := version.ParseBinary(m.Version)
   187  			if err != nil {
   188  				return nil, 0, errors.Annotate(err, "error parsing metadata version")
   189  			}
   190  
   191  			// Exact match so just use that with os type name substitution.
   192  			if m.Version == vers.String() {
   193  				osMatchVersion = &metaVers
   194  				break
   195  			}
   196  			if osMatchVersion != nil {
   197  				continue
   198  			}
   199  			metaOSType := metaVers.Release
   200  			if !ostype.IsValidOSTypeName(metaVers.Release) {
   201  				metaOSType = corebase.DefaultOSTypeNameFromSeries(metaVers.Release)
   202  			}
   203  			toCompare := metaVers
   204  			toCompare.Release = strings.ToLower(metaOSType)
   205  			if toCompare.String() == vers.String() {
   206  				logger.Debugf("using os based version %s for requested %s", toCompare, vers)
   207  				osMatchVersion = &metaVers
   208  				osTypeName = toCompare.Release
   209  			}
   210  		}
   211  		// Set the version to store to be the match we found
   212  		// for any compatible series.
   213  		if osMatchVersion != nil {
   214  			storageVers = *osMatchVersion
   215  		}
   216  	}
   217  
   218  	locker := h.fetchMutex.Locker(storageVers.String())
   219  	locker.Lock()
   220  	defer locker.Unlock()
   221  
   222  	md, reader, err := storage.Open(storageVers.String())
   223  	if errors.IsNotFound(err) {
   224  		// Tools could not be found in tools storage,
   225  		// so look for them in simplestreams,
   226  		// fetch them and cache in tools storage.
   227  		logger.Infof("%v agent binaries not found locally, fetching", vers)
   228  		if osTypeName != "" {
   229  			storageVers.Release = osTypeName
   230  		}
   231  		err = h.fetchAndCacheTools(vers, storageVers, st, storage)
   232  		if err != nil {
   233  			err = errors.Annotate(err, "error fetching agent binaries")
   234  		} else {
   235  			md, reader, err = storage.Open(storageVers.String())
   236  		}
   237  	}
   238  	if err != nil {
   239  		return nil, 0, errors.Trace(err)
   240  	}
   241  
   242  	return &toolsReadCloser{f: reader, st: storage}, md.Size, nil
   243  }
   244  
   245  // fetchAndCacheTools fetches tools with the specified version by searching for a URL
   246  // in simplestreams and GETting it, caching the result in tools storage before returning
   247  // to the caller.
   248  func (h *toolsDownloadHandler) fetchAndCacheTools(
   249  	v version.Binary,
   250  	storageVers version.Binary,
   251  	st *state.State,
   252  	modelStorage binarystorage.Storage,
   253  ) error {
   254  	systemState, err := h.ctxt.statePool().SystemState()
   255  	if err != nil {
   256  		return errors.Trace(err)
   257  	}
   258  
   259  	controllerModel, err := systemState.Model()
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	var model *state.Model
   265  	var storage binarystorage.Storage
   266  	switch controllerModel.Type() {
   267  	case state.ModelTypeCAAS:
   268  		// TODO(caas): unify tool fetching
   269  		// Cache the tools against the model when the controller is CAAS.
   270  		model, err = st.Model()
   271  		if err != nil {
   272  			return err
   273  		}
   274  		storage = modelStorage
   275  	case state.ModelTypeIAAS:
   276  		// Cache the tools against the controller when the controller is IAAS.
   277  		model = controllerModel
   278  		controllerStorage, err := systemState.ToolsStorage()
   279  		if err != nil {
   280  			return err
   281  		}
   282  		defer controllerStorage.Close()
   283  		storage = controllerStorage
   284  	default:
   285  		return errors.NotValidf("model type %q", controllerModel.Type())
   286  	}
   287  
   288  	newEnviron := stateenvirons.GetNewEnvironFunc(environs.New)
   289  	env, err := newEnviron(model)
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	ss := simplestreams.NewSimpleStreams(simplestreams.DefaultDataSourceFactory())
   295  	exactTools, err := envtools.FindExactTools(ss, env, v.Number, v.Release, v.Arch)
   296  	if err != nil {
   297  		return err
   298  	}
   299  
   300  	// No need to verify the server's identity because we verify the SHA-256 hash.
   301  	logger.Infof("fetching %v agent binaries from %v", v, exactTools.URL)
   302  	client := jujuhttp.NewClient(jujuhttp.WithSkipHostnameVerification(true))
   303  	resp, err := client.Get(context.TODO(), exactTools.URL)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	defer func() { _ = resp.Body.Close() }()
   308  	if resp.StatusCode != http.StatusOK {
   309  		msg := fmt.Sprintf("bad HTTP response: %v", resp.Status)
   310  		if body, err := io.ReadAll(resp.Body); err == nil {
   311  			msg += fmt.Sprintf(" (%s)", bytes.TrimSpace(body))
   312  		}
   313  		return errors.New(msg)
   314  	}
   315  
   316  	data, respSha256, size, err := tmpCacheAndHash(resp.Body)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	defer data.Close()
   321  	if size != exactTools.Size {
   322  		return errors.Errorf("size mismatch for %s", exactTools.URL)
   323  	}
   324  	if respSha256 != exactTools.SHA256 {
   325  		return errors.Errorf("hash mismatch for %s", exactTools.URL)
   326  	}
   327  
   328  	md := binarystorage.Metadata{
   329  		Version: storageVers.String(),
   330  		Size:    exactTools.Size,
   331  		SHA256:  exactTools.SHA256,
   332  	}
   333  	if err := storage.Add(data, md); err != nil {
   334  		return errors.Annotate(err, "error caching agent binaries")
   335  	}
   336  
   337  	return nil
   338  }
   339  
   340  // sendTools streams the tools tarball to the client.
   341  func (h *toolsDownloadHandler) sendTools(w http.ResponseWriter, reader io.ReadCloser, size int64) error {
   342  	logger.Tracef("sending %d bytes", size)
   343  
   344  	w.Header().Set("Content-Type", "application/x-tar-gz")
   345  	w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
   346  
   347  	if _, err := io.Copy(w, reader); err != nil {
   348  		// Having begun writing, it is too late to send an error response here.
   349  		return errors.Annotatef(err, "failed to send agent binaries")
   350  	}
   351  	return nil
   352  }
   353  
   354  // processPost handles a tools upload POST request after authentication.
   355  func (h *toolsUploadHandler) processPost(r *http.Request, st *state.State) (*tools.Tools, error) {
   356  	query := r.URL.Query()
   357  
   358  	binaryVersionParam := query.Get("binaryVersion")
   359  	if binaryVersionParam == "" {
   360  		return nil, errors.BadRequestf("expected binaryVersion argument")
   361  	}
   362  	toolsVersion, err := version.ParseBinary(binaryVersionParam)
   363  	if err != nil {
   364  		return nil, errors.NewBadRequest(err, fmt.Sprintf("invalid agent binaries version %q", binaryVersionParam))
   365  	}
   366  
   367  	// Make sure the content type is x-tar-gz.
   368  	contentType := r.Header.Get("Content-Type")
   369  	if contentType != "application/x-tar-gz" {
   370  		return nil, errors.BadRequestf("expected Content-Type: application/x-tar-gz, got: %v", contentType)
   371  	}
   372  
   373  	logger.Debugf("request to upload agent binaries: %s", toolsVersion)
   374  	toolsVersions := []version.Binary{toolsVersion}
   375  	serverRoot := h.getServerRoot(r, query, st)
   376  	return h.handleUpload(r.Body, toolsVersions, serverRoot, st)
   377  }
   378  
   379  func (h *toolsUploadHandler) getServerRoot(r *http.Request, query url.Values, st *state.State) string {
   380  	modelUUID := httpcontext.RequestModelUUID(r)
   381  	return fmt.Sprintf("https://%s/model/%s", r.Host, modelUUID)
   382  }
   383  
   384  // handleUpload uploads the tools data from the reader to env storage as the specified version.
   385  func (h *toolsUploadHandler) handleUpload(r io.Reader, toolsVersions []version.Binary, serverRoot string, st *state.State) (*tools.Tools, error) {
   386  	// Check if changes are allowed and the command may proceed.
   387  	blockChecker := common.NewBlockChecker(st)
   388  	if err := blockChecker.ChangeAllowed(); err != nil {
   389  		return nil, errors.Trace(err)
   390  	}
   391  	storage, err := st.ToolsStorage()
   392  	if err != nil {
   393  		return nil, err
   394  	}
   395  	defer storage.Close()
   396  
   397  	// Read the tools tarball from the request, calculating the sha256 along the way.
   398  	data, sha256, size, err := tmpCacheAndHash(r)
   399  	if err != nil {
   400  		return nil, err
   401  	}
   402  	defer data.Close()
   403  
   404  	if size == 0 {
   405  		return nil, errors.BadRequestf("no agent binaries uploaded")
   406  	}
   407  
   408  	// TODO(wallyworld): check integrity of tools tarball.
   409  
   410  	// Store tools and metadata in tools storage.
   411  	for _, v := range toolsVersions {
   412  		metadata := binarystorage.Metadata{
   413  			Version: v.String(),
   414  			Size:    size,
   415  			SHA256:  sha256,
   416  		}
   417  		logger.Debugf("uploading agent binaries %+v to storage", metadata)
   418  		if err := storage.Add(data, metadata); err != nil {
   419  			return nil, err
   420  		}
   421  	}
   422  
   423  	tools := &tools.Tools{
   424  		Version: toolsVersions[0],
   425  		Size:    size,
   426  		SHA256:  sha256,
   427  		URL:     common.ToolsURL(serverRoot, toolsVersions[0]),
   428  	}
   429  	return tools, nil
   430  }
   431  
   432  type cleanupCloser struct {
   433  	io.ReadCloser
   434  	cleanup func()
   435  }
   436  
   437  func (c *cleanupCloser) Close() error {
   438  	if c.cleanup != nil {
   439  		c.cleanup()
   440  	}
   441  	return c.ReadCloser.Close()
   442  }
   443  
   444  func tmpCacheAndHash(r io.Reader) (data io.ReadCloser, sha256hex string, size int64, err error) {
   445  	tmpFile, err := os.CreateTemp("", "jujutools*")
   446  	tmpFilename := tmpFile.Name()
   447  	cleanup := func() {
   448  		_ = tmpFile.Close()
   449  		_ = os.Remove(tmpFilename)
   450  	}
   451  	defer func() {
   452  		if err != nil {
   453  			cleanup()
   454  		}
   455  	}()
   456  	tr := io.TeeReader(r, tmpFile)
   457  	hasher := sha256.New()
   458  	_, err = io.Copy(hasher, tr)
   459  	if err != nil {
   460  		return nil, "", 0, errors.Annotatef(err, "failed to hash agent tools and write to file %q", tmpFilename)
   461  	}
   462  	_, err = tmpFile.Seek(0, 0)
   463  	if err != nil {
   464  		return nil, "", 0, errors.Trace(err)
   465  	}
   466  	stat, err := tmpFile.Stat()
   467  	if err != nil {
   468  		return nil, "", 0, errors.Trace(err)
   469  	}
   470  	return &cleanupCloser{tmpFile, cleanup}, fmt.Sprintf("%x", hasher.Sum(nil)), stat.Size(), nil
   471  }