Skip to content

Commit

Permalink
Implement fake charm store client that avoids HTTP
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim McNamara committed Apr 2, 2019
1 parent b34b230 commit 358f90d
Show file tree
Hide file tree
Showing 13 changed files with 491 additions and 121 deletions.
24 changes: 12 additions & 12 deletions charmstore/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/juju/errors"
"github.com/juju/loggo"
"gopkg.in/juju/charm.v6"
charmresource "gopkg.in/juju/charm.v6/resource"
"gopkg.in/juju/charm.v6/resource"
"gopkg.in/juju/charmrepo.v3/csclient"
csparams "gopkg.in/juju/charmrepo.v3/csclient/params"
"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
Expand Down Expand Up @@ -189,7 +189,7 @@ type ResourceData struct {
io.ReadCloser

// Resource holds the metadata for the resource.
Resource charmresource.Resource
Resource resource.Resource
}

// GetResource returns the data (bytes) and metadata for a resource from the charmstore.
Expand Down Expand Up @@ -217,7 +217,7 @@ func (c Client) GetResource(req ResourceRequest) (data ResourceData, err error)
}
}()
data.ReadCloser = resData.ReadCloser
if data.Resource.Type == charmresource.TypeFile {
if data.Resource.Type == resource.TypeFile {
fpHash := data.Resource.Fingerprint.String()
if resData.Hash != fpHash {
return ResourceData{},
Expand All @@ -228,25 +228,25 @@ func (c Client) GetResource(req ResourceRequest) (data ResourceData, err error)
}

// ResourceInfo returns the metadata for the given resource from the charmstore.
func (c Client) ResourceInfo(req ResourceRequest) (charmresource.Resource, error) {
func (c Client) ResourceInfo(req ResourceRequest) (resource.Resource, error) {
if err := c.jar.Activate(req.Charm); err != nil {
return charmresource.Resource{}, errors.Trace(err)
return resource.Resource{}, errors.Trace(err)
}
defer c.jar.Deactivate()
meta, err := c.csWrapper.ResourceMeta(req.Channel, req.Charm, req.Name, req.Revision)
if err != nil {
return charmresource.Resource{}, errors.Trace(err)
return resource.Resource{}, errors.Trace(err)
}
res, err := csparams.API2Resource(meta)
if err != nil {
return charmresource.Resource{}, errors.Trace(err)
return resource.Resource{}, errors.Trace(err)
}
return res, nil
}

// ListResources returns a list of resources for each of the given charms.
func (c Client) ListResources(charms []CharmID) ([][]charmresource.Resource, error) {
results := make([][]charmresource.Resource, len(charms))
func (c Client) ListResources(charms []CharmID) ([][]resource.Resource, error) {
results := make([][]resource.Resource, len(charms))
for i, ch := range charms {
res, err := c.listResources(ch)
if err != nil {
Expand All @@ -262,7 +262,7 @@ func (c Client) ListResources(charms []CharmID) ([][]charmresource.Resource, err
return results, nil
}

func (c Client) listResources(ch CharmID) ([]charmresource.Resource, error) {
func (c Client) listResources(ch CharmID) ([]resource.Resource, error) {
if err := c.jar.Activate(ch.URL); err != nil {
return nil, errors.Trace(err)
}
Expand Down Expand Up @@ -316,8 +316,8 @@ func (c csclientImpl) ResourceMeta(channel csparams.Channel, id *charm.URL, name
return client.ResourceMeta(id, name, revision)
}

func api2resources(res []csparams.Resource) ([]charmresource.Resource, error) {
result := make([]charmresource.Resource, len(res))
func api2resources(res []csparams.Resource) ([]resource.Resource, error) {
result := make([]resource.Resource, len(res))
for i, r := range res {
var err error
result[i], err = csparams.API2Resource(r)
Expand Down
225 changes: 225 additions & 0 deletions charmstore/fakeclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Copyright 2019 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

// fakeclient provides two charmstore client sthat do not use any HTTP connections
// or authentication mechanisms. Why two? Each reflects one of two informally-defined
// styles for interacting with the charmstore.
//
// - FakeClient has methods that include a channel parameter
// - ChannelAwareFakeClient maintains a record of the channel that it is currently talking about
//
// fakeclient also includes a Repository type. The Repository preforms the role of a substitute charmrepo.
// More technically, it implements the gopkg.in/juju/charmrepo Interface.
//
// Each of these three types are interrelated:
//
// Repository --> FakeClient --> ChannelAwareFakeClient
// | [extended by] | [extended by] |
// \ \ \
// provides storage provides front-end provides alternative
// for charms, front end
// bundles,
// resources

package charmstore

import (
"io"

"github.com/juju/errors"
"github.com/juju/juju/core/model"
"gopkg.in/juju/charm.v6"
"gopkg.in/juju/charmrepo.v3/csclient/params"
)

// datastore is a small, in-memory key/value store. Its primary use case is to
// fake HTTP calls.
//
// Note
//
// datastore's methods support a path argument that represents a URL path. However,
// no normalisation is performed on this parameter. Therefore,
// two strings that refer to the same canonical URL will not match within datastore.
type datastore map[string]interface{}

// Get retrieves contents stored at path and saves it to data. If nothing exists at path,
// an error satisfying errors.IsNotFound is returned.
func (d datastore) Get(path string, data interface{}) error {
current := d[path]
if current == nil {
return errors.NotFoundf(path)
}
data = current
return nil
}

// Put stores data at path. It will be accessible later via Get.
// Data already at path will is overwritten and no
// revision history is saved.
func (d datastore) Put(path string, data interface{}) error {
d[path] = data
return nil
}

// PutReader data from a an io.Reader at path. It will be accessible later via Get.
// Data already at path will is overwritten and no
// revision history is saved.
func (d *datastore) PutReader(path string, data io.Reader) error {
buffer := []byte{}
_, err := data.Read(buffer)
if err != nil {
return errors.Trace(err)
}
return d.Put(path, buffer)
}

// FakeClient is a stand-in for the gopkg.in/juju/charmrepo.v3/csclient Client type.
// Its "stores" data within several an in-memory map for each object that charmstores know about, primarily charms, bundles and resources.
//
// An abridged session would look something like this, where charmId is a *charm.URL:
// // initialise a new charmstore with an empty repository
// repo := NewRepository()
// client := NewFakeClient(repo)
// client.UploadCharm(charmId)
// // later on
// charm := client.Get(charmId)
type FakeClient struct {
repo *Repository
}

//var _ csWrapper = (*FakeClient)(nil)

// NewFakeClient returns a FakeClient that is initialised
// with repo.
func NewFakeClient(repo *Repository) *FakeClient {
if repo == nil {
repo = NewRepository()
}
return &FakeClient{repo}
}

// Get retrieves data from path
func (c FakeClient) Get(path string, value interface{}) error {
return c.repo.resourcesData.Get(path, value)
}

func (c FakeClient) WithChannel(channel params.Channel) *ChannelAwareFakeClient {
return &ChannelAwareFakeClient{channel, c}
}

// ChannelAwareFakeClient is a charmstore client that stores the channel that its methods
// refer to across calls. That is, it is stateful. It is modelled on the Client type defined in
// gopkg.in/juju/charmrepo.v3/csclient.
//
// Constructing ChannelAwareFakeClient
//
// ChannelAwareFakeClient does not have a NewChannelAwareFakeClient method. To construct an
// instance, use the following pattern:
// NewFakeClient(nil).WithChannel(channel)
//
// Setting the channel
//
// To set ChannelAwareFakeClient's channel, its the WithChannel method.
type ChannelAwareFakeClient struct {
channel params.Channel
charmstore FakeClient
}

func (c ChannelAwareFakeClient) Get(path string, value interface{}) error {
return c.charmstore.Get(path, value)
}

func (c ChannelAwareFakeClient) WithChannel(channel params.Channel) *ChannelAwareFakeClient {
c.channel = channel
return &c
}

// Repository provides in-memory access to charms and other objects
// held in a charmstore (or locally), such as bundles and resources.
// Its intended use case is to act as a fake charmrepo for testing purposes.
//
// Warnings
//
// No guarantees are made that Repository maintains its invariants or that the behaviour
// matches the behaviour of the actual charm store. For example, Repository's information
// about which charm revisions it knows about is decoupled from the charm data that it currently
// stores.
//
// Related Interfaces
//
// Repository implements gopkg.in/juju/charmrepo Interface and derivative
// interfaces, such as github.com/juju/juju/cmd/juju/application DeployAPI.
type Repository struct {
channel params.Channel
charms map[params.Channel]map[charm.URL]charm.Charm
bundles map[params.Channel]map[charm.URL]charm.Bundle
resources map[params.Channel]map[charm.URL][]params.Resource
revisions map[params.Channel]map[charm.URL]int
added map[string][]charm.URL
resourcesData datastore
generations map[model.GenerationVersion]string
}

// NewRepository returns an empty Repository. To populate it with charms, bundles and resources
// use UploadCharm, UploadBundle and/or UploadResource.
func NewRepository() *Repository {
repo := Repository{
channel: params.StableChannel,
charms: make(map[params.Channel]map[charm.URL]charm.Charm),
bundles: make(map[params.Channel]map[charm.URL]charm.Bundle),
resources: make(map[params.Channel]map[charm.URL][]params.Resource),
revisions: make(map[params.Channel]map[charm.URL]int),
added: make(map[string][]charm.URL),
resourcesData: make(datastore),
}
for _, channel := range params.OrderedChannels {
repo.charms[channel] = make(map[charm.URL]charm.Charm)
repo.bundles[channel] = make(map[charm.URL]charm.Bundle)
repo.resources[channel] = make(map[charm.URL][]params.Resource)
repo.revisions[channel] = make(map[charm.URL]int)
}
return &repo
}

func (r *Repository) addRevision(ref *charm.URL) *charm.URL {
revision := r.revisions[r.channel][*ref]
return ref.WithRevision(revision)
}

// Resolve disambiguates a charm to a specific revision.
//
// Part of the charmrepo.Interface
func (r Repository) Resolve(ref *charm.URL) (canonRef *charm.URL, supportedSeries []string, err error) {
return r.addRevision(ref), []string{"trusty", "wily", "quantal"}, nil
}

// ResolveWithChannel disambiguates a charm to a specific revision.
//
// Part of the cmd/juju/application.DeployAPI interface
func (r Repository) ResolveWithChannel(ref *charm.URL) (*charm.URL, params.Channel, []string, error) {
canonRef, supportedSeries, err := r.Resolve(ref)
return canonRef, r.channel, supportedSeries, err
}

// Get retrieves a charm from the repository.
//
// Part of the charmrepo.Interface
func (r Repository) Get(id *charm.URL) (charm.Charm, error) {
withRevision := r.addRevision(id)
charmData := r.charms[r.channel][*withRevision]
if charmData == nil {
return charmData, errors.NotFoundf("cannot retrieve \"%v\": charm", id.String())
}
return charmData, nil
}

// GetBundle retrieves a bundle from the repository.
//
// Part of the charmrepo.Interface
func (r Repository) GetBundle(id *charm.URL) (charm.Bundle, error) {
bundleData := r.bundles[r.channel][*id]
if bundleData == nil {
return bundleData, errors.NotFoundf(id.String())
}
return bundleData, nil
}
4 changes: 2 additions & 2 deletions charmstore/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"time"

"gopkg.in/juju/charm.v6"
charmresource "gopkg.in/juju/charm.v6/resource"
"gopkg.in/juju/charm.v6/resource"
)

// CharmInfo holds the information about a charm from the charm store.
Expand All @@ -31,7 +31,7 @@ type CharmInfo struct {
// LatestResources is the list of resource info for each of the
// charm's resources. This list is accurate as of the time that the
// charm store handled the request for the charm info.
LatestResources []charmresource.Resource
LatestResources []resource.Resource
}

// LatestURL returns the charm URL for the latest revision of the charm.
Expand Down
4 changes: 2 additions & 2 deletions charmstore/latest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"
"gopkg.in/juju/charm.v6"
charmresource "gopkg.in/juju/charm.v6/resource"
"gopkg.in/juju/charm.v6/resource"
"gopkg.in/juju/charmrepo.v3/csclient/params"

"github.com/juju/juju/version"
Expand Down Expand Up @@ -112,7 +112,7 @@ func (s *LatestCharmInfoSuite) TestSuccess(c *gc.C) {
OriginalURL: charm.MustParseURL("cs:quantal/spam-17"),
Timestamp: timestamp,
LatestRevision: 17,
LatestResources: []charmresource.Resource{
LatestResources: []resource.Resource{
expectedRes,
},
},
Expand Down
Loading

0 comments on commit 358f90d

Please sign in to comment.