github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/charms.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  	"archive/zip"
     8  	"bytes"
     9  	"crypto/sha256"
    10  	"encoding/hex"
    11  	"fmt"
    12  	"io"
    13  	"mime"
    14  	"net/http"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  
    22  	"github.com/juju/charm/v12"
    23  	"github.com/juju/errors"
    24  	ziputil "github.com/juju/utils/v3/zip"
    25  
    26  	"github.com/juju/juju/apiserver/common"
    27  	apiservererrors "github.com/juju/juju/apiserver/errors"
    28  	"github.com/juju/juju/apiserver/facades/client/charms/services"
    29  	"github.com/juju/juju/core/charm/downloader"
    30  	"github.com/juju/juju/rpc/params"
    31  	"github.com/juju/juju/state"
    32  	"github.com/juju/juju/state/storage"
    33  )
    34  
    35  type FailableHandlerFunc func(http.ResponseWriter, *http.Request) error
    36  
    37  // CharmsHTTPHandler creates is a http.Handler which serves POST
    38  // requests to a PostHandler and GET requests to a GetHandler.
    39  //
    40  // TODO(katco): This is the beginning of inverting the dependencies in
    41  // this callstack by splitting out the serving mechanism from the
    42  // modules that are processing the requests. The next step is to
    43  // publically expose construction of a suitable PostHandler and
    44  // GetHandler whose goals should be clearly called out in their names,
    45  // (e.g. charmPersitAPI for POSTs).
    46  //
    47  // To accomplish this, we'll have to make the httpContext type public
    48  // so that we can pass it into these public functions.
    49  //
    50  // After we do this, we can then test the individual funcs/structs
    51  // without standing up an entire HTTP server. I.e. actual unit
    52  // tests. If you're in this area and can, please chisel away at this
    53  // problem and update this TODO as needed! Many thanks, hacker!
    54  //
    55  // TODO(stickupkid): This handler is terrible, we could implement a middleware
    56  // pattern to handle discreet logic and then pass it on to the next in the
    57  // pipeline.
    58  //
    59  // As usual big methods lead to untestable code and it causes testing pain.
    60  type CharmsHTTPHandler struct {
    61  	PostHandler FailableHandlerFunc
    62  	GetHandler  FailableHandlerFunc
    63  }
    64  
    65  func (h *CharmsHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    66  	var err error
    67  	switch r.Method {
    68  	case "POST":
    69  		err = errors.Annotate(h.PostHandler(w, r), "cannot upload charm")
    70  	case "GET":
    71  		err = errors.Annotate(h.GetHandler(w, r), "cannot retrieve charm")
    72  	default:
    73  		err = emitUnsupportedMethodErr(r.Method)
    74  	}
    75  
    76  	if err != nil {
    77  		if err := sendJSONError(w, r, errors.Trace(err)); err != nil {
    78  			logger.Errorf("%v", errors.Annotate(err, "cannot return error to user"))
    79  		}
    80  	}
    81  }
    82  
    83  // charmsHandler handles charm upload through HTTPS in the API server.
    84  type charmsHandler struct {
    85  	ctxt          httpContext
    86  	dataDir       string
    87  	stateAuthFunc func(*http.Request) (*state.PooledState, error)
    88  }
    89  
    90  // bundleContentSenderFunc functions are responsible for sending a
    91  // response related to a charm bundle.
    92  type bundleContentSenderFunc func(w http.ResponseWriter, r *http.Request, bundle *charm.CharmArchive) error
    93  
    94  func (h *charmsHandler) ServeUnsupported(w http.ResponseWriter, r *http.Request) error {
    95  	return errors.Trace(emitUnsupportedMethodErr(r.Method))
    96  }
    97  
    98  func (h *charmsHandler) ServePost(w http.ResponseWriter, r *http.Request) error {
    99  	logger.Child("charmsHandler").Tracef("ServePost(%s)", r.URL)
   100  	if r.Method != "POST" {
   101  		return errors.Trace(emitUnsupportedMethodErr(r.Method))
   102  	}
   103  
   104  	// Make sure the content type is zip.
   105  	contentType := r.Header.Get("Content-Type")
   106  	if contentType != "application/zip" {
   107  		return errors.BadRequestf("expected Content-Type: application/zip, got: %v", contentType)
   108  	}
   109  
   110  	st, err := h.stateAuthFunc(r)
   111  	if err != nil {
   112  		return errors.Trace(err)
   113  	}
   114  	defer st.Release()
   115  
   116  	// Add a charm to the store provider.
   117  	charmURL, err := h.processPost(r, st.State)
   118  	if err != nil {
   119  		return errors.NewBadRequest(err, "")
   120  	}
   121  	return errors.Trace(sendStatusAndHeadersAndJSON(w, http.StatusOK, map[string]string{"Juju-Curl": charmURL}, &params.CharmsResponse{CharmURL: charmURL}))
   122  }
   123  
   124  func (h *charmsHandler) ServeGet(w http.ResponseWriter, r *http.Request) error {
   125  	logger.Child("charmsHandler").Tracef("ServeGet(%s)", r.URL)
   126  	if r.Method != "GET" {
   127  		return errors.Trace(emitUnsupportedMethodErr(r.Method))
   128  	}
   129  
   130  	st, _, err := h.ctxt.stateForRequestAuthenticated(r)
   131  	if err != nil {
   132  		return errors.Trace(err)
   133  	}
   134  	defer st.Release()
   135  
   136  	// Retrieve or list charm files.
   137  	// Requires "url" (charm URL) and an optional "file" (the path to the
   138  	// charm file) to be included in the query. Optionally also receives an
   139  	// "icon" query for returning the charm icon or a default one in case the
   140  	// charm has no icon.
   141  	charmArchivePath, fileArg, serveIcon, err := h.processGet(r, st.State)
   142  	if err != nil {
   143  		// An error occurred retrieving the charm bundle.
   144  		if errors.IsNotFound(err) || errors.IsNotYetAvailable(err) {
   145  			return errors.Trace(err)
   146  		}
   147  
   148  		return errors.NewBadRequest(err, "")
   149  	}
   150  	defer os.Remove(charmArchivePath)
   151  
   152  	var sender bundleContentSenderFunc
   153  	switch fileArg {
   154  	case "":
   155  		// The client requested the list of charm files.
   156  		sender = h.manifestSender
   157  	case "*":
   158  		// The client requested the archive.
   159  		sender = h.archiveSender
   160  	default:
   161  		// The client requested a specific file.
   162  		sender = h.archiveEntrySender(fileArg, serveIcon)
   163  	}
   164  
   165  	return errors.Trace(sendBundleContent(w, r, charmArchivePath, sender))
   166  }
   167  
   168  // manifestSender sends a JSON-encoded response to the client including the
   169  // list of files contained in the charm bundle.
   170  func (h *charmsHandler) manifestSender(w http.ResponseWriter, r *http.Request, bundle *charm.CharmArchive) error {
   171  	manifest, err := bundle.ArchiveMembers()
   172  	if err != nil {
   173  		return errors.Annotatef(err, "unable to read manifest in %q", bundle.Path)
   174  	}
   175  	return errors.Trace(sendStatusAndJSON(w, http.StatusOK, &params.CharmsResponse{
   176  		Files: manifest.SortedValues(),
   177  	}))
   178  }
   179  
   180  // archiveEntrySender returns a bundleContentSenderFunc which is responsible
   181  // for sending the contents of filePath included in the given charm bundle. If
   182  // filePath does not identify a file or a symlink, a 403 forbidden error is
   183  // returned. If serveIcon is true, then the charm icon.svg file is sent, or a
   184  // default icon if that file is not included in the charm.
   185  func (h *charmsHandler) archiveEntrySender(filePath string, serveIcon bool) bundleContentSenderFunc {
   186  	return func(w http.ResponseWriter, r *http.Request, bundle *charm.CharmArchive) error {
   187  		contents, err := common.CharmArchiveEntry(bundle.Path, filePath, serveIcon)
   188  		if err != nil {
   189  			return errors.Trace(err)
   190  		}
   191  		ctype := mime.TypeByExtension(filepath.Ext(filePath))
   192  		if ctype != "" {
   193  			// Older mime.types may map .js to x-javascript.
   194  			// Map it to javascript for consistency.
   195  			if ctype == params.ContentTypeXJS {
   196  				ctype = params.ContentTypeJS
   197  			}
   198  			w.Header().Set("Content-Type", ctype)
   199  		}
   200  		w.Header().Set("Content-Length", strconv.Itoa(len(contents)))
   201  		w.WriteHeader(http.StatusOK)
   202  		_, _ = io.Copy(w, bytes.NewReader(contents))
   203  		return nil
   204  	}
   205  }
   206  
   207  // archiveSender is a bundleContentSenderFunc which is responsible for sending
   208  // the contents of the given charm bundle.
   209  func (h *charmsHandler) archiveSender(w http.ResponseWriter, r *http.Request, bundle *charm.CharmArchive) error {
   210  	// Note that http.ServeFile's error responses are not our standard JSON
   211  	// responses (they are the usual textual error messages as produced
   212  	// by http.Error), but there's not a great deal we can do about that,
   213  	// except accept non-JSON error responses in the client, because
   214  	// http.ServeFile does not provide a way of customizing its
   215  	// error responses.
   216  	http.ServeFile(w, r, bundle.Path)
   217  	return nil
   218  }
   219  
   220  // processPost handles a charm upload POST request after authentication.
   221  func (h *charmsHandler) processPost(r *http.Request, st *state.State) (string, error) {
   222  	query := r.URL.Query()
   223  	schema := query.Get("schema")
   224  	if schema == "" {
   225  		schema = "local"
   226  	}
   227  	if schema != "local" {
   228  		// charmhub charms may only be uploaded into models
   229  		// which are being imported during model migrations.
   230  		// There's currently no other time where it makes sense
   231  		// to accept repository charms through this endpoint.
   232  		if isImporting, err := modelIsImporting(st); err != nil {
   233  			return "", errors.Trace(err)
   234  		} else if !isImporting {
   235  			return "", errors.New("charms may only be uploaded during model migration import")
   236  		}
   237  	}
   238  
   239  	charmFileName, err := writeCharmToTempFile(r.Body)
   240  	if err != nil {
   241  		return "", errors.Trace(err)
   242  	}
   243  	defer os.Remove(charmFileName)
   244  
   245  	err = h.processUploadedArchive(charmFileName)
   246  	if err != nil {
   247  		return "", err
   248  	}
   249  	archive, err := charm.ReadCharmArchive(charmFileName)
   250  	if err != nil {
   251  		return "", errors.BadRequestf("invalid charm archive: %v", err)
   252  	}
   253  
   254  	// Use the name from the query string. If we're dealing with an older client
   255  	// then this won't be sent, instead fallback to the archive metadata name.
   256  	name := query.Get("name")
   257  	if name == "" {
   258  		name = archive.Meta().Name
   259  	}
   260  	if err := charm.ValidateName(name); err != nil {
   261  		return "", errors.NewBadRequest(err, "")
   262  	}
   263  
   264  	var revision int
   265  	if revisionStr := query.Get("revision"); revisionStr != "" {
   266  		revision, err = strconv.Atoi(revisionStr)
   267  		if err != nil {
   268  			return "", errors.NewBadRequest(errors.NewNotValid(err, "revision"), "")
   269  		}
   270  	} else {
   271  		revision = archive.Revision()
   272  	}
   273  
   274  	// We got it, now let's reserve a charm URL for it in state.
   275  	curlStr := curlString(schema, query.Get("arch"), name, query.Get("series"), revision)
   276  
   277  	switch charm.Schema(schema) {
   278  	case charm.Local:
   279  		curl, err := st.PrepareLocalCharmUpload(curlStr)
   280  		if err != nil {
   281  			return "", errors.Trace(err)
   282  		}
   283  		curlStr = curl.String()
   284  
   285  	case charm.CharmHub:
   286  		if _, err := st.PrepareCharmUpload(curlStr); err != nil {
   287  			return "", errors.Trace(err)
   288  		}
   289  
   290  	default:
   291  		return "", errors.Errorf("unsupported schema %q", schema)
   292  	}
   293  
   294  	err = RepackageAndUploadCharm(st, archive, curlStr, revision)
   295  	if err != nil {
   296  		return "", errors.Trace(err)
   297  	}
   298  	return curlStr, nil
   299  }
   300  
   301  // curlString takes the constituent parts of a charm url and renders the url as a string.
   302  // This is required since, to support migrations from legacy controllers, we need to support
   303  // charm urls with series since controllers do not allow migrations to mutate charm urls during
   304  // migration.
   305  //
   306  // This is the only place in Juju 4 where series in a charm url needs to be processed. As such,
   307  // instead of dragging support for series with us into 4.0, in this one place we string-hack the
   308  // url
   309  func curlString(schema, arch, name, series string, revision int) string {
   310  	if series == "" {
   311  		curl := &charm.URL{
   312  			Schema:       schema,
   313  			Architecture: arch,
   314  			Name:         name,
   315  			Revision:     revision,
   316  		}
   317  		return curl.String()
   318  	}
   319  	var curl string
   320  	if arch == "" {
   321  		curl = fmt.Sprintf("%s:%s/%s", schema, series, name)
   322  	} else {
   323  		curl = fmt.Sprintf("%s:%s/%s/%s", schema, arch, series, name)
   324  	}
   325  	if revision != -1 {
   326  		curl = fmt.Sprintf("%s-%d", curl, revision)
   327  	}
   328  	return curl
   329  }
   330  
   331  // processUploadedArchive opens the given charm archive from path,
   332  // inspects it to see if it has all files at the root of the archive
   333  // or it has subdirs. It repackages the archive so it has all the
   334  // files at the root dir, if necessary, replacing the original archive
   335  // at path.
   336  func (h *charmsHandler) processUploadedArchive(path string) error {
   337  	// Open the archive as a zip.
   338  	f, err := os.OpenFile(path, os.O_RDWR, 0644)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	defer f.Close()
   343  	fi, err := f.Stat()
   344  	if err != nil {
   345  		return err
   346  	}
   347  	zipr, err := zip.NewReader(f, fi.Size())
   348  	if err != nil {
   349  		return errors.Annotate(err, "cannot open charm archive")
   350  	}
   351  
   352  	// Find out the root dir prefix from the archive.
   353  	rootDir, err := h.findArchiveRootDir(zipr)
   354  	if err != nil {
   355  		return errors.Annotate(err, "cannot read charm archive")
   356  	}
   357  	if rootDir == "." {
   358  		// Normal charm, just use charm.ReadCharmArchive).
   359  		return nil
   360  	}
   361  
   362  	// There is one or more subdirs, so we need extract it to a temp
   363  	// dir and then read it as a charm dir.
   364  	tempDir, err := os.MkdirTemp("", "charm-extract")
   365  	if err != nil {
   366  		return errors.Annotate(err, "cannot create temp directory")
   367  	}
   368  	defer os.RemoveAll(tempDir)
   369  	if err := ziputil.Extract(zipr, tempDir, rootDir); err != nil {
   370  		return errors.Annotate(err, "cannot extract charm archive")
   371  	}
   372  	dir, err := charm.ReadCharmDir(tempDir)
   373  	if err != nil {
   374  		return errors.Annotate(err, "cannot read extracted archive")
   375  	}
   376  
   377  	// Now repackage the dir as a bundle at the original path.
   378  	if err := f.Truncate(0); err != nil {
   379  		return err
   380  	}
   381  	if err := dir.ArchiveTo(f); err != nil {
   382  		return err
   383  	}
   384  	return nil
   385  }
   386  
   387  // findArchiveRootDir scans a zip archive and returns the rootDir of
   388  // the archive, the one containing metadata.yaml, config.yaml and
   389  // revision files, or an error if the archive appears invalid.
   390  func (h *charmsHandler) findArchiveRootDir(zipr *zip.Reader) (string, error) {
   391  	paths, err := ziputil.Find(zipr, "metadata.yaml")
   392  	if err != nil {
   393  		return "", err
   394  	}
   395  	switch len(paths) {
   396  	case 0:
   397  		return "", errors.Errorf("invalid charm archive: missing metadata.yaml")
   398  	case 1:
   399  	default:
   400  		sort.Sort(byDepth(paths))
   401  		if depth(paths[0]) == depth(paths[1]) {
   402  			return "", errors.Errorf("invalid charm archive: ambiguous root directory")
   403  		}
   404  	}
   405  	return filepath.Dir(paths[0]), nil
   406  }
   407  
   408  func depth(path string) int {
   409  	return strings.Count(path, "/")
   410  }
   411  
   412  type byDepth []string
   413  
   414  func (d byDepth) Len() int           { return len(d) }
   415  func (d byDepth) Swap(i, j int)      { d[i], d[j] = d[j], d[i] }
   416  func (d byDepth) Less(i, j int) bool { return depth(d[i]) < depth(d[j]) }
   417  
   418  // RepackageAndUploadCharm expands the given charm archive to a
   419  // temporary directory, repackages it with the given curl's revision,
   420  // then uploads it to storage, and finally updates the state.
   421  func RepackageAndUploadCharm(st *state.State, archive *charm.CharmArchive, curl string, charmRevision int) error {
   422  	// Create a temp dir to contain the extracted charm dir.
   423  	tempDir, err := os.MkdirTemp("", "charm-download")
   424  	if err != nil {
   425  		return errors.Annotate(err, "cannot create temp directory")
   426  	}
   427  	defer os.RemoveAll(tempDir)
   428  	extractPath := filepath.Join(tempDir, "extracted")
   429  
   430  	// Expand and repack it with the specified revision
   431  	archive.SetRevision(charmRevision)
   432  	if err := archive.ExpandTo(extractPath); err != nil {
   433  		return errors.Annotate(err, "cannot extract uploaded charm")
   434  	}
   435  
   436  	charmDir, err := charm.ReadCharmDir(extractPath)
   437  	if err != nil {
   438  		return errors.Annotate(err, "cannot read extracted charm")
   439  	}
   440  
   441  	// Try to get the version details here.
   442  	// read just the first line of the file.
   443  	var version string
   444  	versionPath := filepath.Join(extractPath, "version")
   445  	if file, err := os.Open(versionPath); err == nil {
   446  		version, err = charm.ReadVersion(file)
   447  		_ = file.Close()
   448  		if err != nil {
   449  			return errors.Trace(err)
   450  		}
   451  	} else if !os.IsNotExist(err) {
   452  		return errors.Annotate(err, "cannot open version file")
   453  	}
   454  
   455  	// Bundle the charm and calculate its sha256 hash at the same time.
   456  	var repackagedArchive bytes.Buffer
   457  	hash := sha256.New()
   458  	err = charmDir.ArchiveTo(io.MultiWriter(hash, &repackagedArchive))
   459  	if err != nil {
   460  		return errors.Annotate(err, "cannot repackage uploaded charm")
   461  	}
   462  	bundleSHA256 := hex.EncodeToString(hash.Sum(nil))
   463  
   464  	// Now we need to repackage it with the reserved URL, upload it to
   465  	// provider storage and update the state.
   466  	charmStorage := services.NewCharmStorage(services.CharmStorageConfig{
   467  		Logger:       logger,
   468  		StateBackend: storageStateShim{st},
   469  		StorageFactory: func(modelUUID string) services.Storage {
   470  			return storage.NewStorage(modelUUID, st.MongoSession())
   471  		},
   472  	})
   473  
   474  	return charmStorage.Store(curl, downloader.DownloadedCharm{
   475  		Charm:        archive,
   476  		CharmData:    &repackagedArchive,
   477  		CharmVersion: version,
   478  		Size:         int64(repackagedArchive.Len()),
   479  		SHA256:       bundleSHA256,
   480  		LXDProfile:   charmDir.LXDProfile(),
   481  	})
   482  }
   483  
   484  type storageStateShim struct {
   485  	*state.State
   486  }
   487  
   488  func (s storageStateShim) UpdateUploadedCharm(info state.CharmInfo) (services.UploadedCharm, error) {
   489  	ch, err := s.State.UpdateUploadedCharm(info)
   490  	return ch, err
   491  }
   492  
   493  func (s storageStateShim) PrepareCharmUpload(curl string) (services.UploadedCharm, error) {
   494  	ch, err := s.State.PrepareCharmUpload(curl)
   495  	return ch, err
   496  }
   497  
   498  // processGet handles a charm file GET request after authentication.
   499  // It returns the bundle path, the requested file path (if any), whether the
   500  // default charm icon has been requested and an error.
   501  func (h *charmsHandler) processGet(r *http.Request, st *state.State) (
   502  	archivePath string,
   503  	fileArg string,
   504  	serveIcon bool,
   505  	err error,
   506  ) {
   507  	errRet := func(err error) (string, string, bool, error) {
   508  		return "", "", false, err
   509  	}
   510  
   511  	query := r.URL.Query()
   512  
   513  	// Retrieve and validate query parameters.
   514  	curl := query.Get("url")
   515  	if curl == "" {
   516  		return errRet(errors.Errorf("expected url=CharmURL query argument"))
   517  	}
   518  	fileArg = query.Get("file")
   519  	if fileArg != "" {
   520  		fileArg = path.Clean(fileArg)
   521  	} else if query.Get("icon") == "1" {
   522  		serveIcon = true
   523  		fileArg = "icon.svg"
   524  	}
   525  
   526  	store := storage.NewStorage(st.ModelUUID(), st.MongoSession())
   527  	// Use the storage to retrieve and save the charm archive.
   528  	ch, err := st.Charm(curl)
   529  	if err != nil {
   530  		return errRet(errors.Annotate(err, "cannot get charm from state"))
   531  	}
   532  	// Check if the charm is still pending to be downloaded and return back
   533  	// a suitable error.
   534  	if !ch.IsUploaded() {
   535  		return errRet(errors.NewNotYetAvailable(nil, curl))
   536  	}
   537  
   538  	archivePath, err = common.ReadCharmFromStorage(store, h.dataDir, ch.StoragePath())
   539  	if err != nil {
   540  		return errRet(errors.Annotatef(err, "cannot read charm %q from storage", curl))
   541  	}
   542  	return archivePath, fileArg, serveIcon, nil
   543  }
   544  
   545  // sendJSONError sends a JSON-encoded error response.  Note the
   546  // difference from the error response sent by the sendError function -
   547  // the error is encoded in the Error field as a string, not an Error
   548  // object.
   549  func sendJSONError(w http.ResponseWriter, req *http.Request, err error) error {
   550  	if errors.IsNotYetAvailable(err) {
   551  		// This error is typically raised when trying to fetch the blob
   552  		// contents for a charm which is still pending to be downloaded.
   553  		//
   554  		// We should log this at debug level to avoid unnecessary noise
   555  		// in the logs.
   556  		logger.Debugf("returning error from %s %s: %s", req.Method, req.URL, errors.Details(err))
   557  	} else {
   558  		logger.Errorf("returning error from %s %s: %s", req.Method, req.URL, errors.Details(err))
   559  	}
   560  
   561  	perr, status := apiservererrors.ServerErrorAndStatus(err)
   562  	return errors.Trace(sendStatusAndJSON(w, status, &params.CharmsResponse{
   563  		Error:     perr.Message,
   564  		ErrorCode: perr.Code,
   565  		ErrorInfo: perr.Info,
   566  	}))
   567  }
   568  
   569  // sendBundleContent uses the given bundleContentSenderFunc to send a
   570  // response related to the charm archive located in the given
   571  // archivePath.
   572  func sendBundleContent(
   573  	w http.ResponseWriter,
   574  	r *http.Request,
   575  	archivePath string,
   576  	sender bundleContentSenderFunc,
   577  ) error {
   578  	logger.Child("charmhttp").Tracef("sendBundleContent %q", archivePath)
   579  	bundle, err := charm.ReadCharmArchive(archivePath)
   580  	if err != nil {
   581  		return errors.Annotatef(err, "unable to read archive in %q", archivePath)
   582  	}
   583  	// The bundleContentSenderFunc will set up and send an appropriate response.
   584  	if err := sender(w, r, bundle); err != nil {
   585  		return errors.Trace(err)
   586  	}
   587  	return nil
   588  }
   589  
   590  func writeCharmToTempFile(r io.Reader) (string, error) {
   591  	tempFile, err := os.CreateTemp("", "charm")
   592  	if err != nil {
   593  		return "", errors.Annotate(err, "creating temp file")
   594  	}
   595  	defer tempFile.Close()
   596  
   597  	if _, err := io.Copy(tempFile, r); err != nil {
   598  		return "", errors.Annotate(err, "processing upload")
   599  	}
   600  	return tempFile.Name(), nil
   601  }
   602  
   603  func modelIsImporting(st *state.State) (bool, error) {
   604  	model, err := st.Model()
   605  	if err != nil {
   606  		return false, errors.Trace(err)
   607  	}
   608  	return model.MigrationMode() == state.MigrationModeImporting, nil
   609  }
   610  
   611  func emitUnsupportedMethodErr(method string) error {
   612  	return errors.MethodNotAllowedf("unsupported method: %q", method)
   613  }