github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/resources/facade.go (about)

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