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 }