github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/application/store.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // TODO(natefinch): change the code in this file to use the
     5  // github.com/juju/juju/charmstore package to interact with the charmstore.
     6  
     7  package application
     8  
     9  import (
    10  	"fmt"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  	"gopkg.in/juju/charm.v6-unstable"
    16  	"gopkg.in/juju/charmrepo.v2-unstable/csclient"
    17  	csparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    18  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    19  	"gopkg.in/macaroon.v1"
    20  
    21  	"github.com/juju/juju/apiserver/params"
    22  	"github.com/juju/juju/environs/config"
    23  )
    24  
    25  // maybeTermsAgreementError returns err as a *termsAgreementError
    26  // if it has a "terms agreement required" error code, otherwise
    27  // it returns err unchanged.
    28  func maybeTermsAgreementError(err error) error {
    29  	const code = "term agreement required"
    30  	e, ok := errors.Cause(err).(*httpbakery.DischargeError)
    31  	if !ok || e.Reason == nil || e.Reason.Code != code {
    32  		return err
    33  	}
    34  	magicMarker := code + ":"
    35  	index := strings.LastIndex(e.Reason.Message, magicMarker)
    36  	if index == -1 {
    37  		return err
    38  	}
    39  	return &termsRequiredError{strings.Fields(e.Reason.Message[index+len(magicMarker):])}
    40  }
    41  
    42  type termsRequiredError struct {
    43  	Terms []string
    44  }
    45  
    46  func (e *termsRequiredError) Error() string {
    47  	return fmt.Sprintf("please agree to terms %q", strings.Join(e.Terms, " "))
    48  }
    49  
    50  func isSeriesSupported(requestedSeries string, supportedSeries []string) bool {
    51  	for _, series := range supportedSeries {
    52  		if series == requestedSeries {
    53  			return true
    54  		}
    55  	}
    56  	return false
    57  }
    58  
    59  // TODO(ericsnow) Return charmstore.CharmID from resolve()?
    60  
    61  // ResolveCharmFunc is the type of a function that resolves a charm URL.
    62  type ResolveCharmFunc func(
    63  	resolveWithChannel func(*charm.URL) (*charm.URL, csparams.Channel, []string, error),
    64  	conf *config.Config,
    65  	url *charm.URL,
    66  ) (*charm.URL, csparams.Channel, []string, error)
    67  
    68  func resolveCharm(
    69  	resolveWithChannel func(*charm.URL) (*charm.URL, csparams.Channel, []string, error),
    70  	conf *config.Config,
    71  	url *charm.URL,
    72  ) (*charm.URL, csparams.Channel, []string, error) {
    73  	if url.Schema != "cs" {
    74  		return nil, csparams.NoChannel, nil, errors.Errorf("unknown schema for charm URL %q", url)
    75  	}
    76  	// If the user hasn't explicitly asked for a particular series,
    77  	// query for the charm that matches the model's default series.
    78  	// If this fails, we'll fall back to asking for whatever charm is available.
    79  	defaultedSeries := false
    80  	if url.Series == "" {
    81  		if s, ok := conf.DefaultSeries(); ok {
    82  			defaultedSeries = true
    83  			// TODO(katco): Don't update the value passed in. Not only
    84  			// is there no indication that this method will do so, we
    85  			// return a charm.URL which signals to the developer that
    86  			// we don't modify the original.
    87  			url.Series = s
    88  		}
    89  	}
    90  
    91  	resultURL, channel, supportedSeries, err := resolveWithChannel(url)
    92  	if defaultedSeries && errors.Cause(err) == csparams.ErrNotFound {
    93  		// we tried to use the model's default the series, but the store said it doesn't exist.
    94  		// retry without the defaulted series, to take what we can get.
    95  		url.Series = ""
    96  		resultURL, channel, supportedSeries, err = resolveWithChannel(url)
    97  	}
    98  	if err != nil {
    99  		return nil, csparams.NoChannel, nil, errors.Trace(err)
   100  	}
   101  	if resultURL.Series != "" && len(supportedSeries) == 0 {
   102  		supportedSeries = []string{resultURL.Series}
   103  	}
   104  	return resultURL, channel, supportedSeries, nil
   105  }
   106  
   107  // TODO(ericsnow) Return charmstore.CharmID from addCharmFromURL()?
   108  
   109  // addCharmFromURL calls the appropriate client API calls to add the
   110  // given charm URL to state. For non-public charm URLs, this function also
   111  // handles the macaroon authorization process using the given csClient.
   112  // The resulting charm URL of the added charm is displayed on stdout.
   113  func addCharmFromURL(client CharmAdder, curl *charm.URL, channel csparams.Channel) (*charm.URL, *macaroon.Macaroon, error) {
   114  	var csMac *macaroon.Macaroon
   115  	if err := client.AddCharm(curl, channel); err != nil {
   116  		if !params.IsCodeUnauthorized(err) {
   117  			return nil, nil, errors.Trace(err)
   118  		}
   119  		m, err := client.AuthorizeCharmstoreEntity(curl)
   120  		if err != nil {
   121  			return nil, nil, maybeTermsAgreementError(err)
   122  		}
   123  		if err := client.AddCharmWithAuthorization(curl, channel, m); err != nil {
   124  			return nil, nil, errors.Trace(err)
   125  		}
   126  		csMac = m
   127  	}
   128  	return curl, csMac, nil
   129  }
   130  
   131  // newCharmStoreClient is called to obtain a charm store client.
   132  // It is defined as a variable so it can be changed for testing purposes.
   133  var newCharmStoreClient = func(client *httpbakery.Client) *csclient.Client {
   134  	return csclient.New(csclient.Params{
   135  		BakeryClient: client,
   136  	})
   137  }
   138  
   139  // authorizeCharmStoreEntity acquires and return the charm store delegatable macaroon to be
   140  // used to add the charm corresponding to the given URL.
   141  // The macaroon is properly attenuated so that it can only be used to deploy
   142  // the given charm URL.
   143  func authorizeCharmStoreEntity(csClient *csclient.Client, curl *charm.URL) (*macaroon.Macaroon, error) {
   144  	endpoint := "/delegatable-macaroon?id=" + url.QueryEscape(curl.String())
   145  	var m *macaroon.Macaroon
   146  	if err := csClient.Get(endpoint, &m); err != nil {
   147  		return nil, errors.Trace(err)
   148  	}
   149  	return m, nil
   150  }