@@ -4,11 +4,23 @@
package main
import (
+ " fmt"
+ " net/http"
+ " path"
+
" github.com/juju/cmd"
" github.com/juju/errors"
+ " github.com/juju/persistent-cookiejar"
+ " github.com/juju/utils"
+ " golang.org/x/net/publicsuffix"
" gopkg.in/juju/charm.v5-unstable"
" gopkg.in/juju/charm.v5-unstable/charmrepo"
+ " gopkg.in/juju/charmstore.v4/csclient"
+ " gopkg.in/macaroon-bakery.v0/httpbakery"
+ " gopkg.in/macaroon.v1"
+ " github.com/juju/juju/api"
+ " github.com/juju/juju/apiserver/params"
" github.com/juju/juju/cmd/envcmd"
" github.com/juju/juju/environs"
" github.com/juju/juju/environs/config"
@@ -129,16 +141,117 @@ func resolveCharmURL(curlStr string, csParams charmrepo.NewCharmStoreParams, rep
logger.Errorf (" The series is not specified in the environment (default-series) or with the charm. Did you mean:\n\t %s " , &possibleURL)
return nil , nil , errors.Errorf (" cannot resolve series for charm: %q " , ref)
}
+ if ref.Series != " " && ref.Revision != -1 {
+ // The URL is already fully resolved; do not
+ // bother with an unnecessary round-trip to the
+ // charm store.
+ curl , err := ref.URL (" " )
+ if err != nil {
+ panic (err)
+ }
+ return curl, repo, nil
+ }
curl , err := repo.Resolve (ref)
if err != nil {
return nil , nil , errors.Trace (err)
}
return curl, repo, nil
}
-// charmStoreParams is called to obtain the parameters for connecting
-// to the charm store. It is defined as a variable so it can be changed
-// for testing purposes.
-var charmStoreParams = func () (charmrepo.NewCharmStoreParams , error ) {
- return charmrepo.NewCharmStoreParams {}, nil
+// addCharmViaAPI calls the appropriate client API calls to add the
+// given charm URL to state. For non-public charm URLs, this function also
+// handles the macaroon authorization process using the given csClient.
+// The resulting charm URL of the added charm is displayed on stdout.
+func addCharmViaAPI (client *api .Client , ctx *cmd .Context , curl *charm .URL , repo charmrepo .Interface , csclient *csClient ) (*charm .URL , error ) {
+ switch curl.Schema {
+ case " local" :
+ ch , err := repo.Get (curl)
+ if err != nil {
+ return nil , err
+ }
+ stateCurl , err := client.AddLocalCharm (curl, ch)
+ if err != nil {
+ return nil , err
+ }
+ curl = stateCurl
+ case " cs" :
+ if err := client.AddCharm (curl); err != nil {
+ if !params.IsCodeUnauthorized (err) {
+ return nil , errors.Mask (err)
+ }
+ m , err := csclient.authorize (curl)
+ if err != nil {
+ return nil , errors.Mask (err)
+ }
+ if err := client.AddCharmWithAuthorization (curl, m); err != nil {
+ return nil , errors.Mask (err)
+ }
+ }
+ default :
+ return nil , fmt.Errorf (" unsupported charm URL schema: %q " , curl.Schema )
+ }
+ ctx.Infof (" Added charm %q to the environment." , curl)
+ return curl, nil
+}
+
+// csClient gives access to the charm store server and provides parameters
+// for connecting to the charm store.
+type csClient struct {
+ jar *cookiejar.Jar
+ params charmrepo.NewCharmStoreParams
+}
+
+// newCharmStoreClient is called to obtain a charm store client
+// including the parameters for connecting to the charm store, and
+// helpers to save the local authorization cookies and to authorize
+// non-public charm deployments. It is defined as a variable so it can
+// be changed for testing purposes.
+var newCharmStoreClient = func () (*csClient, error ) {
+ jar , client , err := newHTTPClient ()
+ if err != nil {
+ return nil , errors.Mask (err)
+ }
+ return &csClient{
+ jar: jar,
+ params: charmrepo.NewCharmStoreParams {
+ HTTPClient: client,
+ VisitWebPage: httpbakery.OpenWebBrowser ,
+ },
+ }, nil
+}
+
+func newHTTPClient () (*cookiejar .Jar , *http .Client , error ) {
+ cookieFile := path.Join (utils.Home (), " .go-cookies" )
+ jar , err := cookiejar.New (&cookiejar.Options {
+ PublicSuffixList: publicsuffix.List ,
+ })
+ if err != nil {
+ panic (err)
+ }
+ if err := jar.Load (cookieFile); err != nil {
+ return nil , nil , err
+ }
+ client := httpbakery.NewHTTPClient ()
+ client.Jar = jar
+ return jar, client, nil
+}
+
+// authorize acquires and return the charm store delegatable macaroon to be
+// used to add the charm corresponding to the given URL.
+// The macaroon is properly attenuated so that it can only be used to deploy
+// the given charm URL.
+func (c *csClient ) authorize (curl *charm .URL ) (*macaroon .Macaroon , error ) {
+ client := csclient.New (csclient.Params {
+ URL: c.params .URL ,
+ HTTPClient: c.params .HTTPClient ,
+ VisitWebPage: c.params .VisitWebPage ,
+ })
+ var m *macaroon.Macaroon
+ if err := client.Get (" /delegatable-macaroon" , &m); err != nil {
+ return nil , errors.Trace (err)
+ }
+ if err := m.AddFirstPartyCaveat (" is-entity " + curl.String ()); err != nil {
+ return nil , errors.Trace (err)
+ }
+ return m, nil
}