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

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charmhub
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  
    13  	"github.com/juju/juju/charmhub/transport"
    14  )
    15  
    16  // RefreshConfig defines a type for building refresh requests.
    17  type RefreshConfig interface {
    18  	// Build a refresh request for sending to the API.
    19  	Build() (transport.RefreshRequest, error)
    20  
    21  	// Ensure that the request back contains the information we requested.
    22  	Ensure([]transport.RefreshResponse) error
    23  
    24  	// String describes the underlying refresh config.
    25  	String() string
    26  }
    27  
    28  // refreshOne holds the config for making refresh calls to the CharmHub API.
    29  type refreshOne struct {
    30  	ID       string
    31  	Revision int
    32  	Channel  string
    33  	Base     RefreshBase
    34  	// instanceKey is a private unique key that we construct for CharmHub API
    35  	// asynchronous calls.
    36  	instanceKey string
    37  	metrics     transport.ContextMetrics
    38  	fields      []string
    39  }
    40  
    41  // InstanceKey returns the underlying instance key.
    42  func (c refreshOne) InstanceKey() string {
    43  	return c.instanceKey
    44  }
    45  
    46  func (c refreshOne) String() string {
    47  	return fmt.Sprintf("Refresh one (instanceKey: %s): using ID %s revision %+v, with channel %s and base %v",
    48  		c.instanceKey, c.ID, c.Revision, c.Channel, c.Base.String())
    49  }
    50  
    51  // Build a refresh request that can be past to the API.
    52  func (c refreshOne) Build() (transport.RefreshRequest, error) {
    53  	base, err := constructRefreshBase(c.Base)
    54  	if err != nil {
    55  		return transport.RefreshRequest{}, errors.Trace(err)
    56  	}
    57  
    58  	return transport.RefreshRequest{
    59  		Context: []transport.RefreshRequestContext{{
    60  			InstanceKey:     c.instanceKey,
    61  			ID:              c.ID,
    62  			Revision:        c.Revision,
    63  			Base:            base,
    64  			TrackingChannel: c.Channel,
    65  			Metrics:         c.metrics,
    66  			// TODO (stickupkid): We need to model the refreshed date. It's
    67  			// currently optional, but will be required at some point. This
    68  			// is the installed date of the charm on the system.
    69  		}},
    70  		Actions: []transport.RefreshRequestAction{{
    71  			Action:      string(refreshAction),
    72  			InstanceKey: c.instanceKey,
    73  			ID:          &c.ID,
    74  		}},
    75  		Fields: c.fields,
    76  	}, nil
    77  }
    78  
    79  // Ensure that the request back contains the information we requested.
    80  func (c refreshOne) Ensure(responses []transport.RefreshResponse) error {
    81  	for _, resp := range responses {
    82  		if resp.InstanceKey == c.instanceKey {
    83  			return nil
    84  		}
    85  	}
    86  	return errors.NotValidf("refresh action key")
    87  }
    88  
    89  type executeOne struct {
    90  	ID       string
    91  	Name     string
    92  	Revision *int
    93  	Channel  *string
    94  	Base     RefreshBase
    95  	// instanceKey is a private unique key that we construct for CharmHub API
    96  	// asynchronous calls.
    97  	action      action
    98  	instanceKey string
    99  	fields      []string
   100  }
   101  
   102  // InstanceKey returns the underlying instance key.
   103  func (c executeOne) InstanceKey() string {
   104  	return c.instanceKey
   105  }
   106  
   107  // Build a refresh request that can be past to the API.
   108  func (c executeOne) Build() (transport.RefreshRequest, error) {
   109  	base, err := constructRefreshBase(c.Base)
   110  	if err != nil {
   111  		return transport.RefreshRequest{}, errors.Trace(err)
   112  	}
   113  
   114  	var id *string
   115  	if c.ID != "" {
   116  		id = &c.ID
   117  	}
   118  	var name *string
   119  	if c.Name != "" {
   120  		name = &c.Name
   121  	}
   122  
   123  	req := transport.RefreshRequest{
   124  		// Context is required here, even if it looks optional.
   125  		Context: []transport.RefreshRequestContext{},
   126  		Actions: []transport.RefreshRequestAction{{
   127  			Action:      string(c.action),
   128  			InstanceKey: c.instanceKey,
   129  			ID:          id,
   130  			Name:        name,
   131  			Revision:    c.Revision,
   132  			Channel:     c.Channel,
   133  			Base:        &base,
   134  		}},
   135  		Fields: c.fields,
   136  	}
   137  	return req, nil
   138  }
   139  
   140  // Ensure that the request back contains the information we requested.
   141  func (c executeOne) Ensure(responses []transport.RefreshResponse) error {
   142  	for _, resp := range responses {
   143  		if resp.InstanceKey == c.instanceKey {
   144  			return nil
   145  		}
   146  	}
   147  	return errors.NotValidf("%v action key", string(c.action))
   148  }
   149  
   150  func (c executeOne) String() string {
   151  	var channel string
   152  	if c.Channel != nil {
   153  		channel = *c.Channel
   154  	}
   155  	var using string
   156  	if c.ID != "" {
   157  		using = fmt.Sprintf("ID %s", c.ID)
   158  	} else {
   159  		using = fmt.Sprintf("Name %s", c.Name)
   160  	}
   161  	var revision string
   162  	if c.Revision != nil {
   163  		revision = fmt.Sprintf(" with revision: %+v", c.Revision)
   164  	}
   165  	return fmt.Sprintf("Execute One (action: %s, instanceKey: %s): using %s%s channel %v and base %s",
   166  		c.action, c.instanceKey, using, revision, channel, c.Base)
   167  }
   168  
   169  type executeOneByRevision struct {
   170  	Name     string
   171  	Revision *int
   172  	// ID is only used for download by revision
   173  	ID                string
   174  	resourceRevisions []transport.RefreshResourceRevision
   175  	// instanceKey is a private unique key that we construct for CharmHub API
   176  	// asynchronous calls.
   177  	instanceKey string
   178  	action      action
   179  	fields      []string
   180  }
   181  
   182  // InstanceKey returns the underlying instance key.
   183  func (c executeOneByRevision) InstanceKey() string {
   184  	return c.instanceKey
   185  }
   186  
   187  // Build a refresh request for sending to the API.
   188  func (c executeOneByRevision) Build() (transport.RefreshRequest, error) {
   189  	var name, id *string
   190  	if c.Name != "" {
   191  		name = &c.Name
   192  	}
   193  	if c.ID != "" {
   194  		id = &c.ID
   195  	}
   196  
   197  	req := transport.RefreshRequest{
   198  		// Context is required here, even if it looks optional.
   199  		Context: []transport.RefreshRequestContext{},
   200  		Actions: []transport.RefreshRequestAction{{
   201  			Action:            string(c.action),
   202  			InstanceKey:       c.instanceKey,
   203  			Name:              name,
   204  			ID:                id,
   205  			Revision:          c.Revision,
   206  			ResourceRevisions: c.resourceRevisions,
   207  		}},
   208  		Fields: []string{"bases", "download", "id", "revision", "version", "resources", "type"},
   209  	}
   210  
   211  	if len(c.fields) != 0 {
   212  		fieldSet := set.NewStrings(req.Fields...)
   213  		for _, field := range c.fields {
   214  			fieldSet.Add(field)
   215  		}
   216  		req.Fields = fieldSet.SortedValues()
   217  	}
   218  
   219  	return req, nil
   220  }
   221  
   222  // Ensure that the request back contains the information we requested.
   223  func (c executeOneByRevision) Ensure(responses []transport.RefreshResponse) error {
   224  	for _, resp := range responses {
   225  		if resp.InstanceKey == c.instanceKey {
   226  			return nil
   227  		}
   228  	}
   229  	return errors.NotValidf("%v action key", string(c.action))
   230  }
   231  
   232  // String describes the underlying refresh config.
   233  func (c executeOneByRevision) String() string {
   234  	var revision string
   235  	if c.Revision != nil {
   236  		revision = fmt.Sprintf(" with revision: %+v", c.Revision)
   237  	}
   238  	return fmt.Sprintf("Install One (action: %s, instanceKey: %s): using Name %s %s",
   239  		c.action, c.instanceKey, c.Name, revision)
   240  }
   241  
   242  type refreshMany struct {
   243  	Configs []RefreshConfig
   244  }
   245  
   246  // RefreshMany will compose many refresh configs.
   247  func RefreshMany(configs ...RefreshConfig) RefreshConfig {
   248  	return refreshMany{
   249  		Configs: configs,
   250  	}
   251  }
   252  
   253  // Build a refresh request that can be past to the API.
   254  func (c refreshMany) Build() (transport.RefreshRequest, error) {
   255  	if len(c.Configs) == 0 {
   256  		return transport.RefreshRequest{}, errors.NotFoundf("configs")
   257  	}
   258  	// Not all configs built here have a context, start out with an empty
   259  	// slice, so we do not call Refresh with a nil context.
   260  	// See executeOne.Build().
   261  	result := transport.RefreshRequest{
   262  		Context: []transport.RefreshRequestContext{},
   263  	}
   264  	for _, config := range c.Configs {
   265  		req, err := config.Build()
   266  		if err != nil {
   267  			return transport.RefreshRequest{}, errors.Trace(err)
   268  		}
   269  		result.Context = append(result.Context, req.Context...)
   270  		result.Actions = append(result.Actions, req.Actions...)
   271  		result.Fields = append(result.Fields, req.Fields...)
   272  	}
   273  
   274  	// Ensure that the required field list contains no duplicates
   275  	if len(result.Fields) != 0 {
   276  		result.Fields = set.NewStrings(result.Fields...).SortedValues()
   277  	}
   278  
   279  	return result, nil
   280  }
   281  
   282  // Ensure that the request back contains the information we requested.
   283  func (c refreshMany) Ensure(responses []transport.RefreshResponse) error {
   284  	for _, config := range c.Configs {
   285  		if err := config.Ensure(responses); err != nil {
   286  			return errors.Annotatef(err, "missing response")
   287  		}
   288  	}
   289  	return nil
   290  }
   291  
   292  func (c refreshMany) String() string {
   293  	plans := make([]string, len(c.Configs))
   294  	for i, config := range c.Configs {
   295  		plans[i] = config.String()
   296  	}
   297  	return strings.Join(plans, "\n")
   298  }