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

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charmdownloader
     5  
     6  import (
     7  	"sync"
     8  
     9  	"github.com/juju/charm/v12"
    10  	"github.com/juju/clock"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/http/v2"
    13  	"github.com/juju/loggo"
    14  	"github.com/juju/names/v5"
    15  
    16  	apiservererrors "github.com/juju/juju/apiserver/errors"
    17  	"github.com/juju/juju/apiserver/facades/client/charms/services"
    18  	"github.com/juju/juju/rpc/params"
    19  	"github.com/juju/juju/state/watcher"
    20  )
    21  
    22  var logger = loggo.GetLogger("juju.apiserver.charmdownloader")
    23  
    24  // CharmDownloaderAPI implements an API for watching the charms collection for
    25  // any entries that have not been yet downloaded to the blobstore and for
    26  // triggering their download.
    27  type CharmDownloaderAPI struct {
    28  	authChecker        AuthChecker
    29  	resourcesBackend   ResourcesBackend
    30  	stateBackend       StateBackend
    31  	modelBackend       ModelBackend
    32  	clock              clock.Clock
    33  	charmhubHTTPClient http.HTTPClient
    34  
    35  	newStorage    func(modelUUID string) services.Storage
    36  	newDownloader func(services.CharmDownloaderConfig) (Downloader, error)
    37  
    38  	mu         sync.Mutex
    39  	downloader Downloader
    40  }
    41  
    42  // newAPI is invoked both by the facade constructor and from our tests. It
    43  // allows us to pass interfaces for the facade's dependencies.
    44  func newAPI(
    45  	authChecker AuthChecker,
    46  	resourcesBackend ResourcesBackend,
    47  	stateBackend StateBackend,
    48  	modelBackend ModelBackend,
    49  	clk clock.Clock,
    50  	httpClient http.HTTPClient,
    51  	newStorage func(string) services.Storage,
    52  	newDownloader func(services.CharmDownloaderConfig) (Downloader, error),
    53  ) *CharmDownloaderAPI {
    54  	return &CharmDownloaderAPI{
    55  		authChecker:        authChecker,
    56  		resourcesBackend:   resourcesBackend,
    57  		stateBackend:       stateBackend,
    58  		modelBackend:       modelBackend,
    59  		clock:              clk,
    60  		charmhubHTTPClient: httpClient,
    61  		newStorage:         newStorage,
    62  		newDownloader:      newDownloader,
    63  	}
    64  }
    65  
    66  // WatchApplicationsWithPendingCharms registers and returns a watcher instance
    67  // that reports the ID of applications that reference a charm which has not yet
    68  // been downloaded.
    69  func (a *CharmDownloaderAPI) WatchApplicationsWithPendingCharms() (params.StringsWatchResult, error) {
    70  	if !a.authChecker.AuthController() {
    71  		return params.StringsWatchResult{}, apiservererrors.ErrPerm
    72  	}
    73  
    74  	w := a.stateBackend.WatchApplicationsWithPendingCharms()
    75  	if initialState, ok := <-w.Changes(); ok {
    76  		return params.StringsWatchResult{
    77  			StringsWatcherId: a.resourcesBackend.Register(w),
    78  			Changes:          initialState,
    79  		}, nil
    80  	}
    81  
    82  	return params.StringsWatchResult{}, watcher.EnsureErr(w)
    83  }
    84  
    85  // DownloadApplicationCharms iterates the list of provided applications and
    86  // downloads any referenced charms that have not yet been persisted to the
    87  // blob store.
    88  func (a *CharmDownloaderAPI) DownloadApplicationCharms(args params.Entities) (params.ErrorResults, error) {
    89  	if !a.authChecker.AuthController() {
    90  		return params.ErrorResults{}, apiservererrors.ErrPerm
    91  	}
    92  
    93  	res := params.ErrorResults{Results: make([]params.ErrorResult, len(args.Entities))}
    94  	for i, arg := range args.Entities {
    95  		app, err := names.ParseApplicationTag(arg.Tag)
    96  		if err != nil {
    97  			res.Results[i].Error = apiservererrors.ServerError(err)
    98  			continue
    99  		}
   100  		res.Results[i].Error = apiservererrors.ServerError(a.downloadApplicationCharm(app))
   101  	}
   102  	return res, nil
   103  }
   104  
   105  func (a *CharmDownloaderAPI) downloadApplicationCharm(appTag names.ApplicationTag) error {
   106  	app, err := a.stateBackend.Application(appTag.Name)
   107  	if err != nil {
   108  		return errors.Trace(err)
   109  	}
   110  
   111  	// In the case of deploying multiple applications utilizing the
   112  	// same charm, keep going to allow DownloadAndStore to return
   113  	// the correct origin to be saved below. The charm will not
   114  	// actually be downloaded more than once. The method will just
   115  	// provide the correct origin. Necessary for deploying resources
   116  	// and refreshing charms.
   117  	if !app.CharmPendingToBeDownloaded() {
   118  		return nil // nothing to do
   119  	}
   120  
   121  	resolvedOrigin := app.CharmOrigin()
   122  	if resolvedOrigin == nil {
   123  		return errors.NotFoundf("download charm for application %q; resolved origin", appTag.Name)
   124  	}
   125  
   126  	downloader, err := a.getDownloader()
   127  	if err != nil {
   128  		return errors.Trace(err)
   129  	}
   130  
   131  	pendingCharm, force, err := app.Charm()
   132  	if err != nil {
   133  		return errors.Trace(err)
   134  	}
   135  	pendingCharmURL, err := charm.ParseURL(pendingCharm.URL())
   136  	if err != nil {
   137  		return errors.Trace(err)
   138  	}
   139  
   140  	logger.Infof("downloading charm %q", pendingCharmURL)
   141  	downloadedOrigin, err := downloader.DownloadAndStore(pendingCharmURL, *resolvedOrigin, force)
   142  	if err != nil {
   143  		return errors.Annotatef(err, "cannot download and store charm %q", pendingCharmURL)
   144  	}
   145  	return errors.Trace(app.SetDownloadedIDAndHash(downloadedOrigin.ID, downloadedOrigin.Hash))
   146  }
   147  
   148  func (a *CharmDownloaderAPI) getDownloader() (Downloader, error) {
   149  	a.mu.Lock()
   150  	defer a.mu.Unlock()
   151  
   152  	if a.downloader != nil {
   153  		return a.downloader, nil
   154  	}
   155  
   156  	downloader, err := a.newDownloader(services.CharmDownloaderConfig{
   157  		Logger:             logger,
   158  		CharmhubHTTPClient: a.charmhubHTTPClient,
   159  		StorageFactory:     a.newStorage,
   160  		StateBackend:       a.stateBackend,
   161  		ModelBackend:       a.modelBackend,
   162  	})
   163  
   164  	if err != nil {
   165  		return nil, errors.Trace(err)
   166  	}
   167  
   168  	a.downloader = downloader
   169  	return downloader, nil
   170  }