github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/resource/api/server/server.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package server
     5  
     6  import (
     7  	"io"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"gopkg.in/juju/charm.v6-unstable"
    12  	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
    13  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    14  	"gopkg.in/juju/names.v2"
    15  	"gopkg.in/macaroon.v1"
    16  
    17  	"github.com/juju/juju/apiserver/common"
    18  	"github.com/juju/juju/apiserver/params"
    19  	"github.com/juju/juju/charmstore"
    20  	"github.com/juju/juju/resource"
    21  	"github.com/juju/juju/resource/api"
    22  )
    23  
    24  var logger = loggo.GetLogger("juju.resource.api.server")
    25  
    26  const (
    27  	// Version is the version number of the current Facade.
    28  	Version = 1
    29  )
    30  
    31  // DataStore is the functionality of Juju's state needed for the resources API.
    32  type DataStore interface {
    33  	resourceInfoStore
    34  	UploadDataStore
    35  }
    36  
    37  // CharmStore exposes the functionality of the charm store as needed here.
    38  type CharmStore interface {
    39  	// ListResources composes, for each of the identified charms, the
    40  	// list of details for each of the charm's resources. Those details
    41  	// are those associated with the specific charm revision. They
    42  	// include the resource's metadata and revision.
    43  	ListResources([]charmstore.CharmID) ([][]charmresource.Resource, error)
    44  
    45  	// ResourceInfo returns the metadata for the given resource.
    46  	ResourceInfo(charmstore.ResourceRequest) (charmresource.Resource, error)
    47  }
    48  
    49  // Facade is the public API facade for resources.
    50  type Facade struct {
    51  	// store is the data source for the facade.
    52  	store resourceInfoStore
    53  
    54  	newCharmstoreClient func() (CharmStore, error)
    55  }
    56  
    57  // NewFacade returns a new resoures facade for the given Juju state.
    58  func NewFacade(store DataStore, newClient func() (CharmStore, error)) (*Facade, error) {
    59  	if store == nil {
    60  		return nil, errors.Errorf("missing data store")
    61  	}
    62  	if newClient == nil {
    63  		// Technically this only matters for one code path through
    64  		// AddPendingResources(). However, that functionality should be
    65  		// provided. So we indicate the problem here instead of later
    66  		// in the specific place where it actually matters.
    67  		return nil, errors.Errorf("missing factory for new charm store clients")
    68  	}
    69  
    70  	f := &Facade{
    71  		store:               store,
    72  		newCharmstoreClient: newClient,
    73  	}
    74  	return f, nil
    75  }
    76  
    77  // resourceInfoStore is the portion of Juju's "state" needed
    78  // for the resources facade.
    79  type resourceInfoStore interface {
    80  	// ListResources returns the resources for the given application.
    81  	ListResources(service string) (resource.ServiceResources, error)
    82  
    83  	// AddPendingResource adds the resource to the data store in a
    84  	// "pending" state. It will stay pending (and unavailable) until
    85  	// it is resolved. The returned ID is used to identify the pending
    86  	// resources when resolving it.
    87  	AddPendingResource(applicationID, userID string, chRes charmresource.Resource, r io.Reader) (string, error)
    88  }
    89  
    90  // ListResources returns the list of resources for the given application.
    91  func (f Facade) ListResources(args api.ListResourcesArgs) (api.ResourcesResults, error) {
    92  	var r api.ResourcesResults
    93  	r.Results = make([]api.ResourcesResult, len(args.Entities))
    94  
    95  	for i, e := range args.Entities {
    96  		logger.Tracef("Listing resources for %q", e.Tag)
    97  		tag, apierr := parseApplicationTag(e.Tag)
    98  		if apierr != nil {
    99  			r.Results[i] = api.ResourcesResult{
   100  				ErrorResult: params.ErrorResult{
   101  					Error: apierr,
   102  				},
   103  			}
   104  			continue
   105  		}
   106  
   107  		svcRes, err := f.store.ListResources(tag.Id())
   108  		if err != nil {
   109  			r.Results[i] = errorResult(err)
   110  			continue
   111  		}
   112  
   113  		r.Results[i] = api.ServiceResources2APIResult(svcRes)
   114  	}
   115  	return r, nil
   116  }
   117  
   118  // AddPendingResources adds the provided resources (info) to the Juju
   119  // model in a pending state, meaning they are not available until
   120  // resolved.
   121  func (f Facade) AddPendingResources(args api.AddPendingResourcesArgs) (api.AddPendingResourcesResult, error) {
   122  	var result api.AddPendingResourcesResult
   123  
   124  	tag, apiErr := parseApplicationTag(args.Tag)
   125  	if apiErr != nil {
   126  		result.Error = apiErr
   127  		return result, nil
   128  	}
   129  	applicationID := tag.Id()
   130  
   131  	channel := csparams.Channel(args.Channel)
   132  	ids, err := f.addPendingResources(applicationID, args.URL, channel, args.CharmStoreMacaroon, args.Resources)
   133  	if err != nil {
   134  		result.Error = common.ServerError(err)
   135  		return result, nil
   136  	}
   137  	result.PendingIDs = ids
   138  	return result, nil
   139  }
   140  
   141  func (f Facade) addPendingResources(applicationID, chRef string, channel csparams.Channel, csMac *macaroon.Macaroon, apiResources []api.CharmResource) ([]string, error) {
   142  	var resources []charmresource.Resource
   143  	for _, apiRes := range apiResources {
   144  		res, err := api.API2CharmResource(apiRes)
   145  		if err != nil {
   146  			return nil, errors.Annotatef(err, "bad resource info for %q", apiRes.Name)
   147  		}
   148  		resources = append(resources, res)
   149  	}
   150  
   151  	if chRef != "" {
   152  		cURL, err := charm.ParseURL(chRef)
   153  		if err != nil {
   154  			return nil, err
   155  		}
   156  
   157  		switch cURL.Schema {
   158  		case "cs":
   159  			id := charmstore.CharmID{
   160  				URL:     cURL,
   161  				Channel: channel,
   162  			}
   163  			resources, err = f.resolveCharmstoreResources(id, csMac, resources)
   164  			if err != nil {
   165  				return nil, errors.Trace(err)
   166  			}
   167  		case "local":
   168  			resources, err = f.resolveLocalResources(resources)
   169  			if err != nil {
   170  				return nil, errors.Trace(err)
   171  			}
   172  		default:
   173  			return nil, errors.Errorf("unrecognized charm schema %q", cURL.Schema)
   174  		}
   175  	}
   176  
   177  	var ids []string
   178  	for _, res := range resources {
   179  		pendingID, err := f.addPendingResource(applicationID, res)
   180  		if err != nil {
   181  			// We don't bother aggregating errors since a partial
   182  			// completion is disruptive and a retry of this endpoint
   183  			// is not expensive.
   184  			return nil, err
   185  		}
   186  		ids = append(ids, pendingID)
   187  	}
   188  	return ids, nil
   189  }
   190  
   191  func (f Facade) resolveCharmstoreResources(id charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) ([]charmresource.Resource, error) {
   192  	client, err := f.newCharmstoreClient()
   193  	if err != nil {
   194  		return nil, errors.Trace(err)
   195  	}
   196  	ids := []charmstore.CharmID{id}
   197  	storeResources, err := f.resourcesFromCharmstore(ids, client)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	resolved, err := resolveResources(resources, storeResources, id, client)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  	// TODO(ericsnow) Ensure that the non-upload resource revisions
   206  	// match a previously published revision set?
   207  	return resolved, nil
   208  }
   209  
   210  func (f Facade) resolveLocalResources(resources []charmresource.Resource) ([]charmresource.Resource, error) {
   211  	var resolved []charmresource.Resource
   212  	for _, res := range resources {
   213  		resolved = append(resolved, charmresource.Resource{
   214  			Meta:   res.Meta,
   215  			Origin: charmresource.OriginUpload,
   216  		})
   217  	}
   218  	return resolved, nil
   219  }
   220  
   221  // resourcesFromCharmstore gets the info for the charm's resources in
   222  // the charm store. If the charm URL has a revision then that revision's
   223  // resources are returned. Otherwise the latest info for each of the
   224  // resources is returned.
   225  func (f Facade) resourcesFromCharmstore(charms []charmstore.CharmID, client CharmStore) (map[string]charmresource.Resource, error) {
   226  	results, err := client.ListResources(charms)
   227  	if err != nil {
   228  		return nil, errors.Trace(err)
   229  	}
   230  	storeResources := make(map[string]charmresource.Resource)
   231  	if len(results) != 0 {
   232  		for _, res := range results[0] {
   233  			storeResources[res.Name] = res
   234  		}
   235  	}
   236  	return storeResources, nil
   237  }
   238  
   239  // resolveResources determines the resource info that should actually
   240  // be stored on the controller. That decision is based on the provided
   241  // resources along with those in the charm store (if any).
   242  func resolveResources(resources []charmresource.Resource, storeResources map[string]charmresource.Resource, id charmstore.CharmID, client CharmStore) ([]charmresource.Resource, error) {
   243  	allResolved := make([]charmresource.Resource, len(resources))
   244  	copy(allResolved, resources)
   245  	for i, res := range resources {
   246  		// Note that incoming "upload" resources take precedence over
   247  		// ones already known to the controller, regardless of their
   248  		// origin.
   249  		if res.Origin != charmresource.OriginStore {
   250  			continue
   251  		}
   252  
   253  		resolved, err := resolveStoreResource(res, storeResources, id, client)
   254  		if err != nil {
   255  			return nil, errors.Trace(err)
   256  		}
   257  		allResolved[i] = resolved
   258  	}
   259  	return allResolved, nil
   260  }
   261  
   262  // resolveStoreResource selects the resource info to use. It decides
   263  // between the provided and latest info based on the revision.
   264  func resolveStoreResource(res charmresource.Resource, storeResources map[string]charmresource.Resource, id charmstore.CharmID, client CharmStore) (charmresource.Resource, error) {
   265  	storeRes, ok := storeResources[res.Name]
   266  	if !ok {
   267  		// This indicates that AddPendingResources() was called for
   268  		// a resource the charm store doesn't know about (for the
   269  		// relevant charm revision).
   270  		// TODO(ericsnow) Do the following once the charm store supports
   271  		// the necessary endpoints:
   272  		// return res, errors.NotFoundf("charm store resource %q", res.Name)
   273  		return res, nil
   274  	}
   275  
   276  	if res.Revision < 0 {
   277  		// The caller wants to use the charm store info.
   278  		return storeRes, nil
   279  	}
   280  	if res.Revision == storeRes.Revision {
   281  		// We don't worry about if they otherwise match. Only the
   282  		// revision is significant here. So we use the info from the
   283  		// charm store since it is authoritative.
   284  		return storeRes, nil
   285  	}
   286  	if res.Fingerprint.IsZero() {
   287  		// The caller wants resource info from the charm store, but with
   288  		// a different resource revision than the one associated with
   289  		// the charm in the store.
   290  		req := charmstore.ResourceRequest{
   291  			Charm:    id.URL,
   292  			Channel:  id.Channel,
   293  			Name:     res.Name,
   294  			Revision: res.Revision,
   295  		}
   296  		storeRes, err := client.ResourceInfo(req)
   297  		if err != nil {
   298  			return storeRes, errors.Trace(err)
   299  		}
   300  		return storeRes, nil
   301  	}
   302  	// The caller fully-specified a resource with a different resource
   303  	// revision than the one associated with the charm in the store. So
   304  	// we use the provided info as-is.
   305  	return res, nil
   306  }
   307  
   308  func (f Facade) addPendingResource(applicationID string, chRes charmresource.Resource) (pendingID string, err error) {
   309  	userID := ""
   310  	var reader io.Reader
   311  	pendingID, err = f.store.AddPendingResource(applicationID, userID, chRes, reader)
   312  	if err != nil {
   313  		return "", errors.Annotatef(err, "while adding pending resource info for %q", chRes.Name)
   314  	}
   315  	return pendingID, nil
   316  }
   317  
   318  func parseApplicationTag(tagStr string) (names.ApplicationTag, *params.Error) { // note the concrete error type
   319  	ApplicationTag, err := names.ParseApplicationTag(tagStr)
   320  	if err != nil {
   321  		return ApplicationTag, &params.Error{
   322  			Message: err.Error(),
   323  			Code:    params.CodeBadRequest,
   324  		}
   325  	}
   326  	return ApplicationTag, nil
   327  }
   328  
   329  func errorResult(err error) api.ResourcesResult {
   330  	return api.ResourcesResult{
   331  		ErrorResult: params.ErrorResult{
   332  			Error: common.ServerError(err),
   333  		},
   334  	}
   335  }