github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/charms/client.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package charms 5 6 import ( 7 "net/http" 8 "net/url" 9 "sync" 10 "time" 11 12 "github.com/juju/charm/v12" 13 "github.com/juju/collections/set" 14 "github.com/juju/collections/transform" 15 "github.com/juju/errors" 16 "github.com/juju/loggo" 17 "github.com/juju/names/v5" 18 19 apiresources "github.com/juju/juju/api/client/resources" 20 commoncharm "github.com/juju/juju/api/common/charm" 21 "github.com/juju/juju/apiserver/authentication" 22 charmscommon "github.com/juju/juju/apiserver/common/charms" 23 apiservererrors "github.com/juju/juju/apiserver/errors" 24 "github.com/juju/juju/apiserver/facade" 25 charmsinterfaces "github.com/juju/juju/apiserver/facades/client/charms/interfaces" 26 "github.com/juju/juju/apiserver/facades/client/charms/services" 27 "github.com/juju/juju/core/arch" 28 "github.com/juju/juju/core/base" 29 corecharm "github.com/juju/juju/core/charm" 30 "github.com/juju/juju/core/constraints" 31 "github.com/juju/juju/core/permission" 32 "github.com/juju/juju/rpc/params" 33 "github.com/juju/juju/state" 34 ) 35 36 var logger = loggo.GetLogger("juju.apiserver.charms") 37 38 // APIv7 provides the Charms API facade for version 7. 39 // v7 guarantees SupportedBases will be provided in ResolveCharms 40 type APIv7 struct { 41 *API 42 } 43 44 // APIv6 provides the Charms API facade for version 6. 45 // It removes the AddCharmWithAuthorization function, as 46 // we no longer support macaroons. 47 type APIv6 struct { 48 *APIv7 49 } 50 51 // APIv5 provides the Charms API facade for version 5. 52 type APIv5 struct { 53 *APIv6 54 } 55 56 // API implements the charms interface and is the concrete 57 // implementation of the API end point. 58 type API struct { 59 charmInfoAPI *charmscommon.CharmInfoAPI 60 authorizer facade.Authorizer 61 backendState charmsinterfaces.BackendState 62 backendModel charmsinterfaces.BackendModel 63 charmhubHTTPClient facade.HTTPClient 64 65 tag names.ModelTag 66 requestRecorder facade.RequestRecorder 67 68 newStorage func(modelUUID string) services.Storage 69 newDownloader func(services.CharmDownloaderConfig) (charmsinterfaces.Downloader, error) 70 newRepoFactory func(services.CharmRepoFactoryConfig) corecharm.RepositoryFactory 71 72 mu sync.Mutex 73 repoFactory corecharm.RepositoryFactory 74 } 75 76 // CharmInfo returns information about the requested charm. 77 func (a *API) CharmInfo(args params.CharmURL) (params.Charm, error) { 78 return a.charmInfoAPI.CharmInfo(args) 79 } 80 81 func (a *API) checkCanRead() error { 82 err := a.authorizer.HasPermission(permission.ReadAccess, a.tag) 83 return err 84 } 85 86 func (a *API) checkCanWrite() error { 87 err := a.authorizer.HasPermission(permission.SuperuserAccess, a.backendState.ControllerTag()) 88 if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) { 89 return errors.Trace(err) 90 } 91 92 if err == nil { 93 return nil 94 } 95 96 return a.authorizer.HasPermission(permission.WriteAccess, a.tag) 97 } 98 99 // NewCharmsAPI is only used for testing. 100 // TODO (stickupkid): We should use the latest NewFacadeV4 to better exercise 101 // the API. 102 func NewCharmsAPI( 103 authorizer facade.Authorizer, 104 st charmsinterfaces.BackendState, 105 m charmsinterfaces.BackendModel, 106 newStorage func(modelUUID string) services.Storage, 107 repoFactory corecharm.RepositoryFactory, 108 newDownloader func(cfg services.CharmDownloaderConfig) (charmsinterfaces.Downloader, error), 109 ) (*API, error) { 110 return &API{ 111 authorizer: authorizer, 112 backendState: st, 113 backendModel: m, 114 newStorage: newStorage, 115 newDownloader: newDownloader, 116 tag: m.ModelTag(), 117 requestRecorder: noopRequestRecorder{}, 118 repoFactory: repoFactory, 119 }, nil 120 } 121 122 // List returns a list of charm URLs currently in the state. 123 // If supplied parameter contains any names, the result will 124 // be filtered to return only the charms with supplied names. 125 func (a *API) List(args params.CharmsList) (params.CharmsListResult, error) { 126 logger.Tracef("List %+v", args) 127 if err := a.checkCanRead(); err != nil { 128 return params.CharmsListResult{}, errors.Trace(err) 129 } 130 131 charms, err := a.backendState.AllCharms() 132 if err != nil { 133 return params.CharmsListResult{}, errors.Annotatef(err, " listing charms ") 134 } 135 136 charmNames := set.NewStrings(args.Names...) 137 checkName := !charmNames.IsEmpty() 138 charmURLs := []string{} 139 for _, aCharm := range charms { 140 if checkName { 141 charmURL, err := charm.ParseURL(aCharm.URL()) 142 if err != nil { 143 return params.CharmsListResult{}, errors.Trace(err) 144 } 145 if !charmNames.Contains(charmURL.Name) { 146 continue 147 } 148 } 149 charmURLs = append(charmURLs, aCharm.URL()) 150 } 151 return params.CharmsListResult{CharmURLs: charmURLs}, nil 152 } 153 154 // GetDownloadInfos attempts to get the bundle corresponding to the charm url 155 // and origin. 156 func (a *API) GetDownloadInfos(args params.CharmURLAndOrigins) (params.DownloadInfoResults, error) { 157 logger.Tracef("GetDownloadInfos %+v", args) 158 159 results := params.DownloadInfoResults{ 160 Results: make([]params.DownloadInfoResult, len(args.Entities)), 161 } 162 for i, arg := range args.Entities { 163 result, err := a.getDownloadInfo(arg) 164 if err != nil { 165 return params.DownloadInfoResults{}, errors.Trace(err) 166 } 167 results.Results[i] = result 168 } 169 return results, nil 170 } 171 172 func (a *API) getDownloadInfo(arg params.CharmURLAndOrigin) (params.DownloadInfoResult, error) { 173 if err := a.checkCanRead(); err != nil { 174 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 175 } 176 177 curl, err := charm.ParseURL(arg.CharmURL) 178 if err != nil { 179 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 180 } 181 182 defaultArch, err := a.getDefaultArch() 183 if err != nil { 184 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 185 } 186 187 charmOrigin, err := normalizeCharmOrigin(arg.Origin, defaultArch) 188 if err != nil { 189 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 190 } 191 192 repo, err := a.getCharmRepository(corecharm.Source(charmOrigin.Source)) 193 if err != nil { 194 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 195 } 196 197 requestedOrigin, err := ConvertParamsOrigin(charmOrigin) 198 if err != nil { 199 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 200 } 201 url, origin, err := repo.GetDownloadURL(curl.Name, requestedOrigin) 202 if err != nil { 203 return params.DownloadInfoResult{}, apiservererrors.ServerError(err) 204 } 205 206 dlorigin, err := convertOrigin(origin) 207 if err != nil { 208 return params.DownloadInfoResult{}, errors.Trace(err) 209 } 210 return params.DownloadInfoResult{ 211 URL: url.String(), 212 Origin: dlorigin, 213 }, nil 214 } 215 216 func (a *API) getDefaultArch() (string, error) { 217 cons, err := a.backendState.ModelConstraints() 218 if err != nil { 219 return "", errors.Trace(err) 220 } 221 return constraints.ArchOrDefault(cons, nil), nil 222 } 223 224 func normalizeCharmOrigin(origin params.CharmOrigin, fallbackArch string) (params.CharmOrigin, error) { 225 // If the series is set to all, we need to ensure that we remove that, so 226 // that we can attempt to derive it at a later stage. Juju itself doesn't 227 // know nor understand what "all" means, so we need to ensure it doesn't leak 228 // out. 229 o := origin 230 if origin.Base.Name == "all" || origin.Base.Channel == "all" { 231 logger.Warningf("Release all detected, removing all from the origin. %s", origin.ID) 232 o.Base = params.Base{} 233 } 234 235 if origin.Architecture == "all" || origin.Architecture == "" { 236 logger.Warningf("Architecture not in expected state, found %q, using fallback architecture %q. %s", origin.Architecture, fallbackArch, origin.ID) 237 o.Architecture = fallbackArch 238 } 239 240 return o, nil 241 } 242 243 // AddCharm adds the given charm URL (which must include revision) to the 244 // environment, if it does not exist yet. Local charms are not supported, 245 // only charm store and charm hub URLs. See also AddLocalCharm(). 246 func (a *API) AddCharm(args params.AddCharmWithOrigin) (params.CharmOriginResult, error) { 247 logger.Tracef("AddCharm %+v", args) 248 return a.addCharmWithAuthorization(params.AddCharmWithAuth{ 249 URL: args.URL, 250 Origin: args.Origin, 251 Force: args.Force, 252 }) 253 } 254 255 // AddCharmWithAuthorization adds the given charm URL (which must include 256 // revision) to the environment, if it does not exist yet. Local charms are 257 // not supported, only charm hub URLs. See also AddLocalCharm(). 258 // 259 // Since the charm macaroons are no longer supported, this is the same as 260 // AddCharm. We keep it for backwards compatibility in APIv5. 261 func (a *APIv5) AddCharmWithAuthorization(args params.AddCharmWithAuth) (params.CharmOriginResult, error) { 262 logger.Tracef("AddCharmWithAuthorization %+v", args) 263 return a.addCharmWithAuthorization(args) 264 } 265 266 func (a *API) addCharmWithAuthorization(args params.AddCharmWithAuth) (params.CharmOriginResult, error) { 267 if commoncharm.OriginSource(args.Origin.Source) != commoncharm.OriginCharmHub { 268 return params.CharmOriginResult{}, errors.Errorf("unknown schema for charm URL %q", args.URL) 269 } 270 271 if args.Origin.Base.Name == "" || args.Origin.Base.Channel == "" { 272 return params.CharmOriginResult{}, errors.BadRequestf("base required for Charmhub charms") 273 } 274 275 if err := a.checkCanWrite(); err != nil { 276 return params.CharmOriginResult{}, err 277 } 278 279 actualOrigin, err := a.queueAsyncCharmDownload(args) 280 if err != nil { 281 return params.CharmOriginResult{}, errors.Trace(err) 282 } 283 284 origin, err := convertOrigin(actualOrigin) 285 if err != nil { 286 return params.CharmOriginResult{}, errors.Trace(err) 287 } 288 return params.CharmOriginResult{ 289 Origin: origin, 290 }, nil 291 } 292 293 func (a *API) queueAsyncCharmDownload(args params.AddCharmWithAuth) (corecharm.Origin, error) { 294 charmURL, err := charm.ParseURL(args.URL) 295 if err != nil { 296 return corecharm.Origin{}, err 297 } 298 299 requestedOrigin, err := ConvertParamsOrigin(args.Origin) 300 if err != nil { 301 return corecharm.Origin{}, errors.Trace(err) 302 } 303 repo, err := a.getCharmRepository(requestedOrigin.Source) 304 if err != nil { 305 return corecharm.Origin{}, errors.Trace(err) 306 } 307 308 // Check if a charm doc already exists for this charm URL. If so, the 309 // charm has already been queued for download so this is a no-op. We 310 // still need to resolve and return back a suitable origin as charmhub 311 // may refer to the same blob using the same revision in different 312 // channels. 313 // 314 // We need to use GetDownloadURL instead of ResolveWithPreferredChannel 315 // to ensure that the resolved origin has the ID/Hash fields correctly 316 // populated. 317 if _, err := a.backendState.Charm(args.URL); err == nil { 318 _, resolvedOrigin, err := repo.GetDownloadURL(charmURL.Name, requestedOrigin) 319 return resolvedOrigin, errors.Trace(err) 320 } 321 322 // Fetch the essential metadata that we require to deploy the charm 323 // without downloading the full archive. The remaining metadata will 324 // be populated once the charm gets downloaded. 325 essentialMeta, err := repo.GetEssentialMetadata(corecharm.MetadataRequest{ 326 CharmName: charmURL.Name, 327 Origin: requestedOrigin, 328 }) 329 if err != nil { 330 return corecharm.Origin{}, errors.Annotatef(err, "retrieving essential metadata for charm %q", charmURL) 331 } 332 metaRes := essentialMeta[0] 333 334 _, err = a.backendState.AddCharmMetadata(state.CharmInfo{ 335 Charm: corecharm.NewCharmInfoAdapter(metaRes), 336 ID: args.URL, 337 }) 338 if err != nil { 339 return corecharm.Origin{}, errors.Trace(err) 340 } 341 342 return metaRes.ResolvedOrigin, nil 343 } 344 345 // ResolveCharms resolves the given charm URLs with an optionally specified 346 // preferred channel. Channel provided via CharmOrigin. 347 func (a *API) ResolveCharms(args params.ResolveCharmsWithChannel) (params.ResolveCharmWithChannelResults, error) { 348 logger.Tracef("ResolveCharms %+v", args) 349 if err := a.checkCanRead(); err != nil { 350 return params.ResolveCharmWithChannelResults{}, errors.Trace(err) 351 } 352 result := params.ResolveCharmWithChannelResults{ 353 Results: make([]params.ResolveCharmWithChannelResult, len(args.Resolve)), 354 } 355 for i, arg := range args.Resolve { 356 result.Results[i] = a.resolveOneCharm(arg) 357 } 358 359 return result, nil 360 } 361 362 func (a *API) resolveOneCharm(arg params.ResolveCharmWithChannel) params.ResolveCharmWithChannelResult { 363 result := params.ResolveCharmWithChannelResult{} 364 curl, err := charm.ParseURL(arg.Reference) 365 if err != nil { 366 result.Error = apiservererrors.ServerError(err) 367 return result 368 } 369 if !charm.CharmHub.Matches(curl.Schema) { 370 result.Error = apiservererrors.ServerError(errors.Errorf("unknown schema for charm URL %q", curl.String())) 371 return result 372 } 373 374 requestedOrigin, err := ConvertParamsOrigin(arg.Origin) 375 if err != nil { 376 result.Error = apiservererrors.ServerError(err) 377 return result 378 } 379 380 // Validate the origin passed in. 381 if err := validateOrigin(requestedOrigin, curl, arg.SwitchCharm); err != nil { 382 result.Error = apiservererrors.ServerError(err) 383 return result 384 } 385 386 repo, err := a.getCharmRepository(corecharm.Source(arg.Origin.Source)) 387 if err != nil { 388 result.Error = apiservererrors.ServerError(err) 389 return result 390 } 391 392 resultURL, origin, resolvedBases, err := repo.ResolveWithPreferredChannel(curl.Name, requestedOrigin) 393 if err != nil { 394 result.Error = apiservererrors.ServerError(err) 395 return result 396 } 397 result.URL = resultURL.String() 398 399 apiOrigin, err := convertOrigin(origin) 400 if err != nil { 401 result.Error = apiservererrors.ServerError(err) 402 return result 403 } 404 405 // The charmhub API can return "all" for architecture as it's not a real 406 // arch we don't know how to correctly model it. "all " doesn't mean use the 407 // default arch, it means use any arch which isn't quite the same. So if we 408 // do get "all" we should see if there is a clean way to resolve it. 409 archOrigin := apiOrigin 410 if apiOrigin.Architecture == "all" { 411 cons, err := a.backendState.ModelConstraints() 412 if err != nil { 413 result.Error = apiservererrors.ServerError(err) 414 return result 415 } 416 archOrigin.Architecture = constraints.ArchOrDefault(cons, nil) 417 } 418 419 result.Origin = archOrigin 420 result.SupportedBases = transform.Slice(resolvedBases, convertCharmBase) 421 422 return result 423 } 424 425 // ResolveCharms resolves the given charm URLs with an optionally specified 426 // preferred channel. Channel provided via CharmOrigin. 427 // We need to include SupportedSeries in facade version 6 428 func (a *APIv6) ResolveCharms(args params.ResolveCharmsWithChannel) (params.ResolveCharmWithChannelResultsV6, error) { 429 res, err := a.API.ResolveCharms(args) 430 if err != nil { 431 return params.ResolveCharmWithChannelResultsV6{}, errors.Trace(err) 432 } 433 results, err := transform.SliceOrErr(res.Results, func(result params.ResolveCharmWithChannelResult) (params.ResolveCharmWithChannelResultV6, error) { 434 supportedSeries, err := transform.SliceOrErr(result.SupportedBases, func(pBase params.Base) (string, error) { 435 b, err := base.ParseBase(pBase.Name, pBase.Channel) 436 if err != nil { 437 return "", err 438 } 439 return base.GetSeriesFromBase(b) 440 }) 441 if err != nil { 442 return params.ResolveCharmWithChannelResultV6{}, err 443 } 444 return params.ResolveCharmWithChannelResultV6{ 445 URL: result.URL, 446 Origin: result.Origin, 447 Error: result.Error, 448 SupportedSeries: supportedSeries, 449 }, nil 450 }) 451 return params.ResolveCharmWithChannelResultsV6{ 452 Results: results, 453 }, err 454 } 455 456 func convertCharmBase(in corecharm.Platform) params.Base { 457 return params.Base{ 458 Name: in.OS, 459 Channel: in.Channel, 460 } 461 } 462 463 func validateOrigin(origin corecharm.Origin, curl *charm.URL, switchCharm bool) error { 464 if !charm.CharmHub.Matches(curl.Schema) { 465 return errors.Errorf("unknown schema for charm URL %q", curl.String()) 466 } 467 // If we are switching to a different charm we can skip the following 468 // origin check; doing so allows us to switch from a charmstore charm 469 // to the equivalent charmhub charm. 470 if !switchCharm { 471 schema := curl.Schema 472 if (corecharm.Local.Matches(origin.Source.String()) && !charm.Local.Matches(schema)) || 473 (corecharm.CharmHub.Matches(origin.Source.String()) && !charm.CharmHub.Matches(schema)) { 474 return errors.NotValidf("origin source %q with schema", origin.Source) 475 } 476 } 477 478 if corecharm.CharmHub.Matches(origin.Source.String()) && origin.Platform.Architecture == "" { 479 return errors.NotValidf("empty architecture") 480 } 481 return nil 482 } 483 484 func (a *API) getCharmRepository(src corecharm.Source) (corecharm.Repository, error) { 485 // The following is only required for testing, as we generate a new http 486 // client here for production. 487 a.mu.Lock() 488 if a.repoFactory != nil { 489 defer a.mu.Unlock() 490 return a.repoFactory.GetCharmRepository(src) 491 } 492 a.mu.Unlock() 493 494 repoFactory := a.newRepoFactory(services.CharmRepoFactoryConfig{ 495 Logger: logger, 496 CharmhubHTTPClient: a.charmhubHTTPClient, 497 StateBackend: a.backendState, 498 ModelBackend: a.backendModel, 499 }) 500 501 return repoFactory.GetCharmRepository(src) 502 } 503 504 // IsMetered returns whether or not the charm is metered. 505 // TODO (cderici) only used for metered charms in cmd MeteredDeployAPI, 506 // kept for client compatibility, remove in juju 4.0 507 func (a *API) IsMetered(args params.CharmURL) (params.IsMeteredResult, error) { 508 if err := a.checkCanRead(); err != nil { 509 return params.IsMeteredResult{}, errors.Trace(err) 510 } 511 512 aCharm, err := a.backendState.Charm(args.URL) 513 if err != nil { 514 return params.IsMeteredResult{Metered: false}, errors.Trace(err) 515 } 516 if aCharm.Metrics() != nil && len(aCharm.Metrics().Metrics) > 0 { 517 return params.IsMeteredResult{Metered: true}, nil 518 } 519 return params.IsMeteredResult{Metered: false}, nil 520 } 521 522 // CheckCharmPlacement checks if a charm is allowed to be placed with in a 523 // given application. 524 func (a *API) CheckCharmPlacement(args params.ApplicationCharmPlacements) (params.ErrorResults, error) { 525 if err := a.checkCanRead(); err != nil { 526 return params.ErrorResults{}, errors.Trace(err) 527 } 528 529 results := params.ErrorResults{ 530 Results: make([]params.ErrorResult, len(args.Placements)), 531 } 532 for i, placement := range args.Placements { 533 result, err := a.checkCharmPlacement(placement) 534 if err != nil { 535 return params.ErrorResults{}, errors.Trace(err) 536 } 537 results.Results[i] = result 538 } 539 540 return results, nil 541 } 542 543 func (a *API) checkCharmPlacement(arg params.ApplicationCharmPlacement) (params.ErrorResult, error) { 544 curl, err := charm.ParseURL(arg.CharmURL) 545 if err != nil { 546 return params.ErrorResult{ 547 Error: apiservererrors.ServerError(err), 548 }, nil 549 } 550 551 // The placement logic below only cares about charmhub charms. Once we have 552 // multiple architecture support for charmhub, we can remove the placement 553 // check. 554 if !charm.CharmHub.Matches(curl.Schema) { 555 return params.ErrorResult{}, nil 556 } 557 558 // Get the application. If it's not found, just return without an error as 559 // the charm can be placed in the application once it's created. 560 app, err := a.backendState.Application(arg.Application) 561 if errors.IsNotFound(err) { 562 return params.ErrorResult{}, nil 563 } else if err != nil { 564 return params.ErrorResult{ 565 Error: apiservererrors.ServerError(err), 566 }, nil 567 } 568 569 // We don't care for subordinates here. 570 if !app.IsPrincipal() { 571 return params.ErrorResult{}, nil 572 } 573 574 constraints, err := app.Constraints() 575 if err != nil && !errors.IsNotFound(err) { 576 return params.ErrorResult{ 577 Error: apiservererrors.ServerError(err), 578 }, nil 579 } 580 581 // If the application has an existing architecture constraint then we're 582 // happy that the constraint logic will prevent heterogenous application 583 // units. 584 if constraints.HasArch() { 585 return params.ErrorResult{}, nil 586 } 587 588 // Unfortunately we now have to check instance data for all units to 589 // validate that we have a homogeneous setup. 590 units, err := app.AllUnits() 591 if err != nil { 592 return params.ErrorResult{ 593 Error: apiservererrors.ServerError(err), 594 }, nil 595 } 596 597 arches := set.NewStrings() 598 for _, unit := range units { 599 machineID, err := unit.AssignedMachineId() 600 if errors.IsNotAssigned(err) { 601 continue 602 } else if err != nil { 603 return params.ErrorResult{ 604 Error: apiservererrors.ServerError(err), 605 }, nil 606 } 607 608 machine, err := a.backendState.Machine(machineID) 609 if errors.IsNotFound(err) { 610 continue 611 } else if err != nil { 612 return params.ErrorResult{ 613 Error: apiservererrors.ServerError(err), 614 }, nil 615 } 616 617 machineArch, err := a.getMachineArch(machine) 618 if err != nil { 619 return params.ErrorResult{ 620 Error: apiservererrors.ServerError(err), 621 }, nil 622 } 623 624 if machineArch == "" { 625 arches.Add(arch.DefaultArchitecture) 626 } else { 627 arches.Add(machineArch) 628 } 629 } 630 631 if arches.Size() > 1 { 632 // It is expected that charmhub charms form a homogeneous workload, 633 // so that each unit is the same architecture. 634 err := errors.Errorf("charm can not be placed in a heterogeneous environment") 635 return params.ErrorResult{ 636 Error: apiservererrors.ServerError(err), 637 }, nil 638 } 639 640 return params.ErrorResult{}, nil 641 } 642 643 func (a *API) getMachineArch(machine charmsinterfaces.Machine) (arch.Arch, error) { 644 cons, err := machine.Constraints() 645 if err == nil && cons.HasArch() { 646 return *cons.Arch, nil 647 } 648 649 hardware, err := machine.HardwareCharacteristics() 650 if errors.IsNotFound(err) { 651 return "", nil 652 } else if err != nil { 653 return "", errors.Trace(err) 654 } 655 656 if hardware.Arch != nil { 657 return *hardware.Arch, nil 658 } 659 660 return "", nil 661 } 662 663 // ListCharmResources returns a series of resources for a given charm. 664 func (a *API) ListCharmResources(args params.CharmURLAndOrigins) (params.CharmResourcesResults, error) { 665 if err := a.checkCanRead(); err != nil { 666 return params.CharmResourcesResults{}, errors.Trace(err) 667 } 668 results := params.CharmResourcesResults{ 669 Results: make([][]params.CharmResourceResult, len(args.Entities)), 670 } 671 for i, arg := range args.Entities { 672 result, err := a.listOneCharmResources(arg) 673 if err != nil { 674 return params.CharmResourcesResults{}, errors.Trace(err) 675 } 676 results.Results[i] = result 677 } 678 return results, nil 679 } 680 681 func (a *API) listOneCharmResources(arg params.CharmURLAndOrigin) ([]params.CharmResourceResult, error) { 682 // TODO (stickupkid) - remove api packages from apiserver packages. 683 curl, err := charm.ParseURL(arg.CharmURL) 684 if err != nil { 685 return nil, apiservererrors.ServerError(err) 686 } 687 if !charm.CharmHub.Matches(curl.Schema) { 688 return nil, apiservererrors.ServerError(errors.NotValidf("charm %q", curl.Name)) 689 } 690 691 defaultArch, err := a.getDefaultArch() 692 if err != nil { 693 return nil, apiservererrors.ServerError(err) 694 } 695 696 charmOrigin, err := normalizeCharmOrigin(arg.Origin, defaultArch) 697 if err != nil { 698 return nil, apiservererrors.ServerError(err) 699 } 700 repo, err := a.getCharmRepository(corecharm.Source(charmOrigin.Source)) 701 if err != nil { 702 return nil, apiservererrors.ServerError(err) 703 } 704 705 requestedOrigin, err := ConvertParamsOrigin(charmOrigin) 706 if err != nil { 707 return nil, apiservererrors.ServerError(err) 708 } 709 resources, err := repo.ListResources(curl.Name, requestedOrigin) 710 if err != nil { 711 return nil, apiservererrors.ServerError(err) 712 } 713 714 results := make([]params.CharmResourceResult, len(resources)) 715 for i, resource := range resources { 716 results[i].CharmResource = apiresources.CharmResource2API(resource) 717 } 718 719 return results, nil 720 } 721 722 type noopRequestRecorder struct{} 723 724 // Record an outgoing request which produced an http.Response. 725 func (noopRequestRecorder) Record(method string, url *url.URL, res *http.Response, rtt time.Duration) { 726 } 727 728 // Record an outgoing request which returned back an error. 729 func (noopRequestRecorder) RecordError(method string, url *url.URL, err error) {}