Skip to content

Commit

Permalink
Add new installByRevisionOne which implements RefreshConfig.
Browse files Browse the repository at this point in the history
InstallByRevision is too different from the other configs using
executeOne, thus need a new one: installByRevisionOne.  No base nor
channel are necessary.  Configs using executeOne do not require the
Fields.

Remove base from InstallByRevision args, it's not sent via the api.

Move RefreshConfig and all structs implementing it to a new file for
readability.
  • Loading branch information
hmlanigan committed Oct 22, 2021
1 parent d492493 commit a9c3ce3
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 278 deletions.
252 changes: 20 additions & 232 deletions charmhub/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,6 @@ func NewRefreshClient(path path.Path, client RESTClient, logger Logger) *Refresh
}
}

// RefreshConfig defines a type for building refresh requests.
type RefreshConfig interface {
// Build a refresh request for sending to the API.
Build() (transport.RefreshRequest, error)

// Ensure that the request back contains the information we requested.
Ensure([]transport.RefreshResponse) error

// String describes the underlying refresh config.
String() string
}

// Refresh is used to refresh installed charms to a more suitable revision.
func (c *RefreshClient) Refresh(ctx context.Context, config RefreshConfig) ([]transport.RefreshResponse, error) {
c.logger.Tracef("Refresh(%s)", pretty.Sprint(config))
Expand Down Expand Up @@ -178,28 +166,6 @@ func (c *RefreshClient) refresh(ctx context.Context, ensure func(responses []tra
return resp.Results, ensure(resp.Results)
}

// refreshOne holds the config for making refresh calls to the CharmHub API.
type refreshOne struct {
ID string
Revision int
Channel string
Base RefreshBase
// instanceKey is a private unique key that we construct for CharmHub API
// asynchronous calls.
instanceKey string
metrics transport.ContextMetrics
}

// InstanceKey returns the underlying instance key.
func (c refreshOne) InstanceKey() string {
return c.instanceKey
}

func (c refreshOne) String() string {
return fmt.Sprintf("Refresh one (instanceKey: %s): using ID %s revision %+v, with channel %s and base %v",
c.instanceKey, c.ID, c.Revision, c.Channel, c.Base.String())
}

// RefreshOne creates a request config for requesting only one charm.
func RefreshOne(key, id string, revision int, channel string, base RefreshBase) (RefreshConfig, error) {
if key == "" {
Expand Down Expand Up @@ -233,80 +199,39 @@ func CreateInstanceKey(app names.ApplicationTag, model names.ModelTag) string {
return base64.RawURLEncoding.EncodeToString(h)
}

// Build a refresh request that can be past to the API.
func (c refreshOne) Build() (transport.RefreshRequest, error) {
base, err := constructRefreshBase(c.Base)
if err != nil {
return transport.RefreshRequest{}, errors.Trace(err)
}

return transport.RefreshRequest{
Context: []transport.RefreshRequestContext{{
InstanceKey: c.instanceKey,
ID: c.ID,
Revision: c.Revision,
Base: base,
TrackingChannel: c.Channel,
Metrics: c.metrics,
// TODO (stickupkid): We need to model the refreshed date. It's
// currently optional, but will be required at some point. This
// is the installed date of the charm on the system.
}},
Actions: []transport.RefreshRequestAction{{
Action: string(RefreshAction),
InstanceKey: c.instanceKey,
ID: &c.ID,
}},
}, nil
}

// Ensure that the request back contains the information we requested.
func (c refreshOne) Ensure(responses []transport.RefreshResponse) error {
for _, resp := range responses {
if resp.InstanceKey == c.instanceKey {
return nil
}
}
return errors.NotValidf("refresh action key")
}

type executeOne struct {
ID string
Name string
Revision *int
Channel *string
Base RefreshBase
// instanceKey is a private unique key that we construct for CharmHub API
// asynchronous calls.
action Action
instanceKey string
resourceRevisions []transport.RefreshResourceRevision
}

// InstanceKey returns the underlying instance key.
func (c executeOne) InstanceKey() string {
return c.instanceKey
}

// InstallOneFromRevision creates a request config using the revision and not
// the channel for requesting only one charm.
func InstallOneFromRevision(name string, revision int, base RefreshBase) (RefreshConfig, error) {
if err := validateBase(base); err != nil {
return nil, errors.Trace(err)
}
func InstallOneFromRevision(name string, revision int) (RefreshConfig, error) {
uuid, err := utils.NewUUID()
if err != nil {
return nil, errors.Trace(err)
}
return executeOne{
return installByRevisionOne{
action: InstallAction,
instanceKey: uuid.String(),
Name: name,
Revision: &revision,
Base: base,
}, nil
}

// AddResource adds resource revision data to a executeOne config.
// Used for install by revision.
func AddResource(config RefreshConfig, name string, revision int) (RefreshConfig, bool) {
c, ok := config.(installByRevisionOne)
if !ok {
return config, false
}
if len(c.resourceRevisions) == 0 {
c.resourceRevisions = make([]transport.RefreshResourceRevision, 0)
}
c.resourceRevisions = append(c.resourceRevisions, transport.RefreshResourceRevision{
Name: name,
Revision: revision,
})
return c, true

}

// AddConfigMetrics adds metrics to a refreshOne config. All values are
// applied at once, subsequent calls, replace all values.
func AddConfigMetrics(config RefreshConfig, metrics map[charmmetrics.MetricKey]string) (RefreshConfig, error) {
Expand Down Expand Up @@ -400,143 +325,6 @@ func DownloadOneFromChannel(id string, channel string, base RefreshBase) (Refres
}, nil
}

// Build a refresh request that can be past to the API.
func (c executeOne) Build() (transport.RefreshRequest, error) {
base, err := constructRefreshBase(c.Base)
if err != nil {
return transport.RefreshRequest{}, errors.Trace(err)
}

var id *string
if c.ID != "" {
id = &c.ID
}
var name *string
if c.Name != "" {
name = &c.Name
}

req := transport.RefreshRequest{
// Context is required here, even if it looks optional.
Context: []transport.RefreshRequestContext{},
Actions: []transport.RefreshRequestAction{{
Action: string(c.action),
InstanceKey: c.instanceKey,
ID: id,
Name: name,
Base: &base,
ResourceRevisions: c.resourceRevisions,
}},
Fields: []string{"bases", "download", "id", "revision", "version", "resources"},
}
if c.Revision != nil {
req.Actions[0].Revision = c.Revision
} else if c.Channel != nil {
req.Actions[0].Revision = c.Revision
req.Actions[0].Channel = c.Channel
}
return req, nil
}

// Ensure that the request back contains the information we requested.
func (c executeOne) Ensure(responses []transport.RefreshResponse) error {
for _, resp := range responses {
if resp.InstanceKey == c.instanceKey {
return nil
}
}
return errors.NotValidf("%v action key", string(c.action))
}

func (c executeOne) String() string {
var channel string
if c.Channel != nil {
channel = *c.Channel
}
var using string
if c.ID != "" {
using = fmt.Sprintf("ID %s", c.ID)
} else {
using = fmt.Sprintf("Name %s", c.Name)
}
var revision string
if c.Revision != nil {
revision = fmt.Sprintf(" with revision: %+v", c.Revision)
}
return fmt.Sprintf("Execute One (action: %s, instanceKey: %s): using %s%s channel %v and base %s",
c.action, c.instanceKey, using, revision, channel, c.Base)
}

// AddResource adds resource revision data to a executeOne config.
// Used for install by revision.
func AddResource(config RefreshConfig, name string, revision int) (RefreshConfig, bool) {
c, ok := config.(executeOne)
if !ok {
return config, false
}
if len(c.resourceRevisions) == 0 {
c.resourceRevisions = make([]transport.RefreshResourceRevision, 0)
}
c.resourceRevisions = append(c.resourceRevisions, transport.RefreshResourceRevision{
Name: name,
Revision: revision,
})
return c, true
}

type refreshMany struct {
Configs []RefreshConfig
}

// RefreshMany will compose many refresh configs.
func RefreshMany(configs ...RefreshConfig) RefreshConfig {
return refreshMany{
Configs: configs,
}
}

// Build a refresh request that can be past to the API.
func (c refreshMany) Build() (transport.RefreshRequest, error) {
if len(c.Configs) == 0 {
return transport.RefreshRequest{}, errors.NotFoundf("configs")
}

// Not all configs built here have a context, start out with an empty
// slice, so we do not call Refresh with a nil context.
// See executeOne.Build().
result := transport.RefreshRequest{
Context: []transport.RefreshRequestContext{},
}
for _, config := range c.Configs {
req, err := config.Build()
if err != nil {
return transport.RefreshRequest{}, errors.Trace(err)
}
result.Context = append(result.Context, req.Context...)
result.Actions = append(result.Actions, req.Actions...)
result.Fields = append(result.Fields, req.Fields...)
}
return result, nil
}

// Ensure that the request back contains the information we requested.
func (c refreshMany) Ensure(responses []transport.RefreshResponse) error {
for _, config := range c.Configs {
if err := config.Ensure(responses); err != nil {
return errors.Annotatef(err, "missing response")
}
}
return nil
}

func (c refreshMany) String() string {
plans := make([]string, len(c.Configs))
for i, config := range c.Configs {
plans[i] = config.String()
}
return strings.Join(plans, "\n")
}

// constructRefreshBase creates a refresh request base that allows for
// partial base queries.
func constructRefreshBase(base RefreshBase) (transport.Base, error) {
Expand Down
22 changes: 5 additions & 17 deletions charmhub/refresh_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,7 @@ func (s *RefreshClientSuite) TestLiveRefreshManyRequest(c *gc.C) {
func (s *RefreshClientSuite) TestLiveInstallRequest(c *gc.C) {
client := refreshClient(c)

charmConfig, err := InstallOneFromRevision("wordpress", 0, RefreshBase{
Name: "ubuntu",
Channel: "18.04",
Architecture: "amd64",
})
charmConfig, err := InstallOneFromRevision("wordpress", 0)
c.Assert(err, jc.ErrorIsNil)

response, err := client.Refresh(context.TODO(), charmConfig)
Expand All @@ -88,11 +84,7 @@ func (s *RefreshClientSuite) TestLiveInstallRequest(c *gc.C) {
func (s *RefreshClientSuite) TestLiveInstallRequestNoBase(c *gc.C) {
client := refreshClient(c)

charmConfig, err := InstallOneFromRevision("wordpress", 0, RefreshBase{
Architecture: "NA",
Name: "NA",
Channel: "NA",
})
charmConfig, err := InstallOneFromRevision("wordpress", 0)
c.Assert(err, jc.ErrorIsNil)

response, err := client.Refresh(context.TODO(), charmConfig)
Expand All @@ -104,11 +96,7 @@ func (s *RefreshClientSuite) TestLiveInstallRequestNoBase(c *gc.C) {
func (s *RefreshClientSuite) TestLiveInstallRequestWithResourceRevisions(c *gc.C) {
client := refreshClient(c)

charmConfig, err := InstallOneFromRevision("prometheus-ceph-exporter", 13, RefreshBase{
Architecture: "NA",
Name: "NA",
Channel: "NA",
})
charmConfig, err := InstallOneFromRevision("prometheus-ceph-exporter", 13)
c.Assert(err, jc.ErrorIsNil)

charmConfig, ok := AddResource(charmConfig, "core", 0)
Expand All @@ -135,7 +123,7 @@ func defineID(c *gc.C, config RefreshConfig, id string) RefreshConfig {
return nil
}

func refreshClient(c *gc.C) *RefreshClient{
func refreshClient(c *gc.C) *RefreshClient {
logger := &FakeLogger{}

config, err := CharmHubConfig(logger)
Expand All @@ -150,4 +138,4 @@ func refreshClient(c *gc.C) *RefreshClient{
restClient := NewHTTPRESTClient(apiRequester, nil)

return NewRefreshClient(refreshPath, restClient, logger)
}
}
Loading

0 comments on commit a9c3ce3

Please sign in to comment.