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) {}