github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/controller/charmrevisionupdater/charmhub.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // TODO(benhoyt) - also add caching and retries
     5  
     6  package charmrevisionupdater
     7  
     8  import (
     9  	"context"
    10  	"time"
    11  
    12  	"github.com/juju/charm/v12/resource"
    13  	"github.com/juju/errors"
    14  
    15  	"github.com/juju/juju/charmhub"
    16  	"github.com/juju/juju/charmhub/transport"
    17  	"github.com/juju/juju/core/charm/metrics"
    18  )
    19  
    20  // charmhubID holds identifying information for several charms for a
    21  // charmhubLatestCharmInfo call.
    22  type charmhubID struct {
    23  	id        string
    24  	revision  int
    25  	channel   string
    26  	osType    string
    27  	osChannel string
    28  	arch      string
    29  	metrics   map[metrics.MetricKey]string
    30  	// Required for charmhub only.  instanceKey is a unique string associated
    31  	// with the application. To assist with keeping KPI data in charmhub, it
    32  	// must be the same for every charmhub Refresh action related to an
    33  	// application. Create with the charmhub.CreateInstanceKey method.
    34  	// LP: 1944582
    35  	instanceKey string
    36  }
    37  
    38  // charmhubResult is the type charmhubLatestCharmInfo returns: information
    39  // about a charm revision and its resources.
    40  type charmhubResult struct {
    41  	name      string
    42  	timestamp time.Time
    43  	revision  int
    44  	resources []resource.Resource
    45  	error     error
    46  }
    47  
    48  // CharmhubRefreshClient is an interface for the methods of the charmhub
    49  // client that we need.
    50  type CharmhubRefreshClient interface {
    51  	RefreshWithRequestMetrics(ctx context.Context, config charmhub.RefreshConfig, metrics map[metrics.MetricKey]map[metrics.MetricKey]string) ([]transport.RefreshResponse, error)
    52  	RefreshWithMetricsOnly(ctx context.Context, metrics map[metrics.MetricKey]map[metrics.MetricKey]string) error
    53  }
    54  
    55  // charmhubLatestCharmInfo fetches the latest information about the given
    56  // charms from charmhub's "charm_refresh" API.
    57  func charmhubLatestCharmInfo(client CharmhubRefreshClient, metrics map[metrics.MetricKey]map[metrics.MetricKey]string, ids []charmhubID, now time.Time) ([]charmhubResult, error) {
    58  	cfgs := make([]charmhub.RefreshConfig, len(ids))
    59  	for i, id := range ids {
    60  		base := charmhub.RefreshBase{
    61  			Architecture: id.arch,
    62  			Name:         id.osType,
    63  			Channel:      id.osChannel,
    64  		}
    65  		cfg, err := charmhub.RefreshOne(id.instanceKey, id.id, id.revision, id.channel, base)
    66  		if err != nil {
    67  			return nil, errors.Trace(err)
    68  		}
    69  		cfg, err = charmhub.AddConfigMetrics(cfg, id.metrics)
    70  		if err != nil {
    71  			return nil, errors.Trace(err)
    72  		}
    73  		cfgs[i] = cfg
    74  	}
    75  	config := charmhub.RefreshMany(cfgs...)
    76  
    77  	ctx, cancel := context.WithTimeout(context.TODO(), charmhub.RefreshTimeout)
    78  	defer cancel()
    79  	responses, err := client.RefreshWithRequestMetrics(ctx, config, metrics)
    80  	if err != nil {
    81  		return nil, errors.Trace(err)
    82  	}
    83  
    84  	results := make([]charmhubResult, len(responses))
    85  	for i, response := range responses {
    86  		results[i] = refreshResponseToCharmhubResult(response, now)
    87  	}
    88  	return results, nil
    89  }
    90  
    91  // refreshResponseToCharmhubResult converts a raw RefreshResponse from the
    92  // charmhub API into a charmhubResult.
    93  func refreshResponseToCharmhubResult(response transport.RefreshResponse, now time.Time) charmhubResult {
    94  	if response.Error != nil {
    95  		return charmhubResult{
    96  			error: errors.Errorf("charmhub API error %s: %s", response.Error.Code, response.Error.Message),
    97  		}
    98  	}
    99  	var resources []resource.Resource
   100  	for _, r := range response.Entity.Resources {
   101  		fingerprint, err := resource.ParseFingerprint(r.Download.HashSHA384)
   102  		if err != nil {
   103  			logger.Warningf("invalid resource fingerprint %q: %v", r.Download.HashSHA384, err)
   104  			continue
   105  		}
   106  		typ, err := resource.ParseType(r.Type)
   107  		if err != nil {
   108  			logger.Warningf("invalid resource type %q: %v", r.Type, err)
   109  			continue
   110  		}
   111  		res := resource.Resource{
   112  			Meta: resource.Meta{
   113  				Name:        r.Name,
   114  				Type:        typ,
   115  				Path:        r.Filename,
   116  				Description: r.Description,
   117  			},
   118  			Origin:      resource.OriginStore,
   119  			Revision:    r.Revision,
   120  			Fingerprint: fingerprint,
   121  			Size:        int64(r.Download.Size),
   122  		}
   123  		resources = append(resources, res)
   124  	}
   125  	return charmhubResult{
   126  		name:      response.Name,
   127  		timestamp: now,
   128  		revision:  response.Entity.Revision,
   129  		resources: resources,
   130  	}
   131  }