github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/charm/repository/charmhub.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package repository
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"github.com/juju/charm/v12"
    13  	charmresource "github.com/juju/charm/v12/resource"
    14  	"github.com/juju/collections/set"
    15  	"github.com/juju/errors"
    16  
    17  	"github.com/juju/juju/charmhub"
    18  	"github.com/juju/juju/charmhub/transport"
    19  	corebase "github.com/juju/juju/core/base"
    20  	corecharm "github.com/juju/juju/core/charm"
    21  )
    22  
    23  // CharmHubClient describes the API exposed by the charmhub client.
    24  type CharmHubClient interface {
    25  	DownloadAndRead(ctx context.Context, resourceURL *url.URL, archivePath string, options ...charmhub.DownloadOption) (*charm.CharmArchive, error)
    26  	ListResourceRevisions(ctx context.Context, charm, resource string) ([]transport.ResourceRevision, error)
    27  	Refresh(ctx context.Context, config charmhub.RefreshConfig) ([]transport.RefreshResponse, error)
    28  }
    29  
    30  // CharmHubRepository provides an API for charm-related operations using charmhub.
    31  type CharmHubRepository struct {
    32  	logger Logger
    33  	client CharmHubClient
    34  }
    35  
    36  // NewCharmHubRepository returns a new repository instance using the provided
    37  // charmhub client.
    38  func NewCharmHubRepository(logger Logger, chClient CharmHubClient) *CharmHubRepository {
    39  	return &CharmHubRepository{
    40  		logger: logger,
    41  		client: chClient,
    42  	}
    43  }
    44  
    45  // ResolveWithPreferredChannel defines a way using the given charm name and
    46  // charm origin (platform and channel) to locate a matching charm against the
    47  // Charmhub API.
    48  func (c *CharmHubRepository) ResolveWithPreferredChannel(charmName string, argOrigin corecharm.Origin) (*charm.URL, corecharm.Origin, []corecharm.Platform, error) {
    49  	c.logger.Tracef("Resolving CharmHub charm %q with origin %+v", charmName, argOrigin)
    50  
    51  	requestedOrigin, err := c.validateOrigin(argOrigin)
    52  	if err != nil {
    53  		return nil, corecharm.Origin{}, nil, err
    54  	}
    55  	resCurl, outputOrigin, resolvedBases, _, err := c.resolveWithPreferredChannel(charmName, requestedOrigin)
    56  	return resCurl, outputOrigin, resolvedBases, err
    57  }
    58  
    59  // ResolveForDeploy combines ResolveWithPreferredChannel, GetEssentialMetadata
    60  // and best effort for repositoryResources into 1 call for server side charm deployment.
    61  // Reducing the number of required calls to a repository.
    62  func (c *CharmHubRepository) ResolveForDeploy(arg corecharm.CharmID) (corecharm.ResolvedDataForDeploy, error) {
    63  	c.logger.Tracef("Resolving CharmHub charm %q with origin %+v", arg.URL, arg.Origin)
    64  
    65  	resultURL, resolvedOrigin, _, resp, resolveErr := c.resolveWithPreferredChannel(arg.URL.Name, arg.Origin)
    66  	if resolveErr != nil {
    67  		return corecharm.ResolvedDataForDeploy{}, errors.Trace(resolveErr)
    68  	}
    69  
    70  	essMeta, err := transformRefreshResult(resultURL.Name, resp)
    71  	if err != nil {
    72  		return corecharm.ResolvedDataForDeploy{}, errors.Trace(err)
    73  	}
    74  	essMeta.ResolvedOrigin = resolvedOrigin
    75  
    76  	// Get ID and Hash for the origin. Needed in the case where this
    77  	// charm has been downloaded before.
    78  	resolvedOrigin.ID = resp.Entity.ID
    79  	resolvedOrigin.Hash = resp.Entity.Download.HashSHA256
    80  
    81  	// Resources are best attempt here. If we were able to resolve the charm
    82  	// via a channel, the resource data will be here. If using a revision,
    83  	// then not. However, that does not mean that the charm has no resources.
    84  	resourceResults, err := transformResourceRevision(resp.Entity.Resources)
    85  	if err != nil {
    86  		return corecharm.ResolvedDataForDeploy{}, errors.Trace(err)
    87  	}
    88  
    89  	thing := corecharm.ResolvedDataForDeploy{
    90  		URL:               resultURL,
    91  		EssentialMetadata: essMeta,
    92  		Resources:         resourceResults,
    93  	}
    94  	return thing, nil
    95  }
    96  
    97  // There are a few things to note in the attempt to resolve the charm and it's
    98  // supporting series.
    99  //
   100  //  1. The algorithm for this is terrible. For Charmhub the worst case for this
   101  //     will be 2.
   102  //     Most of the initial requests from the client will hit this first time
   103  //     around (think `juju deploy foo`) without a series (client can then
   104  //     determine what to call the real request with) will be default of 2
   105  //     requests.
   106  //  2. Attempting to find the default series will require 2 requests so that
   107  //     we can find the correct charm ID ensure that the default series exists
   108  //     along with the revision.
   109  //  3. In theory we could just return most of this information without the
   110  //     re-request, but we end up with missing data and potential incorrect
   111  //     charm downloads later.
   112  func (c *CharmHubRepository) resolveWithPreferredChannel(charmName string, requestedOrigin corecharm.Origin) (*charm.URL, corecharm.Origin, []corecharm.Platform, transport.RefreshResponse, error) {
   113  	c.logger.Tracef("Resolving CharmHub charm %q with origin %v", charmName, requestedOrigin)
   114  
   115  	// First attempt to find the charm based on the only input provided.
   116  	response, err := c.refreshOne(charmName, requestedOrigin)
   117  	if err != nil {
   118  		return nil, corecharm.Origin{}, nil, transport.RefreshResponse{}, errors.Annotatef(err, "resolving with preferred channel")
   119  	}
   120  
   121  	// resolvedBases holds a slice of supported bases from the subsequent
   122  	// refresh API call. The bases can inform the consumer of the API about what
   123  	// they can also install *IF* the retry resolution uses a base that doesn't
   124  	// match their requirements. This can happen in the client if the series
   125  	// selection also wants to consider model-config default-base after the
   126  	// call.
   127  	var (
   128  		effectiveChannel  string
   129  		resolvedBases     []corecharm.Platform
   130  		chSuggestedOrigin = requestedOrigin
   131  	)
   132  	switch {
   133  	case response.Error != nil:
   134  		retryResult, err := c.retryResolveWithPreferredChannel(charmName, requestedOrigin, response.Error)
   135  		if err != nil {
   136  			return nil, corecharm.Origin{}, nil, transport.RefreshResponse{}, errors.Trace(err)
   137  		}
   138  
   139  		response = retryResult.refreshResponse
   140  		resolvedBases = retryResult.bases
   141  		chSuggestedOrigin = retryResult.origin
   142  
   143  		// Fill these back on the origin, so that we can fix the issue of
   144  		// bundles passing back "all" on the response type.
   145  		// Note: we can be sure these have at least one, because of the
   146  		// validation logic in retry method.
   147  		requestedOrigin.Platform.OS = resolvedBases[0].OS
   148  		requestedOrigin.Platform.Channel = resolvedBases[0].Channel
   149  
   150  		effectiveChannel = response.EffectiveChannel
   151  	case requestedOrigin.Revision != nil && *requestedOrigin.Revision != -1:
   152  		if len(response.Entity.Bases) > 0 {
   153  			for _, v := range response.Entity.Bases {
   154  				resolvedBases = append(resolvedBases, corecharm.Platform{
   155  					Architecture: v.Architecture,
   156  					OS:           v.Name,
   157  					Channel:      v.Channel,
   158  				})
   159  			}
   160  		}
   161  		// Entities installed by revision do not have an effective channel in the data.
   162  		// A charm with revision X can be in multiple different channels.  The user
   163  		// specified channel for future refresh calls, is located in the origin.
   164  		if response.Entity.Type == transport.CharmType && requestedOrigin.Channel != nil {
   165  			effectiveChannel = requestedOrigin.Channel.String()
   166  		} else if response.Entity.Type == transport.BundleType {
   167  			// This is a hack. A bundle does not require a channel moving forward as refresh
   168  			// by bundle is not implemented, however the following code expects a channel.
   169  			effectiveChannel = "stable"
   170  		}
   171  	default:
   172  		effectiveChannel = response.EffectiveChannel
   173  	}
   174  
   175  	// Use the channel that was actually picked by the API. This should
   176  	// account for the closed tracks in a given channel.
   177  	channel, err := charm.ParseChannelNormalize(effectiveChannel)
   178  	if err != nil {
   179  		return nil, corecharm.Origin{}, nil, transport.RefreshResponse{}, errors.Annotatef(err, "invalid channel for %q", charmName)
   180  	}
   181  
   182  	// Ensure we send the updated charmURL back, with all the correct segments.
   183  	revision := response.Entity.Revision
   184  	// TODO(wallyworld) - does charm url still need a series?
   185  	var series string
   186  	if chSuggestedOrigin.Platform.Channel != "" {
   187  		series, err = corebase.GetSeriesFromChannel(chSuggestedOrigin.Platform.OS, chSuggestedOrigin.Platform.Channel)
   188  		if err != nil {
   189  			return nil, corecharm.Origin{}, nil, transport.RefreshResponse{}, errors.Trace(err)
   190  		}
   191  	}
   192  	resCurl := &charm.URL{
   193  		Schema:       "ch",
   194  		Name:         charmName,
   195  		Revision:     revision,
   196  		Series:       series,
   197  		Architecture: chSuggestedOrigin.Platform.Architecture,
   198  	}
   199  
   200  	// Create a resolved origin.  Keep the original values for ID and Hash, if
   201  	// any were passed in.  ResolveWithPreferredChannel is called for both
   202  	// charms to be deployed, and charms which are being upgraded.
   203  	// Only charms being upgraded will have an ID and Hash. Those values should
   204  	// only ever be updated in DownloadURL.
   205  	resOrigin := corecharm.Origin{
   206  		Source:      requestedOrigin.Source,
   207  		ID:          requestedOrigin.ID,
   208  		Hash:        requestedOrigin.Hash,
   209  		Type:        string(response.Entity.Type),
   210  		Channel:     &channel,
   211  		Revision:    &revision,
   212  		Platform:    chSuggestedOrigin.Platform,
   213  		InstanceKey: requestedOrigin.InstanceKey,
   214  	}
   215  
   216  	outputOrigin, err := sanitiseCharmOrigin(resOrigin, requestedOrigin)
   217  	if err != nil {
   218  		return nil, corecharm.Origin{}, nil, transport.RefreshResponse{}, errors.Trace(err)
   219  	}
   220  	c.logger.Tracef("Resolved CharmHub charm %q with origin %v", resCurl, outputOrigin)
   221  
   222  	// If the callee of the API defines a base and that base is pick and
   223  	// identified as being selected (think `juju deploy --base`) then we will
   224  	// never have to retry. The API will never give us back any other supported
   225  	// base, so we can just pass back what the callee requested.
   226  	// This is the happy path for resolving a charm.
   227  	//
   228  	// Unfortunately, most deployments will not pass a base flag, so we will
   229  	// have to ask the API to give us back a potential base. The supported
   230  	// bases can be passed back. The callee can then determine which base they
   231  	// want to use and deploy that accordingly without another API request.
   232  	if len(resolvedBases) == 0 && outputOrigin.Platform.Channel != "" {
   233  		resolvedBases = []corecharm.Platform{outputOrigin.Platform}
   234  	}
   235  	return resCurl, outputOrigin, resolvedBases, response, nil
   236  }
   237  
   238  // validateOrigin, validate the origin and maybe fix as follows:
   239  //
   240  //	Platform must have an architecture.
   241  //	Platform can have both an empty Channel AND os.
   242  //	Platform must have channel if os defined.
   243  //	Platform must have os if channel defined.
   244  func (c *CharmHubRepository) validateOrigin(origin corecharm.Origin) (corecharm.Origin, error) {
   245  	p := origin.Platform
   246  
   247  	if p.Architecture == "" {
   248  		return corecharm.Origin{}, errors.BadRequestf("origin.Platform requires an Architecture")
   249  	}
   250  
   251  	if p.OS != "" && p.Channel == "" {
   252  		return corecharm.Origin{}, errors.BadRequestf("origin.Platform requires a Channel, if OS set")
   253  	}
   254  
   255  	if p.OS == "" && p.Channel != "" {
   256  		return corecharm.Origin{}, errors.BadRequestf("origin.Platform requires an OS, if channel set")
   257  	}
   258  	return origin, nil
   259  }
   260  
   261  type retryResolveResult struct {
   262  	refreshResponse transport.RefreshResponse
   263  	origin          corecharm.Origin
   264  	bases           []corecharm.Platform
   265  }
   266  
   267  // retryResolveWithPreferredChannel will attempt to inspect the transport
   268  // APIError and determine if a retry is possible with the information gathered
   269  // from the error.
   270  func (c *CharmHubRepository) retryResolveWithPreferredChannel(charmName string, origin corecharm.Origin, resErr *transport.APIError) (*retryResolveResult, error) {
   271  	var (
   272  		err   error
   273  		bases []corecharm.Platform
   274  	)
   275  	switch resErr.Code {
   276  	case transport.ErrorCodeInvalidCharmPlatform, transport.ErrorCodeInvalidCharmBase:
   277  		c.logger.Tracef("Invalid charm base %q %v - Default Base: %v", charmName, origin, resErr.Extra.DefaultBases)
   278  
   279  		if bases, err = c.selectNextBases(resErr.Extra.DefaultBases, origin); err != nil {
   280  			return nil, errors.Annotatef(err, "selecting next bases")
   281  		}
   282  
   283  	case transport.ErrorCodeRevisionNotFound:
   284  		c.logger.Tracef("Revision not found %q %v - Releases: %v", charmName, origin, resErr.Extra.Releases)
   285  
   286  		return nil, errors.Annotatef(c.handleRevisionNotFound(resErr.Extra.Releases, origin), "selecting releases")
   287  
   288  	default:
   289  		return nil, errors.Errorf("resolving error: %s", resErr.Message)
   290  	}
   291  
   292  	if len(bases) == 0 {
   293  		ch := origin.Channel.String()
   294  		if ch == "" {
   295  			ch = "stable"
   296  		}
   297  		return nil, errors.Wrap(resErr, errors.Errorf("no releases found for channel %q", ch))
   298  	}
   299  	base := bases[0]
   300  
   301  	origin.Platform.OS = base.OS
   302  	origin.Platform.Channel = base.Channel
   303  
   304  	if origin.Platform.Channel == "" {
   305  		return nil, errors.NotValidf("channel for %s", charmName)
   306  	}
   307  
   308  	c.logger.Tracef("Refresh again with %q %v", charmName, origin)
   309  	res, err := c.refreshOne(charmName, origin)
   310  	if err != nil {
   311  		return nil, errors.Annotatef(err, "retrying")
   312  	}
   313  	if resErr := res.Error; resErr != nil {
   314  		return nil, errors.Errorf("resolving retry error: %s", resErr.Message)
   315  	}
   316  	return &retryResolveResult{
   317  		refreshResponse: res,
   318  		origin:          origin,
   319  		bases:           bases,
   320  	}, nil
   321  }
   322  
   323  func transformRefreshResult(charmName string, refreshResult transport.RefreshResponse) (corecharm.EssentialMetadata, error) {
   324  	if refreshResult.Entity.MetadataYAML == "" {
   325  		return corecharm.EssentialMetadata{}, errors.NotValidf("charmhub refresh response for %q does not include the contents of metadata.yaml", charmName)
   326  	}
   327  	chMeta, err := charm.ReadMeta(strings.NewReader(refreshResult.Entity.MetadataYAML))
   328  	if err != nil {
   329  		return corecharm.EssentialMetadata{}, errors.Annotatef(err, "parsing metadata.yaml for %q", charmName)
   330  	}
   331  
   332  	configYAML := refreshResult.Entity.ConfigYAML
   333  	var chConfig *charm.Config
   334  	// NOTE: Charmhub returns a "{}\n" when no config.yaml exists for
   335  	// the charm, e.g. postgreql. However, this will fail the charm
   336  	// config validation which happens in ReadConfig. Valid config
   337  	// are nil and "Options: {}"
   338  	if configYAML == "" || strings.TrimSpace(configYAML) == "{}" {
   339  		chConfig = charm.NewConfig()
   340  	} else {
   341  		chConfig, err = charm.ReadConfig(strings.NewReader(configYAML))
   342  		if err != nil {
   343  			return corecharm.EssentialMetadata{}, errors.Annotatef(err, "parsing config.yaml for %q", charmName)
   344  		}
   345  	}
   346  
   347  	chManifest := new(charm.Manifest)
   348  	for _, base := range refreshResult.Entity.Bases {
   349  		baseCh, err := charm.ParseChannelNormalize(base.Channel)
   350  		if err != nil {
   351  			return corecharm.EssentialMetadata{}, errors.Annotatef(err, "parsing base channel for %q", charmName)
   352  		}
   353  
   354  		chManifest.Bases = append(chManifest.Bases, charm.Base{
   355  			Name:          base.Name,
   356  			Channel:       baseCh,
   357  			Architectures: []string{base.Architecture},
   358  		})
   359  	}
   360  	return corecharm.EssentialMetadata{Meta: chMeta, Config: chConfig, Manifest: chManifest}, nil
   361  }
   362  
   363  // DownloadCharm retrieves specified charm from the store and saves its
   364  // contents to the specified path.
   365  func (c *CharmHubRepository) DownloadCharm(charmName string, requestedOrigin corecharm.Origin, archivePath string) (corecharm.CharmArchive, corecharm.Origin, error) {
   366  	c.logger.Tracef("DownloadCharm %q, origin: %q", charmName, requestedOrigin)
   367  
   368  	// Resolve charm URL to a link to the charm blob and keep track of the
   369  	// actual resolved origin which may be different from the requested one.
   370  	resURL, actualOrigin, err := c.GetDownloadURL(charmName, requestedOrigin)
   371  	if err != nil {
   372  		return nil, corecharm.Origin{}, errors.Trace(err)
   373  	}
   374  
   375  	charmArchive, err := c.client.DownloadAndRead(context.TODO(), resURL, archivePath)
   376  	if err != nil {
   377  		return nil, corecharm.Origin{}, errors.Trace(err)
   378  	}
   379  
   380  	return charmArchive, actualOrigin, nil
   381  }
   382  
   383  // GetDownloadURL returns the url from which to download the CharmHub charm/bundle
   384  // defined by the provided charm name and origin.  An updated charm origin is
   385  // also returned with the ID and hash for the charm to be downloaded.  If the
   386  // provided charm origin has no ID, it is assumed that the charm is being
   387  // installed, not refreshed.
   388  func (c *CharmHubRepository) GetDownloadURL(charmName string, requestedOrigin corecharm.Origin) (*url.URL, corecharm.Origin, error) {
   389  	c.logger.Tracef("GetDownloadURL %q, origin: %q", charmName, requestedOrigin)
   390  
   391  	refreshRes, err := c.refreshOne(charmName, requestedOrigin)
   392  	if err != nil {
   393  		return nil, corecharm.Origin{}, errors.Trace(err)
   394  	}
   395  	if refreshRes.Error != nil {
   396  		return nil, corecharm.Origin{}, errors.Errorf("%s: %s", refreshRes.Error.Code, refreshRes.Error.Message)
   397  	}
   398  
   399  	resOrigin := requestedOrigin
   400  
   401  	// We've called Refresh with the install action.  Now update the
   402  	// charm ID and Hash values saved.  This is the only place where
   403  	// they should be saved.
   404  	resOrigin.ID = refreshRes.Entity.ID
   405  	resOrigin.Hash = refreshRes.Entity.Download.HashSHA256
   406  
   407  	durl, err := url.Parse(refreshRes.Entity.Download.URL)
   408  	if err != nil {
   409  		return nil, corecharm.Origin{}, errors.Trace(err)
   410  	}
   411  	outputOrigin, err := sanitiseCharmOrigin(resOrigin, requestedOrigin)
   412  	return durl, outputOrigin, errors.Trace(err)
   413  }
   414  
   415  // ListResources returns the resources for a given charm and origin.
   416  func (c *CharmHubRepository) ListResources(charmName string, origin corecharm.Origin) ([]charmresource.Resource, error) {
   417  	c.logger.Tracef("ListResources %q", charmName)
   418  
   419  	resCurl, resOrigin, _, err := c.ResolveWithPreferredChannel(charmName, origin)
   420  	if isErrSelection(err) {
   421  		var channel string
   422  		if origin.Channel != nil {
   423  			channel = origin.Channel.String()
   424  		}
   425  		return nil, errors.Errorf("unable to locate charm %q with matching channel %q", charmName, channel)
   426  	} else if err != nil {
   427  		return nil, errors.Trace(err)
   428  	}
   429  
   430  	// If a revision is included with an install action, no resources will be
   431  	// returned. Resources are dependent on a channel, a specific revision can
   432  	// be in multiple channels.  refreshOne gives priority to a revision if
   433  	// specified.  ListResources is used by the "charm-resources" cli cmd,
   434  	// therefore specific charm revisions are less important.
   435  	resOrigin.Revision = nil
   436  	resp, err := c.refreshOne(resCurl.Name, resOrigin)
   437  	if err != nil {
   438  		return nil, errors.Trace(err)
   439  	}
   440  
   441  	results := make([]charmresource.Resource, len(resp.Entity.Resources))
   442  	for i, resource := range resp.Entity.Resources {
   443  		results[i], err = resourceFromRevision(resource)
   444  		if err != nil {
   445  			return nil, errors.Trace(err)
   446  		}
   447  	}
   448  
   449  	return results, nil
   450  }
   451  
   452  // TODO 30-Nov-2022
   453  // ResolveResources can be made more efficient, some choices left over from
   454  // integration with charmstore style of working.
   455  
   456  // ResolveResources looks at the provided charmhub and backend (already
   457  // downloaded) resources to determine which to use. Provided (uploaded) take
   458  // precedence. If charmhub has a newer resource than the back end, use that.
   459  func (c *CharmHubRepository) ResolveResources(resources []charmresource.Resource, id corecharm.CharmID) ([]charmresource.Resource, error) {
   460  	revisionResources, err := c.listResourcesIfRevisions(resources, id.URL.Name)
   461  	if err != nil {
   462  		return nil, errors.Trace(err)
   463  	}
   464  	storeResources, err := c.repositoryResources(id)
   465  	if err != nil {
   466  		return nil, errors.Trace(err)
   467  	}
   468  	storeResourcesMap, err := transformResourceRevision(storeResources)
   469  	if err != nil {
   470  		return nil, errors.Trace(err)
   471  	}
   472  	for k, v := range revisionResources {
   473  		storeResourcesMap[k] = v
   474  	}
   475  	resolved, err := c.resolveResources(resources, storeResourcesMap, id)
   476  	if err != nil {
   477  		return nil, errors.Trace(err)
   478  	}
   479  	return resolved, nil
   480  }
   481  
   482  func (c *CharmHubRepository) listResourcesIfRevisions(resources []charmresource.Resource, charmName string) (map[string]charmresource.Resource, error) {
   483  	results := make(map[string]charmresource.Resource, 0)
   484  	for _, resource := range resources {
   485  		// If not revision is specified, or the resource has already been
   486  		// uploaded, no need to attempt to find it here.
   487  		if resource.Revision == -1 || resource.Origin == charmresource.OriginUpload {
   488  			continue
   489  		}
   490  		refreshResp, err := c.client.ListResourceRevisions(context.TODO(), charmName, resource.Name)
   491  		if err != nil {
   492  			return nil, errors.Annotatef(err, "refreshing charm %q", charmName)
   493  		}
   494  		if len(refreshResp) == 0 {
   495  			return nil, errors.Errorf("no download refresh responses received")
   496  		}
   497  		for _, res := range refreshResp {
   498  			if res.Revision == resource.Revision {
   499  				results[resource.Name], err = resourceFromRevision(refreshResp[0])
   500  				if err != nil {
   501  					return nil, errors.Trace(err)
   502  				}
   503  			}
   504  		}
   505  	}
   506  	return results, nil
   507  }
   508  
   509  // repositoryResources composes, a map of details for each of the charm's
   510  // resources. Those details are those associated with the specific
   511  // charm channel. They include the resource's metadata and revision.
   512  // Found via the CharmHub api. ListResources requires charm resolution,
   513  // this method does not.
   514  func (c *CharmHubRepository) repositoryResources(id corecharm.CharmID) ([]transport.ResourceRevision, error) {
   515  	curl := id.URL
   516  	origin := id.Origin
   517  	refBase := charmhub.RefreshBase{
   518  		Architecture: origin.Platform.Architecture,
   519  		Name:         origin.Platform.OS,
   520  		Channel:      origin.Platform.Channel,
   521  	}
   522  	var cfg charmhub.RefreshConfig
   523  	var err error
   524  	switch {
   525  	// Do not get resource data via revision here, it is only provided if explicitly
   526  	// asked for by resource revision.  The purpose here is to find a resource revision
   527  	// in the channel, if one was not provided on the cli.
   528  	case origin.ID != "":
   529  		cfg, err = charmhub.DownloadOneFromChannel(origin.ID, origin.Channel.String(), refBase)
   530  		if err != nil {
   531  			c.logger.Errorf("creating resources config for charm (%q, %q): %s", origin.ID, origin.Channel.String(), err)
   532  			return nil, errors.Annotatef(err, "creating resources config for charm %q", curl.String())
   533  		}
   534  	case origin.ID == "":
   535  		cfg, err = charmhub.DownloadOneFromChannelByName(curl.Name, origin.Channel.String(), refBase)
   536  		if err != nil {
   537  			c.logger.Errorf("creating resources config for charm (%q, %q): %s", curl.Name, origin.Channel.String(), err)
   538  			return nil, errors.Annotatef(err, "creating resources config for charm %q", curl.String())
   539  		}
   540  	}
   541  	refreshResp, err := c.client.Refresh(context.TODO(), cfg)
   542  	if err != nil {
   543  		return nil, errors.Annotatef(err, "refreshing charm %q", curl.String())
   544  	}
   545  	if len(refreshResp) == 0 {
   546  		return nil, errors.Errorf("no download refresh responses received")
   547  	}
   548  	resp := refreshResp[0]
   549  	if resp.Error != nil {
   550  		return nil, errors.Annotatef(errors.New(resp.Error.Message), "listing resources for charm %q", curl.String())
   551  	}
   552  	return resp.Entity.Resources, nil
   553  }
   554  
   555  // transformResourceRevision transforms resource revision structs in charmhub format into
   556  // charmresource format for use within juju.
   557  func transformResourceRevision(resources []transport.ResourceRevision) (map[string]charmresource.Resource, error) {
   558  	if len(resources) == 0 {
   559  		return nil, nil
   560  	}
   561  	results := make(map[string]charmresource.Resource, len(resources))
   562  	for _, v := range resources {
   563  		var err error
   564  		results[v.Name], err = resourceFromRevision(v)
   565  		if err != nil {
   566  			return nil, errors.Trace(err)
   567  		}
   568  	}
   569  	return results, nil
   570  }
   571  
   572  // resolveResources determines the resource info that should actually
   573  // be stored on the controller. That decision is based on the provided
   574  // resources along with those in the charm backend (if any).
   575  func (c *CharmHubRepository) resolveResources(resources []charmresource.Resource,
   576  	storeResources map[string]charmresource.Resource,
   577  	id corecharm.CharmID,
   578  ) ([]charmresource.Resource, error) {
   579  	allResolved := make([]charmresource.Resource, len(resources))
   580  	copy(allResolved, resources)
   581  	for i, res := range resources {
   582  		// Note that incoming "upload" resources take precedence over
   583  		// ones already known to the controller, regardless of their
   584  		// origin.
   585  		if res.Origin != charmresource.OriginStore {
   586  			continue
   587  		}
   588  
   589  		resolved, err := c.resolveRepositoryResources(res, storeResources, id)
   590  		if err != nil {
   591  			return nil, errors.Trace(err)
   592  		}
   593  		allResolved[i] = resolved
   594  	}
   595  	return allResolved, nil
   596  }
   597  
   598  // resolveRepositoryResources selects the resource info to use. It decides
   599  // between the provided and latest info based on the revision.
   600  func (c *CharmHubRepository) resolveRepositoryResources(res charmresource.Resource,
   601  	storeResources map[string]charmresource.Resource,
   602  	id corecharm.CharmID,
   603  ) (charmresource.Resource, error) {
   604  	storeRes, ok := storeResources[res.Name]
   605  	if !ok {
   606  		// This indicates that AddPendingResources() was called for
   607  		// a resource the charm backend doesn't know about (for the
   608  		// relevant charm revision).
   609  		return res, nil
   610  	}
   611  
   612  	if res.Revision < 0 {
   613  		// The caller wants to use the charm backend info.
   614  		return storeRes, nil
   615  	}
   616  	if res.Revision == storeRes.Revision {
   617  		// We don't worry about if they otherwise match. Only the
   618  		// revision is significant here. So we use the info from the
   619  		// charm backend since it is authoritative.
   620  		return storeRes, nil
   621  	}
   622  	if res.Fingerprint.IsZero() {
   623  		// The caller wants resource info from the charm backend, but with
   624  		// a different resource revision than the one associated with
   625  		// the charm in the backend.
   626  		return c.resourceInfo(id.URL, id.Origin, res.Name, res.Revision)
   627  	}
   628  	// The caller fully-specified a resource with a different resource
   629  	// revision than the one associated with the charm in the backend. So
   630  	// we use the provided info as-is.
   631  	return res, nil
   632  }
   633  
   634  func (c *CharmHubRepository) resourceInfo(curl *charm.URL, origin corecharm.Origin, name string, revision int) (charmresource.Resource, error) {
   635  	var configs []charmhub.RefreshConfig
   636  	var err error
   637  
   638  	// Due to async charm downloading we may not always have a charm ID to
   639  	// use for getting resource info, however it is preferred. A charm name
   640  	// is second best due to anticipation of charms being renamed in the
   641  	// future. The charm url may not change, but the ID will reference the
   642  	// new name.
   643  	if origin.ID != "" {
   644  		configs, err = configsByID(curl, origin, name, revision)
   645  	} else {
   646  		configs, err = configsByName(curl, origin, name, revision)
   647  	}
   648  	if err != nil {
   649  		return charmresource.Resource{}, err
   650  	}
   651  
   652  	refreshResp, err := c.client.Refresh(context.TODO(), charmhub.RefreshMany(configs...))
   653  	if err != nil {
   654  		return charmresource.Resource{}, errors.Trace(err)
   655  	}
   656  	if len(refreshResp) == 0 {
   657  		return charmresource.Resource{}, errors.Errorf("no download refresh responses received")
   658  	}
   659  
   660  	for _, resp := range refreshResp {
   661  		if resp.Error != nil {
   662  			return charmresource.Resource{}, errors.Trace(errors.New(resp.Error.Message))
   663  		}
   664  
   665  		for _, entity := range resp.Entity.Resources {
   666  			if entity.Name == name && entity.Revision == revision {
   667  				rfr, err := resourceFromRevision(entity)
   668  				return rfr, err
   669  			}
   670  		}
   671  	}
   672  	return charmresource.Resource{}, errors.NotFoundf("charm resource %q at revision %d", name, revision)
   673  }
   674  
   675  func configsByID(curl *charm.URL, origin corecharm.Origin, name string, revision int) ([]charmhub.RefreshConfig, error) {
   676  	var (
   677  		configs []charmhub.RefreshConfig
   678  		refBase = charmhub.RefreshBase{
   679  			Architecture: origin.Platform.Architecture,
   680  			Name:         origin.Platform.OS,
   681  			Channel:      origin.Platform.Channel,
   682  		}
   683  	)
   684  	// Get all the resources for everything and just find out which one matches.
   685  	// The order is expected to be kept so when the response is looped through
   686  	// we get channel, then revision.
   687  	if sChan := origin.Channel.String(); sChan != "" {
   688  		cfg, err := charmhub.DownloadOneFromChannel(origin.ID, sChan, refBase)
   689  		if err != nil {
   690  			return configs, errors.Trace(err)
   691  		}
   692  		configs = append(configs, cfg)
   693  	}
   694  	if rev := origin.Revision; rev != nil {
   695  		cfg, err := charmhub.DownloadOneFromRevision(origin.ID, *rev)
   696  		if err != nil {
   697  			return configs, errors.Trace(err)
   698  		}
   699  		if newCfg, ok := charmhub.AddResource(cfg, name, revision); ok {
   700  			cfg = newCfg
   701  		}
   702  		configs = append(configs, cfg)
   703  	}
   704  	if rev := curl.Revision; rev >= 0 {
   705  		cfg, err := charmhub.DownloadOneFromRevision(origin.ID, rev)
   706  		if err != nil {
   707  			return configs, errors.Trace(err)
   708  		}
   709  		if newCfg, ok := charmhub.AddResource(cfg, name, revision); ok {
   710  			cfg = newCfg
   711  		}
   712  		configs = append(configs, cfg)
   713  	}
   714  	return configs, nil
   715  }
   716  
   717  func configsByName(curl *charm.URL, origin corecharm.Origin, name string, revision int) ([]charmhub.RefreshConfig, error) {
   718  	charmName := curl.Name
   719  	var configs []charmhub.RefreshConfig
   720  	// Get all the resource for everything and just find out which one matches.
   721  	// The order is expected to be kept so when the response is looped through
   722  	// we get channel, then revision.
   723  	if sChan := origin.Channel.String(); sChan != "" {
   724  		refBase := charmhub.RefreshBase{
   725  			Architecture: origin.Platform.Architecture,
   726  			Name:         origin.Platform.OS,
   727  			Channel:      origin.Platform.Channel,
   728  		}
   729  		cfg, err := charmhub.DownloadOneFromChannelByName(charmName, sChan, refBase)
   730  		if err != nil {
   731  			return configs, errors.Trace(err)
   732  		}
   733  		configs = append(configs, cfg)
   734  	}
   735  	if rev := origin.Revision; rev != nil {
   736  		cfg, err := charmhub.DownloadOneFromRevisionByName(charmName, *rev)
   737  		if err != nil {
   738  			return configs, errors.Trace(err)
   739  		}
   740  		if newCfg, ok := charmhub.AddResource(cfg, name, revision); ok {
   741  			cfg = newCfg
   742  		}
   743  		configs = append(configs, cfg)
   744  	}
   745  	if rev := curl.Revision; rev >= 0 {
   746  		cfg, err := charmhub.DownloadOneFromRevisionByName(charmName, rev)
   747  		if err != nil {
   748  			return configs, errors.Trace(err)
   749  		}
   750  		if newCfg, ok := charmhub.AddResource(cfg, name, revision); ok {
   751  			cfg = newCfg
   752  		}
   753  		configs = append(configs, cfg)
   754  	}
   755  	return configs, nil
   756  }
   757  
   758  // GetEssentialMetadata resolves each provided MetadataRequest and returns back
   759  // a slice with the results. The results include the minimum set of metadata
   760  // that is required for deploying each charm.
   761  func (c *CharmHubRepository) GetEssentialMetadata(reqs ...corecharm.MetadataRequest) ([]corecharm.EssentialMetadata, error) {
   762  	if len(reqs) == 0 {
   763  		return nil, nil
   764  	}
   765  
   766  	resolvedOrigins := make([]corecharm.Origin, len(reqs))
   767  	refreshCfgs := make([]charmhub.RefreshConfig, len(reqs))
   768  	for reqIdx, req := range reqs {
   769  		// TODO(achilleasa): We should add support for resolving origin
   770  		// batches and move this outside the loop.
   771  		_, resolvedOrigin, _, err := c.ResolveWithPreferredChannel(req.CharmName, req.Origin)
   772  		if err != nil {
   773  			return nil, errors.Annotatef(err, "resolving origin for %q", req.CharmName)
   774  		}
   775  
   776  		refreshCfg, err := refreshConfig(req.CharmName, resolvedOrigin)
   777  		if err != nil {
   778  			return nil, errors.Trace(err)
   779  		}
   780  
   781  		resolvedOrigins[reqIdx] = resolvedOrigin
   782  		refreshCfgs[reqIdx] = refreshCfg
   783  	}
   784  
   785  	refreshResults, err := c.client.Refresh(context.TODO(), charmhub.RefreshMany(refreshCfgs...))
   786  	if err != nil {
   787  		return nil, errors.Trace(err)
   788  	}
   789  
   790  	var metaRes = make([]corecharm.EssentialMetadata, len(reqs))
   791  	for resIdx, refreshResult := range refreshResults {
   792  		essMeta, err := transformRefreshResult(reqs[resIdx].CharmName, refreshResult)
   793  		if err != nil {
   794  			return nil, err
   795  		}
   796  		essMeta.ResolvedOrigin = resolvedOrigins[resIdx]
   797  		metaRes[resIdx] = essMeta
   798  	}
   799  
   800  	return metaRes, nil
   801  }
   802  
   803  func (c *CharmHubRepository) refreshOne(charmName string, origin corecharm.Origin) (transport.RefreshResponse, error) {
   804  	cfg, err := refreshConfig(charmName, origin)
   805  	if err != nil {
   806  		return transport.RefreshResponse{}, errors.Trace(err)
   807  	}
   808  	c.logger.Tracef("Locate charm using: %v", cfg)
   809  	result, err := c.client.Refresh(context.TODO(), cfg)
   810  	if err != nil {
   811  		return transport.RefreshResponse{}, errors.Trace(err)
   812  	}
   813  	if len(result) != 1 {
   814  		return transport.RefreshResponse{}, errors.Errorf("more than 1 result found")
   815  	}
   816  
   817  	return result[0], nil
   818  }
   819  
   820  func (c *CharmHubRepository) selectNextBases(bases []transport.Base, origin corecharm.Origin) ([]corecharm.Platform, error) {
   821  	if len(bases) == 0 {
   822  		return nil, errors.Errorf("no bases available")
   823  	}
   824  	// We've got a invalid charm platform error, the error should contain
   825  	// a valid platform to query again to get the right information. If
   826  	// the platform is empty, consider it a failure.
   827  	var compatible []transport.Base
   828  	for _, base := range bases {
   829  		if base.Architecture != origin.Platform.Architecture {
   830  			continue
   831  		}
   832  		compatible = append(compatible, base)
   833  	}
   834  	if len(compatible) == 0 {
   835  		return nil, errors.NotFoundf("bases matching architecture %q", origin.Platform.Architecture)
   836  	}
   837  
   838  	// Serialize all the platforms into core entities.
   839  	var results []corecharm.Platform
   840  	seen := set.NewStrings()
   841  	for _, base := range compatible {
   842  		platform, err := corecharm.ParsePlatform(fmt.Sprintf("%s/%s/%s", base.Architecture, base.Name, base.Channel))
   843  		if err != nil {
   844  			return nil, errors.Annotate(err, "base")
   845  		}
   846  		if !seen.Contains(platform.String()) {
   847  			seen.Add(platform.String())
   848  			results = append(results, platform)
   849  		}
   850  	}
   851  
   852  	return results, nil
   853  }
   854  
   855  func (c *CharmHubRepository) handleRevisionNotFound(releases []transport.Release, origin corecharm.Origin) error {
   856  	if len(releases) == 0 {
   857  		return errors.Errorf("no releases available")
   858  	}
   859  	// If the user passed in a branch, but not enough information about the
   860  	// arch and channel, then we can help by giving a better error message.
   861  	if origin.Channel != nil && origin.Channel.Branch != "" {
   862  		return errors.Errorf("ambiguous arch and series with channel %q, specify both arch and series along with channel", origin.Channel.String())
   863  	}
   864  	// Help the user out by creating a list of channel/base suggestions to try.
   865  	suggestions := c.composeSuggestions(releases, origin)
   866  	var s string
   867  	if len(suggestions) > 0 {
   868  		s = fmt.Sprintf("\navailable releases are:\n  %v", strings.Join(suggestions, "\n  "))
   869  	}
   870  	// If the origin's channel is nil, one wasn't specified by the user,
   871  	// so we requested "stable", which indicates the charm's default channel.
   872  	// However, at the time we're writing this message, we do not know what
   873  	// the charm's default channel is.
   874  	var channelString string
   875  	if origin.Channel != nil {
   876  		channelString = fmt.Sprintf("for channel %q", origin.Channel.String())
   877  	} else {
   878  		channelString = "in the charm's default channel"
   879  	}
   880  
   881  	return errSelection{
   882  		err: errors.Errorf(
   883  			"charm or bundle not found %s, base %q%s",
   884  			channelString, origin.Platform.String(), s),
   885  	}
   886  }
   887  
   888  type errSelection struct {
   889  	err error
   890  }
   891  
   892  func (e errSelection) Error() string {
   893  	return e.err.Error()
   894  }
   895  
   896  func isErrSelection(err error) bool {
   897  	_, ok := errors.Cause(err).(errSelection)
   898  	return ok
   899  }
   900  
   901  // Method describes the method for requesting the charm using the RefreshAPI.
   902  type Method string
   903  
   904  const (
   905  	// MethodRevision utilizes an install action by the revision only. A
   906  	// channel must be in the origin, however it's not used in this request,
   907  	// but saved in the origin for future use.
   908  	MethodRevision Method = "revision"
   909  	// MethodChannel utilizes an install action by the channel only.
   910  	MethodChannel Method = "channel"
   911  	// MethodID utilizes an refresh action by the id, revision and
   912  	// channel (falls back to latest/stable if channel is not found).
   913  	MethodID Method = "id"
   914  )
   915  
   916  // refreshConfig creates a RefreshConfig for the given input.
   917  // If the origin.ID is not set, a install refresh config is returned. For
   918  // install. Channel and Revision are mutually exclusive in the api, only
   919  // one will be used.
   920  //
   921  // If the origin.ID is set, a refresh config is returned.
   922  //
   923  // NOTE: There is one idiosyncrasy of this method.  The charm URL and and
   924  // origin have a revision number in them when called by GetDownloadURL
   925  // to install a charm. Potentially causing an unexpected install by revision.
   926  // This is okay as all of the data is ready and correct in the origin.
   927  func refreshConfig(charmName string, origin corecharm.Origin) (charmhub.RefreshConfig, error) {
   928  	// Work out the correct install method.
   929  	rev := -1
   930  	var method Method
   931  	if origin.Revision != nil && *origin.Revision >= 0 {
   932  		rev = *origin.Revision
   933  	}
   934  	if origin.ID == "" && rev != -1 {
   935  		method = MethodRevision
   936  	}
   937  
   938  	var (
   939  		channel         string
   940  		nonEmptyChannel = origin.Channel != nil && !origin.Channel.Empty()
   941  	)
   942  
   943  	// Select the appropriate channel based on the supplied origin.
   944  	// We need to ensure that we always, always normalize the incoming channel
   945  	// before we hit the refresh API.
   946  	if nonEmptyChannel {
   947  		channel = origin.Channel.Normalize().String()
   948  	} else if method != MethodRevision {
   949  		channel = corecharm.DefaultChannel.Normalize().String()
   950  	}
   951  
   952  	if method != MethodRevision && channel != "" {
   953  		method = MethodChannel
   954  	}
   955  
   956  	// Bundles can not use method IDs, which in turn forces a refresh.
   957  	if !transport.BundleType.Matches(origin.Type) && origin.ID != "" {
   958  		method = MethodID
   959  	}
   960  
   961  	var (
   962  		cfg charmhub.RefreshConfig
   963  		err error
   964  
   965  		base = charmhub.RefreshBase{
   966  			Architecture: origin.Platform.Architecture,
   967  			Name:         origin.Platform.OS,
   968  			Channel:      origin.Platform.Channel,
   969  		}
   970  	)
   971  	switch method {
   972  	case MethodChannel:
   973  		// Install from just the name and the channel. If there is no origin ID,
   974  		// we haven't downloaded this charm before.
   975  		// Try channel first.
   976  		cfg, err = charmhub.InstallOneFromChannel(charmName, channel, base)
   977  	case MethodRevision:
   978  		// If there is a revision, install it using that. If there is no origin
   979  		// ID, we haven't downloaded this charm before.
   980  		cfg, err = charmhub.InstallOneFromRevision(charmName, rev)
   981  	case MethodID:
   982  		// This must be a charm upgrade if we have an ID.  Use the refresh
   983  		// action for metric keeping on the CharmHub side.
   984  		cfg, err = charmhub.RefreshOne(origin.InstanceKey, origin.ID, rev, channel, base)
   985  	default:
   986  		return nil, errors.NotValidf("origin %v", origin)
   987  	}
   988  	return cfg, err
   989  }
   990  
   991  func (c *CharmHubRepository) composeSuggestions(releases []transport.Release, origin corecharm.Origin) []string {
   992  	charmRisks := set.NewStrings()
   993  	for _, v := range charm.Risks {
   994  		charmRisks.Add(string(v))
   995  	}
   996  	channelSeries := make(map[string][]string)
   997  	for _, release := range releases {
   998  		arch := release.Base.Architecture
   999  		if arch == "all" {
  1000  			arch = origin.Platform.Architecture
  1001  		}
  1002  		if arch != origin.Platform.Architecture {
  1003  			continue
  1004  		}
  1005  		var (
  1006  			base corebase.Base
  1007  			err  error
  1008  		)
  1009  
  1010  		channel, err := corebase.ParseChannel(release.Base.Channel)
  1011  		if err != nil {
  1012  			c.logger.Errorf("invalid base channel %v: %s", release.Base.Channel, err)
  1013  			continue
  1014  		}
  1015  		if channel.Track == "all" || release.Base.Name == "all" {
  1016  			base, err = corebase.ParseBase(origin.Platform.OS, origin.Platform.Channel)
  1017  		} else {
  1018  			base, err = corebase.ParseBase(release.Base.Name, release.Base.Channel)
  1019  		}
  1020  		if err != nil {
  1021  			c.logger.Errorf("converting version to base: %s", err)
  1022  			continue
  1023  		}
  1024  		// Now that we have default tracks other than latest:
  1025  		// If a channel is risk only, add latest as the track
  1026  		// to be more clear for the user facing error message.
  1027  		// At this point, we do not know the default channel,
  1028  		// or if the charm has one, therefore risk only output
  1029  		// is ambiguous.
  1030  		charmChannel := release.Channel
  1031  		if charmRisks.Contains(charmChannel) {
  1032  			charmChannel = "latest/" + charmChannel
  1033  		}
  1034  		channelSeries[charmChannel] = append(channelSeries[charmChannel], base.DisplayString())
  1035  	}
  1036  
  1037  	var suggestions []string
  1038  	for channel, values := range channelSeries {
  1039  		suggestions = append(suggestions, fmt.Sprintf("channel %q: available bases are: %s", channel, strings.Join(values, ", ")))
  1040  	}
  1041  	return suggestions
  1042  }
  1043  
  1044  func resourceFromRevision(rev transport.ResourceRevision) (charmresource.Resource, error) {
  1045  	resType, err := charmresource.ParseType(rev.Type)
  1046  	if err != nil {
  1047  		return charmresource.Resource{}, errors.Trace(err)
  1048  	}
  1049  	fp, err := charmresource.ParseFingerprint(rev.Download.HashSHA384)
  1050  	if err != nil {
  1051  		return charmresource.Resource{}, errors.Trace(err)
  1052  	}
  1053  	r := charmresource.Resource{
  1054  		Meta: charmresource.Meta{
  1055  			Name:        rev.Name,
  1056  			Type:        resType,
  1057  			Path:        rev.Filename,
  1058  			Description: rev.Description,
  1059  		},
  1060  		Origin:      charmresource.OriginStore,
  1061  		Revision:    rev.Revision,
  1062  		Fingerprint: fp,
  1063  		Size:        int64(rev.Download.Size),
  1064  	}
  1065  	return r, nil
  1066  }