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 }