github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cmd/juju/service/store.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package service
     5  
     6  import (
     7  	"fmt"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"gopkg.in/juju/charm.v6-unstable"
    13  	"gopkg.in/juju/charmrepo.v2-unstable"
    14  	"gopkg.in/juju/charmrepo.v2-unstable/csclient"
    15  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    16  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    17  	"gopkg.in/macaroon.v1"
    18  
    19  	"github.com/juju/juju/api"
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/environs/config"
    22  )
    23  
    24  // maybeTermsAgreementError returns err as a *termsAgreementError
    25  // if it has a "terms agreement required" error code, otherwise
    26  // it returns err unchanged.
    27  func maybeTermsAgreementError(err error) error {
    28  	const code = "term agreement required"
    29  	e, ok := errors.Cause(err).(*httpbakery.DischargeError)
    30  	if !ok || e.Reason == nil || e.Reason.Code != code {
    31  		return err
    32  	}
    33  	magicMarker := code + ":"
    34  	index := strings.LastIndex(e.Reason.Message, magicMarker)
    35  	if index == -1 {
    36  		return err
    37  	}
    38  	return &termsRequiredError{strings.Fields(e.Reason.Message[index+len(magicMarker):])}
    39  }
    40  
    41  type termsRequiredError struct {
    42  	Terms []string
    43  }
    44  
    45  func (e *termsRequiredError) Error() string {
    46  	return fmt.Sprintf("please agree to terms %q", strings.Join(e.Terms, " "))
    47  }
    48  
    49  func isSeriesSupported(requestedSeries string, supportedSeries []string) bool {
    50  	for _, series := range supportedSeries {
    51  		if series == requestedSeries {
    52  			return true
    53  		}
    54  	}
    55  	return false
    56  }
    57  
    58  // charmURLResolver holds the information necessary to
    59  // resolve charm and bundle URLs.
    60  type charmURLResolver struct {
    61  	// store holds the repository to use for charmstore charms.
    62  	store *charmrepo.CharmStore
    63  
    64  	// conf holds the current model configuration.
    65  	conf *config.Config
    66  }
    67  
    68  func newCharmURLResolver(conf *config.Config, csClient *csclient.Client) *charmURLResolver {
    69  	r := &charmURLResolver{
    70  		store: charmrepo.NewCharmStoreFromClient(csClient),
    71  		conf:  conf,
    72  	}
    73  	return r
    74  }
    75  
    76  // TODO(ericsnow) Return charmstore.CharmID from resolve()?
    77  
    78  // resolve resolves the given given charm or bundle URL
    79  // string by looking it up in the charm store.
    80  // The given csParams will be used to access the charm store.
    81  //
    82  // It returns the fully resolved URL, any series supported by the entity,
    83  // and the store that holds it.
    84  func (r *charmURLResolver) resolve(urlStr string) (*charm.URL, csparams.Channel, []string, *charmrepo.CharmStore, error) {
    85  	var noChannel csparams.Channel
    86  	url, err := charm.ParseURL(urlStr)
    87  	if err != nil {
    88  		return nil, noChannel, nil, nil, errors.Trace(err)
    89  	}
    90  
    91  	if url.Schema != "cs" {
    92  		return nil, noChannel, nil, nil, errors.Errorf("unknown schema for charm URL %q", url)
    93  	}
    94  	charmStore := config.SpecializeCharmRepo(r.store, r.conf).(*charmrepo.CharmStore)
    95  
    96  	resultUrl, channel, supportedSeries, err := charmStore.ResolveWithChannel(url)
    97  	if err != nil {
    98  		return nil, noChannel, nil, nil, errors.Trace(err)
    99  	}
   100  	return resultUrl, channel, supportedSeries, charmStore, nil
   101  }
   102  
   103  // TODO(ericsnow) Return charmstore.CharmID from addCharmFromURL()?
   104  
   105  // addCharmFromURL calls the appropriate client API calls to add the
   106  // given charm URL to state. For non-public charm URLs, this function also
   107  // handles the macaroon authorization process using the given csClient.
   108  // The resulting charm URL of the added charm is displayed on stdout.
   109  func addCharmFromURL(client *api.Client, curl *charm.URL, channel csparams.Channel, csClient *csclient.Client) (*charm.URL, *macaroon.Macaroon, error) {
   110  	var csMac *macaroon.Macaroon
   111  	if err := client.AddCharm(curl, channel); err != nil {
   112  		if !params.IsCodeUnauthorized(err) {
   113  			return nil, nil, errors.Trace(err)
   114  		}
   115  		m, err := authorizeCharmStoreEntity(csClient, curl)
   116  		if err != nil {
   117  			return nil, nil, maybeTermsAgreementError(err)
   118  		}
   119  		if err := client.AddCharmWithAuthorization(curl, channel, m); err != nil {
   120  			return nil, nil, errors.Trace(err)
   121  		}
   122  		csMac = m
   123  	}
   124  	return curl, csMac, nil
   125  }
   126  
   127  // newCharmStoreClient is called to obtain a charm store client.
   128  // It is defined as a variable so it can be changed for testing purposes.
   129  var newCharmStoreClient = func(client *httpbakery.Client) *csclient.Client {
   130  	return csclient.New(csclient.Params{
   131  		BakeryClient: client,
   132  	})
   133  }
   134  
   135  // TODO(natefinch): change the code in this file to use the
   136  // github.com/juju/juju/charmstore package to interact with the charmstore.
   137  
   138  // authorizeCharmStoreEntity acquires and return the charm store delegatable macaroon to be
   139  // used to add the charm corresponding to the given URL.
   140  // The macaroon is properly attenuated so that it can only be used to deploy
   141  // the given charm URL.
   142  func authorizeCharmStoreEntity(csClient *csclient.Client, curl *charm.URL) (*macaroon.Macaroon, error) {
   143  	endpoint := "/delegatable-macaroon?id=" + url.QueryEscape(curl.String())
   144  	var m *macaroon.Macaroon
   145  	if err := csClient.Get(endpoint, &m); err != nil {
   146  		return nil, errors.Trace(err)
   147  	}
   148  	return m, nil
   149  }