cmd/juju/user: consult external directory server to find out host names for public controllers #7087

Merged
merged 1 commit into from Mar 10, 2017
Jump to file or symbol
Failed to load files and symbols.
+70 −15
Split
View
@@ -5,13 +5,14 @@ package user
import (
"fmt"
+ "net/http"
"os"
"strings"
"github.com/juju/cmd"
"github.com/juju/errors"
"github.com/juju/gnuflag"
- "github.com/juju/utils/keyvalues"
+ "github.com/juju/httprequest"
"github.com/juju/utils/set"
"gopkg.in/juju/names.v2"
@@ -112,7 +113,7 @@ func (c *loginCommand) SetFlags(fset *gnuflag.FlagSet) {
c.ControllerCommandBase.SetFlags(fset)
fset.StringVar(&c.controllerName, "c", "", "Controller to operate in")
fset.StringVar(&c.controllerName, "controller", "", "")
- fset.BoolVar(&c.forceHost, "host", false, "force the domain argument to be treated as the host name of a controller")
+ fset.BoolVar(&c.forceHost, "host", false, "force the domain argument to be treated as the host name of a controller rather than a user name")
}
// Init implements Command.Init.
@@ -229,12 +230,11 @@ Run "juju logout" first before attempting to log in as a different user.`)
}
func (c *loginCommand) controllerLogin(ctx *cmd.Context, store jujuclient.ClientStore) error {
- knownDomains, err := c.getKnownControllerDomains()
- if err != nil {
- // TODO(rogpeppe) perhaps this shouldn't be fatal.
- return errors.Trace(err)
+ // TODO(rog) provide a way of avoiding this external network access.
+ controllerHost, err := c.getKnownControllerDomain(c.userOrDomain)
+ if err != nil && !errors.IsNotFound(err) {
+ logger.Warningf("could not determine controller domain: %v", err)
}
- controllerHost := knownDomains[c.userOrDomain]
if controllerHost == "" {
if !c.forceHost {
return errNotControllerLogin
@@ -419,15 +419,49 @@ one of them:
return nil
}
-// getKnownControllerDomains returns the list of known
+type controllerDomainResponse struct {
+ Host string `json:"host"`
+}
+
+const defaultJujuDirectory = "https://api.jujucharms.com/directory"
+
+// getKnownControllerDomain returns the list of known
// controller domain aliases.
-func (c *loginCommand) getKnownControllerDomains() (map[string]string, error) {
- controllers := os.Getenv("JUJU_PUBLIC_CONTROLLERS")
- m, err := keyvalues.Parse(strings.Fields(controllers), false)
+func (c *loginCommand) getKnownControllerDomain(name string) (string, error) {
+ if strings.Contains(name, ".") || strings.Contains(name, ":") {
+ return "", errors.NotFoundf("controller %q", name)
+ }
+ baseURL := defaultJujuDirectory
+ if u := os.Getenv("JUJU_DIRECTORY"); u != "" {
+ baseURL = u
+ }
+ client, err := c.BakeryClient()
if err != nil {
- return nil, errors.Annotatef(err, "bad value for JUJU_PUBLIC_CONTROLLERS")
+ return "", errors.Trace(err)
+ }
+ req, err := http.NewRequest("GET", baseURL+"/v1/controller/"+name, nil)
+ if err != nil {
+ return "", errors.Trace(err)
+ }
+ httpResp, err := client.Do(req)
+ if err != nil {
+ return "", errors.Trace(err)
+ }
+ defer httpResp.Body.Close()
+ if httpResp.StatusCode != http.StatusOK {
+ if httpResp.StatusCode == http.StatusNotFound {
+ return "", errors.NotFoundf("controller %q", name)
+ }
+ return "", errors.Errorf("unexpected HTTP response %q", httpResp.Status)
+ }
+ var resp controllerDomainResponse
+ if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil {
+ return "", errors.Trace(err)
+ }
+ if resp.Host == "" {
+ return "", errors.Errorf("no host field found in response")
}
- return m, nil
+ return resp.Host, nil
}
func friendlyUserName(user string) string {
@@ -4,6 +4,7 @@ package user_test
import (
"bytes"
+ "fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -61,6 +62,7 @@ func (s *LoginControllerSuite) SetUpTest(c *gc.C) {
return s.apiConnection, nil
})
s.PatchValue(user.LoginClientStore, s.store)
+ s.PatchEnvironment("JUJU_DIRECTORY", "http://0.1.2.3/directory")
}
func (s *LoginControllerSuite) TearDownTest(c *gc.C) {
@@ -69,7 +71,11 @@ func (s *LoginControllerSuite) TearDownTest(c *gc.C) {
}
func (s *LoginControllerSuite) TestRegisterPublic(c *gc.C) {
- os.Setenv("JUJU_PUBLIC_CONTROLLERS", "bighost=bighost.jujucharms.com")
+ dirSrv := serveDirectory(map[string]string{
+ "bighost": "bighost.jujucharms.com:443",
+ })
+ defer dirSrv.Close()
+ os.Setenv("JUJU_DIRECTORY", dirSrv.URL)
s.apiConnection.authTag = names.NewUserTag("bob@external")
s.apiConnection.controllerAccess = "login"
stdout, stderr, code := s.run(c, "bighost")
@@ -156,7 +162,9 @@ Welcome, bob@external. You are now logged into "foo".
}
func (s *LoginControllerSuite) TestRegisterPublicAPIOpenError(c *gc.C) {
- os.Setenv("JUJU_PUBLIC_CONTROLLERS", "bighost=bighost.jujucharms.com")
+ srv := serveDirectory(map[string]string{"bighost": "https://0.1.2.3/directory"})
+ defer srv.Close()
+ os.Setenv("JUJU_DIRECTORY", srv.URL)
*user.APIOpen = func(c *modelcmd.JujuCommandBase, info *api.Info, opts api.DialOpts) (api.Connection, error) {
return nil, errors.New("open failed")
}
@@ -216,3 +224,15 @@ func (m *loginMockAPI) ControllerAccess() string {
}
const mockControllerUUID = "df136476-12e9-11e4-8a70-b2227cce2b54"
+
+func serveDirectory(dir map[string]string) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ name := strings.TrimPrefix(req.URL.Path, "/v1/controller/")
+ if name == req.URL.Path || dir[name] == "" {
+ http.NotFound(w, req)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintf(w, `{"host":%q}`, dir[name])
+ }))
+}
@@ -63,6 +63,7 @@ func (s *cmdLoginSuite) TestLoginCommand(c *gc.C) {
context := s.run(c, strings.NewReader("hunter2\nhunter2\n"), "login", "test")
c.Assert(testing.Stdout(context), gc.Equals, "")
c.Assert(testing.Stderr(context), gc.Equals, `
+WARNING could not determine controller domain: Get https://api.jujucharms.com/directory/v1/controller/test: access to address "api.jujucharms.com:443" not allowed
please enter password for test on kontroll:
You are now logged in to "kontroll" as "test".
`[1:])