github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 }