Skip to content

Commit

Permalink
Charmhub metrics work, to be used via Refresh API.
Browse files Browse the repository at this point in the history
Update RefreshRequest and RefreshRequestContext to have metrics
attached.

Add RefreshWithRequestMetrics to send a Refresh command with Request
level metrics for model and controller.

Add AddConfigMetrics to add metrics to a specific RefreshContext.
Including update of refreshOne to include these metrics.
  • Loading branch information
hmlanigan committed Oct 5, 2021
1 parent f74b398 commit 2f6c9f9
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 7 deletions.
11 changes: 9 additions & 2 deletions charmhub/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

charmhubpath "github.com/juju/juju/charmhub/path"
"github.com/juju/juju/charmhub/transport"
charmmetrics "github.com/juju/juju/core/charm/metrics"
corelogger "github.com/juju/juju/core/logger"
"github.com/juju/juju/version"
)
Expand Down Expand Up @@ -269,12 +270,18 @@ func (c *Client) Find(ctx context.Context, name string, options ...FindOption) (
return c.findClient.Find(ctx, name, options...)
}

// Refresh defines a client for making refresh API calls, that allow for
// updating a series of charms to the latest version.
// Refresh defines a client for making refresh API calls with different actions.
func (c *Client) Refresh(ctx context.Context, config RefreshConfig) ([]transport.RefreshResponse, error) {
return c.refreshClient.Refresh(ctx, config)
}

// RefreshWithRequestMetrics defines a client for making refresh API calls.
// Specifically to use the refresh action and provide metrics. Intended for
// use in the charm revision updater facade only. Otherwise use Refresh.
func (c *Client) RefreshWithRequestMetrics(ctx context.Context, config RefreshConfig, metrics map[charmmetrics.MetricKey]map[charmmetrics.MetricKey]string) ([]transport.RefreshResponse, error) {
return c.refreshClient.RefreshWithRequestMetrics(ctx, config, metrics)
}

// Download defines a client for downloading charms directly.
func (c *Client) Download(ctx context.Context, resourceURL *url.URL, archivePath string, options ...DownloadOption) error {
return c.downloadClient.Download(ctx, resourceURL, archivePath, options...)
Expand Down
49 changes: 48 additions & 1 deletion charmhub/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/juju/juju/charmhub/path"
"github.com/juju/juju/charmhub/transport"
charmmetrics "github.com/juju/juju/core/charm/metrics"
coreseries "github.com/juju/juju/core/series"
)

Expand Down Expand Up @@ -99,7 +100,34 @@ func (c *RefreshClient) Refresh(ctx context.Context, config RefreshConfig) ([]tr
if err != nil {
return nil, errors.Trace(err)
}
return c.refresh(ctx, config.Ensure, req, headers)
}

// RefreshWithRequestMetrics is to get refreshed charm data and provide metrics
// at the same time. Used as part of the charm revision updater facade.
func (c *RefreshClient) RefreshWithRequestMetrics(ctx context.Context, config RefreshConfig, metrics map[charmmetrics.MetricKey]map[charmmetrics.MetricKey]string) ([]transport.RefreshResponse, error) {
c.logger.Tracef("RefreshWithRequestMetrics(%s, %+v)", pretty.Sprint(config), metrics)
req, headers, err := config.Build()
if err != nil {
return nil, errors.Trace(err)
}
m := make(transport.RequestMetrics, 0)
for k, v := range metrics {
// verify top level "model" and "controller" keys
if k != charmmetrics.Controller && k != charmmetrics.Model {
return nil, errors.Trace(errors.NotValidf("highlevel metrics label %q", k))
}
ctxM := make(map[string]string, len(v))
for k2, v2 := range v {
ctxM[k2.String()] = v2
}
m[k.String()] = ctxM
}
req.Metrics = m
return c.refresh(ctx, config.Ensure, req, headers)
}

func (c *RefreshClient) refresh(ctx context.Context, ensure func(responses []transport.RefreshResponse) error, req transport.RefreshRequest, headers Headers) ([]transport.RefreshResponse, error) {
httpHeaders := make(http.Header)
for k, values := range headers {
for _, value := range values {
Expand All @@ -120,7 +148,7 @@ func (c *RefreshClient) Refresh(ctx context.Context, config RefreshConfig) ([]tr
}

c.logger.Tracef("Refresh() unmarshalled: %s", pretty.Sprint(resp.Results))
return resp.Results, config.Ensure(resp.Results)
return resp.Results, ensure(resp.Results)
}

// refreshOne holds the config for making refresh calls to the CharmHub API.
Expand All @@ -132,6 +160,7 @@ type refreshOne struct {
// 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.
Expand Down Expand Up @@ -176,6 +205,7 @@ func (c refreshOne) Build() (transport.RefreshRequest, Headers, error) {
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.
Expand Down Expand Up @@ -234,6 +264,23 @@ func InstallOneFromRevision(name string, revision int, base RefreshBase) (Refres
}, nil
}

// 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) {
c, ok := config.(refreshOne)
if !ok {
return config, nil // error?
}
if len(metrics) < 1 {
return c, nil
}
c.metrics = make(transport.ContextMetrics, 0)
for k, v := range metrics {
c.metrics[k.String()] = v
}
return c, nil
}

// InstallOneFromChannel creates a request config using the channel and not the
// revision for requesting only one charm.
func InstallOneFromChannel(name string, channel string, base RefreshBase) (RefreshConfig, error) {
Expand Down
101 changes: 101 additions & 0 deletions charmhub/refresh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
path "github.com/juju/juju/charmhub/path"
"github.com/juju/juju/charmhub/transport"
"github.com/juju/juju/core/arch"
charmmetrics "github.com/juju/juju/core/charm/metrics"
)

type RefreshSuite struct {
Expand Down Expand Up @@ -197,6 +198,63 @@ func (s *RefreshSuite) TestRefreshMetadata(c *gc.C) {
})
}

func (s *RefreshSuite) RefreshWithRequestMetrics(c *gc.C) {
ctrl := gomock.NewController(c)
defer ctrl.Finish()

baseURL := MustParseURL(c, "http://api.foo.bar")

path := path.MakePath(baseURL)
id := "meshuggah"
body := transport.RefreshRequest{
Context: []transport.RefreshRequestContext{{
InstanceKey: "foo-bar",
ID: id,
Revision: 1,
Base: transport.Base{
Name: "ubuntu",
Channel: "20.04",
Architecture: arch.DefaultArchitecture,
},
TrackingChannel: "latest/stable",
}},
Actions: []transport.RefreshRequestAction{{
Action: "refresh",
InstanceKey: "foo-bar",
ID: &id,
}},
}

config1, err := RefreshOne("foo", 1, "latest/stable", RefreshBase{
Name: "ubuntu",
Channel: "20.04",
Architecture: "amd64",
})
c.Assert(err, jc.ErrorIsNil)
config1 = DefineInstanceKey(c, config1, "key-foo")

config2, err := RefreshOne("bar", 2, "latest/edge", RefreshBase{
Name: "ubuntu",
Channel: "14.04",
Architecture: "amd64",
})
c.Assert(err, jc.ErrorIsNil)
config2 = DefineInstanceKey(c, config2, "key-bar")

config := RefreshMany(config1, config2)

restClient := NewMockRESTClient(ctrl)
s.expectPost(c, restClient, path, id, body)

metrics := map[charmmetrics.MetricKey]map[charmmetrics.MetricKey]string{}

client := NewRefreshClient(path, restClient, &FakeLogger{})
responses, err := client.RefreshWithRequestMetrics(context.TODO(), config, metrics)
c.Assert(err, jc.ErrorIsNil)
c.Assert(len(responses), gc.Equals, 1)
c.Assert(responses[0].Name, gc.Equals, id)
}

func (s *RefreshSuite) TestRefreshFailure(c *gc.C) {
ctrl := gomock.NewController(c)
defer ctrl.Finish()
Expand Down Expand Up @@ -289,6 +347,49 @@ func (s *RefreshConfigSuite) TestRefreshOneBuild(c *gc.C) {
})
}

func (s *RefreshConfigSuite) TestRefreshOneWithMetricsBuild(c *gc.C) {
id := "foo"
config, err := RefreshOne(id, 1, "latest/stable", RefreshBase{
Name: "ubuntu",
Channel: "20.04",
Architecture: arch.DefaultArchitecture,
})
c.Assert(err, jc.ErrorIsNil)

config = DefineInstanceKey(c, config, "foo-bar")

config, err = AddConfigMetrics(config, map[charmmetrics.MetricKey]string{
charmmetrics.Provider: "openstack",
charmmetrics.NumApplications: "4",
})
c.Assert(err, jc.ErrorIsNil)

req, _, err := config.Build()
c.Assert(err, jc.ErrorIsNil)
c.Assert(req, gc.DeepEquals, transport.RefreshRequest{
Context: []transport.RefreshRequestContext{{
InstanceKey: "foo-bar",
ID: "foo",
Revision: 1,
Base: transport.Base{
Name: "ubuntu",
Channel: "20.04",
Architecture: arch.DefaultArchitecture,
},
TrackingChannel: "latest/stable",
Metrics: map[string]string{
"provider": "openstack",
"num-applications": "4",
},
}},
Actions: []transport.RefreshRequestAction{{
Action: "refresh",
InstanceKey: "foo-bar",
ID: &id,
}},
})
}

func (s *RefreshConfigSuite) TestRefreshOneEnsure(c *gc.C) {
config, err := RefreshOne("foo", 1, "latest/stable", RefreshBase{
Name: "ubuntu",
Expand Down
18 changes: 14 additions & 4 deletions charmhub/transport/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@ type RefreshRequest struct {
// be always present and hence the no omitempty.
Context []RefreshRequestContext `json:"context"`
Actions []RefreshRequestAction `json:"actions"`
Metrics RequestMetrics `json:"metrics,omitempty"`
}

// RequestMetrics are a map of key value pairs of metrics for the controller
// and the model in the request.
type RequestMetrics map[string]map[string]string

// RefreshRequestContext can request a given context for making multiple
// requests to one given entity.
type RefreshRequestContext struct {
InstanceKey string `json:"instance-key"`
ID string `json:"id"`

Revision int `json:"revision"`
Base Base `json:"base,omitempty"`
TrackingChannel string `json:"tracking-channel,omitempty"`
RefreshedDate *time.Time `json:"refresh-date,omitempty"`
Revision int `json:"revision"`
Base Base `json:"base,omitempty"`
TrackingChannel string `json:"tracking-channel,omitempty"`
RefreshedDate *time.Time `json:"refresh-date,omitempty"`
Metrics ContextMetrics `json:"metrics,omitempty"`
}

// ContextMetrics are a map of key value pairs of metrics for the specific
// charm/application in the context.
type ContextMetrics map[string]string

// RefreshRequestAction defines a action to perform against the Refresh API.
type RefreshRequestAction struct {
// Action can be install, download or refresh.
Expand Down

0 comments on commit 2f6c9f9

Please sign in to comment.