github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/resources/facade.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package resources 5 6 import ( 7 "github.com/juju/charm/v12" 8 charmresource "github.com/juju/charm/v12/resource" 9 "github.com/juju/errors" 10 "github.com/juju/loggo" 11 "github.com/juju/names/v5" 12 13 apiresources "github.com/juju/juju/api/client/resources" 14 apiservererrors "github.com/juju/juju/apiserver/errors" 15 "github.com/juju/juju/apiserver/facade" 16 "github.com/juju/juju/apiserver/facades/client/charms" 17 "github.com/juju/juju/charmhub" 18 corecharm "github.com/juju/juju/core/charm" 19 "github.com/juju/juju/core/charm/repository" 20 corelogger "github.com/juju/juju/core/logger" 21 "github.com/juju/juju/core/resources" 22 "github.com/juju/juju/rpc/params" 23 ) 24 25 var logger = loggo.GetLogger("juju.apiserver.resources") 26 27 // Backend is the functionality of Juju's state needed for the resources API. 28 type Backend interface { 29 // ListResources returns the resources for the given application. 30 ListResources(application string) (resources.ApplicationResources, error) 31 32 // AddPendingResource adds the resource to the data backend in a 33 // "pending" state. It will stay pending (and unavailable) until 34 // it is resolved. The returned ID is used to identify the pending 35 // resources when resolving it. 36 AddPendingResource(applicationID, userID string, chRes charmresource.Resource) (string, error) 37 } 38 39 // API is the public API facade for resources. 40 type API struct { 41 // backend is the data source for the facade. 42 backend Backend 43 44 factory func(*charm.URL) (NewCharmRepository, error) 45 } 46 47 // NewFacade creates a public API facade for resources. It is 48 // used for API registration. 49 func NewFacade(ctx facade.Context) (*API, error) { 50 authorizer := ctx.Auth() 51 if !authorizer.AuthClient() { 52 return nil, apiservererrors.ErrPerm 53 } 54 55 st := ctx.State() 56 rst := st.Resources() 57 58 m, err := st.Model() 59 if err != nil { 60 return nil, errors.Trace(err) 61 } 62 modelCfg, err := m.Config() 63 if err != nil { 64 return nil, errors.Trace(err) 65 } 66 67 factory := func(curl *charm.URL) (NewCharmRepository, error) { 68 schema := curl.Schema 69 switch { 70 case charm.CharmHub.Matches(schema): 71 chURL, _ := modelCfg.CharmHubURL() 72 chClient, err := charmhub.NewClient(charmhub.Config{ 73 URL: chURL, 74 HTTPClient: ctx.HTTPClient(facade.CharmhubHTTPClient), 75 Logger: logger, 76 }) 77 if err != nil { 78 return nil, errors.Trace(err) 79 } 80 return repository.NewCharmHubRepository(logger.ChildWithLabels("charmhub", corelogger.CHARMHUB), chClient), nil 81 82 case charm.Local.Matches(schema): 83 return &localClient{}, nil 84 85 default: 86 return nil, errors.Errorf("unrecognized charm schema %q", curl.Schema) 87 } 88 } 89 90 f, err := NewResourcesAPI(rst, factory) 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 return f, nil 95 } 96 97 // NewResourcesAPI returns a new resources API facade. 98 func NewResourcesAPI(backend Backend, factory func(*charm.URL) (NewCharmRepository, error)) (*API, error) { 99 if backend == nil { 100 return nil, errors.Errorf("missing data backend") 101 } 102 if factory == nil { 103 // Technically this only matters for one code path through 104 // AddPendingResources(). However, that functionality should be 105 // provided. So we indicate the problem here instead of later 106 // in the specific place where it actually matters. 107 return nil, errors.Errorf("missing factory for new repository") 108 } 109 110 f := &API{ 111 backend: backend, 112 factory: factory, 113 } 114 return f, nil 115 } 116 117 // ListResources returns the list of resources for the given application. 118 func (a *API) ListResources(args params.ListResourcesArgs) (params.ResourcesResults, error) { 119 var r params.ResourcesResults 120 r.Results = make([]params.ResourcesResult, len(args.Entities)) 121 122 for i, e := range args.Entities { 123 logger.Tracef("Listing resources for %q", e.Tag) 124 tag, apierr := parseApplicationTag(e.Tag) 125 if apierr != nil { 126 r.Results[i] = params.ResourcesResult{ 127 ErrorResult: params.ErrorResult{ 128 Error: apierr, 129 }, 130 } 131 continue 132 } 133 134 svcRes, err := a.backend.ListResources(tag.Id()) 135 if err != nil { 136 r.Results[i] = errorResult(err) 137 continue 138 } 139 140 r.Results[i] = apiresources.ApplicationResources2APIResult(svcRes) 141 } 142 return r, nil 143 } 144 145 // AddPendingResources adds the provided resources (info) to the Juju 146 // model in a pending state, meaning they are not available until 147 // resolved. Handles CharmHub and Local charms. 148 func (a *API) AddPendingResources(args params.AddPendingResourcesArgsV2) (params.AddPendingResourcesResult, error) { 149 var result params.AddPendingResourcesResult 150 151 tag, apiErr := parseApplicationTag(args.Tag) 152 if apiErr != nil { 153 result.Error = apiErr 154 return result, nil 155 } 156 applicationID := tag.Id() 157 158 requestedOrigin, err := charms.ConvertParamsOrigin(args.CharmOrigin) 159 if err != nil { 160 result.Error = apiservererrors.ServerError(err) 161 return result, nil 162 } 163 ids, err := a.addPendingResources(applicationID, args.URL, requestedOrigin, args.Resources) 164 if err != nil { 165 result.Error = apiservererrors.ServerError(err) 166 return result, nil 167 } 168 result.PendingIDs = ids 169 return result, nil 170 } 171 172 func (a *API) addPendingResources(appName, chRef string, origin corecharm.Origin, apiResources []params.CharmResource) ([]string, error) { 173 var resources []charmresource.Resource 174 for _, apiRes := range apiResources { 175 res, err := apiresources.API2CharmResource(apiRes) 176 if err != nil { 177 return nil, errors.Annotatef(err, "bad resource info for %q", apiRes.Name) 178 } 179 resources = append(resources, res) 180 } 181 182 if chRef != "" { 183 cURL, err := charm.ParseURL(chRef) 184 if err != nil { 185 return nil, errors.Trace(err) 186 } 187 id := corecharm.CharmID{ 188 URL: cURL, 189 Origin: origin, 190 } 191 repository, err := a.factory(id.URL) 192 if err != nil { 193 return nil, errors.Trace(err) 194 } 195 resources, err = repository.ResolveResources(resources, id) 196 if err != nil { 197 return nil, errors.Trace(err) 198 } 199 } 200 201 var ids []string 202 for _, res := range resources { 203 pendingID, err := a.addPendingResource(appName, res) 204 if err != nil { 205 // We don't bother aggregating errors since a partial 206 // completion is disruptive and a retry of this endpoint 207 // is not expensive. 208 return nil, err 209 } 210 ids = append(ids, pendingID) 211 } 212 return ids, nil 213 } 214 215 func (a *API) addPendingResource(appName string, chRes charmresource.Resource) (pendingID string, err error) { 216 userID := "" 217 pendingID, err = a.backend.AddPendingResource(appName, userID, chRes) 218 if err != nil { 219 return "", errors.Annotatef(err, "while adding pending resource info for %q", chRes.Name) 220 } 221 return pendingID, nil 222 } 223 224 func parseApplicationTag(tagStr string) (names.ApplicationTag, *params.Error) { // note the concrete error type 225 ApplicationTag, err := names.ParseApplicationTag(tagStr) 226 if err != nil { 227 return ApplicationTag, ¶ms.Error{ 228 Message: err.Error(), 229 Code: params.CodeBadRequest, 230 } 231 } 232 return ApplicationTag, nil 233 } 234 235 func errorResult(err error) params.ResourcesResult { 236 return params.ResourcesResult{ 237 ErrorResult: params.ErrorResult{ 238 Error: apiservererrors.ServerError(err), 239 }, 240 } 241 }