cmd/juju: add support for deploying non-public charms. #2048

Merged
merged 1 commit into from Apr 9, 2015
Jump to file or symbol
Failed to load files and symbols.
+403 −87
Split
View
@@ -821,18 +821,26 @@ func (c *Client) localCharmUploadEndpoint(series string) (string, error) {
// the environment, if it does not exist yet. Local charms are not
// supported, only charm store URLs. See also AddLocalCharm() in the
// client-side API.
+//
+// If the AddCharm API call fails because of an authorization error
+// when retrieving the charm from the charm store, an error
+// satisfying params.IsCodeUnauthorized will be returned.
func (c *Client) AddCharm(curl *charm.URL) error {
args := params.CharmURL{
URL: curl.String(),
}
return c.facade.FacadeCall("AddCharm", args, nil)
}
-// AddCharmWithAuthorization is like AddCharm except it also
-// provides the given charmstore macaroon for the juju
-// server to use when obtaining the charm from the charm store.
-// The macaroon is conventionally obtained from the /delegatable-macaroon
-// endpoint in the charm store.
+// AddCharmWithAuthorization is like AddCharm except it also provides
+// the given charmstore macaroon for the juju server to use when
+// obtaining the charm from the charm store. The macaroon is
+// conventionally obtained from the /delegatable-macaroon endpoint in
+// the charm store.
+//
+// If the AddCharmWithAuthorization API call fails because of an
+// authorization error when retrieving the charm from the charm store,
+// an error satisfying params.IsCodeUnauthorized will be returned.
func (c *Client) AddCharmWithAuthorization(curl *charm.URL, csMac *macaroon.Macaroon) error {
args := params.AddCharmWithAuthorization{
URL: curl.String(),
@@ -11,7 +11,7 @@ import (
"gopkg.in/juju/charm.v5-unstable"
"gopkg.in/juju/charm.v5-unstable/charmrepo"
"gopkg.in/juju/charmstore.v4"
- charmstoretesting "gopkg.in/juju/charmstore.v4/testing"
+ "gopkg.in/juju/charmstore.v4/charmstoretesting"
"github.com/juju/juju/apiserver/charmrevisionupdater"
jujutesting "github.com/juju/juju/juju/testing"
@@ -1300,7 +1300,11 @@ func (c *Client) AddCharmWithAuthorization(args params.AddCharmWithAuthorization
)
downloadedCharm, err := repo.Get(charmURL)
if err != nil {
- return errors.Mask(err)
+ cause := errors.Cause(err)
+ if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) {
+ return errors.NewUnauthorized(err, "")
+ }
+ return errors.Trace(err)
}
// Open it and calculate the SHA256 hash.
@@ -4,8 +4,6 @@
package client_test
import (
- "bytes"
- "encoding/json"
"fmt"
"io"
"net/http"
@@ -24,11 +22,10 @@ import (
"gopkg.in/juju/charm.v5-unstable"
"gopkg.in/juju/charm.v5-unstable/charmrepo"
"gopkg.in/juju/charmstore.v4"
+ "gopkg.in/juju/charmstore.v4/charmstoretesting"
"gopkg.in/juju/charmstore.v4/csclient"
- charmstoretesting "gopkg.in/juju/charmstore.v4/testing"
"gopkg.in/macaroon-bakery.v0/bakery/checkers"
"gopkg.in/macaroon-bakery.v0/bakerytest"
- "gopkg.in/macaroon-bakery.v0/httpbakery"
"gopkg.in/macaroon.v1"
"gopkg.in/mgo.v2"
@@ -1583,14 +1580,15 @@ type clientRepoSuite struct {
// macaroons for. If it is empty, no caveats will be discharged.
dischargeUser string
- srv *charmstoretesting.Server
+ discharger *bakerytest.Discharger
+ srv *charmstoretesting.Server
}
var _ = gc.Suite(&clientRepoSuite{})
func (s *clientRepoSuite) SetUpTest(c *gc.C) {
s.baseSuite.SetUpTest(c)
- discharger := bakerytest.NewDischarger(nil, func(cond string, arg string) ([]checkers.Caveat, error) {
+ s.discharger = bakerytest.NewDischarger(nil, func(_ *http.Request, cond string, arg string) ([]checkers.Caveat, error) {
if s.dischargeUser == "" {
return nil, fmt.Errorf("discharge denied")
}
@@ -1599,10 +1597,8 @@ func (s *clientRepoSuite) SetUpTest(c *gc.C) {
}, nil
})
s.srv = charmstoretesting.OpenServer(c, s.Session, charmstore.ServerParams{
- IdentityLocation: discharger.Location(),
- PublicKeyLocator: discharger,
- AuthUsername: "test-user",
- AuthPassword: "test-password",
+ IdentityLocation: s.discharger.Location(),
+ PublicKeyLocator: s.discharger,
})
s.PatchValue(&charmrepo.CacheDir, c.MkDir())
s.PatchValue(client.NewCharmStore, func(p charmrepo.NewCharmStoreParams) charmrepo.Interface {
@@ -1612,6 +1608,7 @@ func (s *clientRepoSuite) SetUpTest(c *gc.C) {
}
func (s *clientRepoSuite) TearDownTest(c *gc.C) {
+ s.discharger.Close()
s.srv.Close()
s.baseSuite.TearDownTest(c)
}
@@ -1854,8 +1851,8 @@ func (s *clientRepoSuite) assertPrincipalDeployed(c *gc.C, serviceName string, c
bundle.Meta().Storage[name] = bundleMeta
}
}
- c.Assert(charm.Meta(), gc.DeepEquals, bundle.Meta())
- c.Assert(charm.Config(), gc.DeepEquals, bundle.Config())
+ c.Assert(charm.Meta(), jc.DeepEquals, bundle.Meta())
+ c.Assert(charm.Config(), jc.DeepEquals, bundle.Config())
serviceCons, err := service.Constraints()
c.Assert(err, jc.ErrorIsNil)
@@ -3549,31 +3546,20 @@ func (s *clientRepoSuite) TestAddCharm(c *gc.C) {
func (s *clientRepoSuite) TestAddCharmWithAuthorization(c *gc.C) {
// Upload a new charm to the charm store.
curl, _ := s.uploadCharm(c, "cs:~restricted/precise/wordpress-3", "wordpress")
- client := csclient.New(csclient.Params{
- URL: s.srv.URL(),
- })
// Change permissions on the new charm such that only bob
// can read from it.
s.dischargeUser = "restricted"
- data, err := json.Marshal([]string{"bob"})
- c.Assert(err, gc.IsNil)
- body := httpbakery.SeekerBody(bytes.NewReader(data))
- req, err := http.NewRequest("PUT", "", nil)
- c.Assert(err, gc.IsNil)
- req.Header.Set("Content-Type", "application/json")
- resp, err := client.DoWithBody(req, "/~restricted/wordpress/meta/perm/read", body)
- c.Assert(err, gc.IsNil)
- resp.Body.Close()
- c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
+ err := s.srv.NewClient().Put("/"+curl.Path()+"/meta/perm/read", []string{"bob"})
+ c.Assert(err, jc.ErrorIsNil)
// Try to add a charm to the environment without authorization.
s.dischargeUser = ""
err = s.APIState.Client().AddCharm(curl)
- c.Assert(err, gc.ErrorMatches, `cannot retrieve charm "cs:~restricted/precise/wordpress-3": cannot get archive: cannot get discharge from ".*": cannot discharge: discharge denied`)
+ c.Assert(err, gc.ErrorMatches, `cannot retrieve charm "cs:~restricted/precise/wordpress-3": cannot get archive: cannot get discharge from ".*": third party refused discharge: cannot discharge: discharge denied`)
tryAs := func(user string) error {
- client = csclient.New(csclient.Params{
+ client := csclient.New(csclient.Params{
URL: s.srv.URL(),
})
s.dischargeUser = user
View
@@ -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
}
View
@@ -13,10 +13,8 @@ import (
"github.com/juju/names"
"github.com/juju/utils/featureflag"
"gopkg.in/juju/charm.v5-unstable"
- "gopkg.in/juju/charm.v5-unstable/charmrepo"
"launchpad.net/gnuflag"
- "github.com/juju/juju/api"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/cmd/envcmd"
"github.com/juju/juju/cmd/juju/block"
@@ -183,16 +181,17 @@ func (c *DeployCommand) Run(ctx *cmd.Context) error {
return err
}
- csParams, err := charmStoreParams()
+ csClient, err := newCharmStoreClient()
if err != nil {
return errors.Trace(err)
}
- curl, repo, err := resolveCharmURL(c.CharmName, csParams, ctx.AbsPath(c.RepoPath), conf)
+ defer csClient.jar.Save()
+ curl, repo, err := resolveCharmURL(c.CharmName, csClient.params, ctx.AbsPath(c.RepoPath), conf)
if err != nil {
return errors.Trace(err)
}
- curl, err = addCharmViaAPI(client, ctx, curl, repo)
+ curl, err = addCharmViaAPI(client, ctx, curl, repo, csClient)
if err != nil {
return block.ProcessBlockedError(err, block.BlockChange)
}
@@ -271,33 +270,6 @@ func (c *DeployCommand) Run(ctx *cmd.Context) error {
return block.ProcessBlockedError(err, block.BlockChange)
}
-// addCharmViaAPI calls the appropriate client API calls to add the
-// given charm URL to state. Also displays the charm URL of the added
-// charm on stdout.
-func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charmrepo.Interface) (*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":
- err := client.AddCharm(curl)
- if err != nil {
- return nil, 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
-}
-
// parseNetworks returns a list of network names by parsing the
// comma-delimited string value of --networks argument.
func parseNetworks(networksValue string) []string {
Oops, something went wrong.