Skip to content

Commit

Permalink
Merge pull request juju#6147 from wallyworld/cache-controller-details
Browse files Browse the repository at this point in the history
Cache user access level and agent version in controller.yaml

For each user in accounts.yaml, we cache their access level on the controller. We also cache the agent version of the controller in comtrollers.yaml. This information is displayed in list-controllers.

At bootstrap, the agent version and access level are set, to the bootstrap agent version and "superuser" respectively. Thereafter, each time the user logs in, or a connection is made to the controller via running a command, the information is also updated.

(Review request: http://reviews.vapour.ws/r/5585/)
  • Loading branch information
jujubot authored Sep 5, 2016
2 parents e5e84a8 + de58bd8 commit df3f84c
Show file tree
Hide file tree
Showing 37 changed files with 387 additions and 179 deletions.
8 changes: 5 additions & 3 deletions api/apiclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ type state struct {
// authTag holds the authenticated entity's tag after login.
authTag names.Tag

// readOnly holds whether the user has read-only access for the
// connected model.
readOnly bool
// mpdelAccess holds the access level of the user to the connected model.
modelAccess string

// controllerAccess holds the access level of the user to the connected controller.
controllerAccess string

// broken is a channel that gets closed when the connection is
// broken.
Expand Down
8 changes: 5 additions & 3 deletions api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,11 @@ type Connection interface {
// connection.
AuthTag() names.Tag

// ReadOnly returns whether the authorized user is connected to the model
// in read-only mode.
ReadOnly() bool
// ModelAccess returns the access level of authorized user to the model.
ModelAccess() string

// ControllerAccess returns the access level of authorized user to the controller.
ControllerAccess() string

// These methods expose a bunch of worker-specific facades, and basically
// just should not exist; but removing them is too noisy for a single CL.
Expand Down
47 changes: 28 additions & 19 deletions api/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,25 @@ func (st *state) Login(tag names.Tag, password, nonce string, macaroons []macaro
}
}

var readOnly bool
var controllerAccess string
var modelAccess string
if result.UserInfo != nil {
tag, err = names.ParseTag(result.UserInfo.Identity)
if err != nil {
return errors.Trace(err)
}
readOnly = result.UserInfo.ReadOnly
controllerAccess = result.UserInfo.ControllerAccess
modelAccess = result.UserInfo.ModelAccess
}
servers := params.NetworkHostsPorts(result.Servers)
if err = st.setLoginResult(loginResultParams{
tag: tag,
modelTag: result.ModelTag,
controllerTag: result.ControllerTag,
servers: servers,
facades: result.Facades,
readOnly: readOnly,
tag: tag,
modelTag: result.ModelTag,
controllerTag: result.ControllerTag,
servers: servers,
facades: result.Facades,
modelAccess: modelAccess,
controllerAccess: controllerAccess,
}); err != nil {
return errors.Trace(err)
}
Expand All @@ -122,12 +125,13 @@ func (st *state) Login(tag names.Tag, password, nonce string, macaroons []macaro
}

type loginResultParams struct {
tag names.Tag
modelTag string
controllerTag string
readOnly bool
servers [][]network.HostPort
facades []params.FacadeVersions
tag names.Tag
modelTag string
controllerTag string
modelAccess string
controllerAccess string
servers [][]network.HostPort
facades []params.FacadeVersions
}

func (st *state) setLoginResult(p loginResultParams) error {
Expand All @@ -148,7 +152,8 @@ func (st *state) setLoginResult(p loginResultParams) error {
return errors.Annotatef(err, "invalid controller tag %q returned from login", p.controllerTag)
}
st.controllerTag = ctag
st.readOnly = p.readOnly
st.controllerAccess = p.controllerAccess
st.modelAccess = p.modelAccess

hostPorts, err := addAddress(p.servers, st.addr)
if err != nil {
Expand All @@ -173,10 +178,14 @@ func (st *state) AuthTag() names.Tag {
return st.authTag
}

// ReadOnly returns whether the authorized user is connected to the model in
// read-only mode.
func (st *state) ReadOnly() bool {
return st.readOnly
// ModelAccess returns the access level of authorized user to the model.
func (st *state) ModelAccess() string {
return st.modelAccess
}

// ControllerAccess returns the access level of authorized user to the model.
func (st *state) ControllerAccess() string {
return st.controllerAccess
}

// slideAddressToFront moves the address at the location (serverIndex, addrIndex) to be
Expand Down
27 changes: 22 additions & 5 deletions api/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,10 @@ func (s *stateSuite) TestLoginMacaroon(c *gc.C) {
c.Assert(apistate.AuthTag(), gc.Equals, tag)
}

func (s *stateSuite) TestLoginReadOnly(c *gc.C) {
// The default user has read and write access.
c.Assert(s.APIState.ReadOnly(), jc.IsFalse)
func (s *stateSuite) TestLoginSetsModelAccess(c *gc.C) {
// The default user has admin access.
c.Assert(s.APIState.ModelAccess(), gc.Equals, "admin")

// Check with an user in read-only mode.
manager := usermanager.NewClient(s.OpenControllerAPI(c))
defer manager.Close()
usertag, _, err := manager.AddUser("ro", "ro", "ro-password")
Expand All @@ -127,7 +126,25 @@ func (s *stateSuite) TestLoginReadOnly(c *gc.C) {
err = mmanager.GrantModel(usertag.Canonical(), "read", modeltag.Id())
c.Assert(err, jc.ErrorIsNil)
conn := s.OpenAPIAs(c, usertag, "ro-password")
c.Assert(conn.ReadOnly(), jc.IsTrue)
c.Assert(conn.ModelAccess(), gc.Equals, "read")
}

func (s *stateSuite) TestLoginSetsControllerAccess(c *gc.C) {
// The default user has admin access.
c.Assert(s.APIState.ControllerAccess(), gc.Equals, "superuser")

manager := usermanager.NewClient(s.OpenControllerAPI(c))
defer manager.Close()
usertag, _, err := manager.AddUser("ro", "ro", "ro-password")
c.Assert(err, jc.ErrorIsNil)
mmanager := modelmanager.NewClient(s.OpenControllerAPI(c))
defer mmanager.Close()
modeltag, ok := s.APIState.ModelTag()
c.Assert(ok, jc.IsTrue)
err = mmanager.GrantModel(usertag.Canonical(), "read", modeltag.Id())
c.Assert(err, jc.ErrorIsNil)
conn := s.OpenAPIAs(c, usertag, "ro-password")
c.Assert(conn.ControllerAccess(), gc.Equals, "login")
}

func (s *stateSuite) TestLoginMacaroonInvalidId(c *gc.C) {
Expand Down
8 changes: 4 additions & 4 deletions apiserver/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@ func (a *admin) login(req params.LoginRequest, loginVersion int) (params.LoginRe
description.IsEmptyUserAccess(everyoneGroupUser) {
return fail, errors.NotFoundf("model or controller access for logged in user %q", userTag.Canonical())
}
maybeUserInfo.ReadOnly = modelUser.Access == description.ReadAccess
if maybeUserInfo.ReadOnly {
logger.Debugf("model user %s is READ ONLY", entity.Tag())
}
maybeUserInfo.ControllerAccess = string(controllerUser.Access)
maybeUserInfo.ModelAccess = string(modelUser.Access)
logger.Tracef("controller user %s has %v", entity.Tag(), controllerUser.Access)
logger.Tracef("model user %s has %s", entity.Tag(), modelUser.Access)
}

// Fetch the API server addresses from state.
Expand Down
7 changes: 4 additions & 3 deletions apiserver/params/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,10 @@ type AuthUserInfo struct {
// the client, if any.
Credentials *string `json:"credentials,omitempty"`

// ReadOnly holds whether the user has read-only access for the
// connected model.
ReadOnly bool `json:"read-only"`
// ControllerAccess holds the access the user has to the connected controller.
ControllerAccess string `json:"controller-access"`
// ModelAccess holds the access the user has to the connected model.
ModelAccess string `json:"model-access"`
}

// LoginResult holds the result of an Admin Login call.
Expand Down
2 changes: 1 addition & 1 deletion cmd/juju/backups/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ func (c *restoreCommand) rebootstrap(ctx *cmd.Context, meta *params.BackupsMetad
// New controller is bootstrapped, so now record the API address so
// we can connect.
apiPort := params.ControllerConfig.APIPort()
err = common.SetBootstrapEndpointAddress(store, c.ControllerName(), apiPort, env)
err = common.SetBootstrapEndpointAddress(store, c.ControllerName(), bootVers, apiPort, env)
if err != nil {
return errors.Trace(err)
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/juju/backups/restore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
_ "github.com/juju/juju/provider/dummy"
_ "github.com/juju/juju/provider/lxd"
"github.com/juju/juju/testing"
"github.com/juju/juju/version"
)

type restoreSuite struct {
Expand Down Expand Up @@ -250,6 +251,7 @@ func (s *restoreSuite) TestRestoreReboostrapWritesUpdatedControllerInfo(c *gc.C)
ControllerUUID: "deadbeef-1bad-500d-9000-4b1d0d06f00d",
APIEndpoints: []string{"10.0.0.1:17777"},
UnresolvedAPIEndpoints: []string{"10.0.0.1:17777"},
AgentVersion: version.Current.String(),
})
}

Expand Down
6 changes: 5 additions & 1 deletion cmd/juju/commands/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,11 @@ See `[1:] + "`juju kill-controller`" + `.`)
return errors.Trace(err)
}

err = common.SetBootstrapEndpointAddress(c.ClientStore(), c.controllerName, controllerConfig.APIPort(), environ)
agentVersion := jujuversion.Current
if c.AgentVersion != nil {
agentVersion = *c.AgentVersion
}
err = common.SetBootstrapEndpointAddress(c.ClientStore(), c.controllerName, agentVersion, controllerConfig.APIPort(), environ)
if err != nil {
return errors.Annotate(err, "saving bootstrap endpoint address")
}
Expand Down
21 changes: 14 additions & 7 deletions cmd/juju/commands/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,19 @@ func (s *BootstrapSuite) run(c *gc.C, test bootstrapTest) testing.Restorer {
var restore testing.Restorer = func() {
s.store = jujuclienttesting.NewMemStore()
}
bootstrapVersion := v100p64
if test.version != "" {
useVersion := strings.Replace(test.version, "%LTS%", series.LatestLts(), 1)
v := version.MustParseBinary(useVersion)
restore = restore.Add(testing.PatchValue(&jujuversion.Current, v.Number))
restore = restore.Add(testing.PatchValue(&arch.HostArch, func() string { return v.Arch }))
restore = restore.Add(testing.PatchValue(&series.HostSeries, func() string { return v.Series }))
v.Build = 1
bootstrapVersion = version.MustParseBinary(useVersion)
restore = restore.Add(testing.PatchValue(&jujuversion.Current, bootstrapVersion.Number))
restore = restore.Add(testing.PatchValue(&arch.HostArch, func() string { return bootstrapVersion.Arch }))
restore = restore.Add(testing.PatchValue(&series.HostSeries, func() string { return bootstrapVersion.Series }))
bootstrapVersion.Build = 1
if test.upload != "" {
uploadVers := version.MustParseBinary(test.upload)
v.Number = uploadVers.Number
bootstrapVersion.Number = uploadVers.Number
}
restore = restore.Add(testing.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c, &v.Number)))
restore = restore.Add(testing.PatchValue(&envtools.BundleTools, toolstesting.GetMockBundleTools(c, &bootstrapVersion.Number)))
}

if test.hostArch != "" {
Expand Down Expand Up @@ -246,6 +247,12 @@ func (s *BootstrapSuite) run(c *gc.C, test bootstrapTest) testing.Restorer {
c.Assert(controller.UnresolvedAPIEndpoints, gc.DeepEquals, addrConnectedTo)
c.Assert(controller.APIEndpoints, gc.DeepEquals, addrConnectedTo)
c.Assert(utils.IsValidUUIDString(controller.ControllerUUID), jc.IsTrue)
// We don't care about build numbers here.
bootstrapVers := bootstrapVersion.Number
bootstrapVers.Build = 0
controllerVers := version.MustParse(controller.AgentVersion)
controllerVers.Build = 0
c.Assert(controllerVers.String(), gc.Equals, bootstrapVers.String())

controllerModel, err := s.store.ModelByName(controllerName, "admin@local/controller")
c.Assert(err, jc.ErrorIsNil)
Expand Down
15 changes: 10 additions & 5 deletions cmd/juju/common/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ import (
"github.com/juju/juju/juju"
"github.com/juju/juju/jujuclient"
"github.com/juju/juju/network"
"github.com/juju/version"
)

var allInstances = func(environ environs.Environ) ([]instance.Instance, error) {
return environ.AllInstances()
}

// SetBootstrapEndpointAddress writes the API endpoint address of the
// bootstrap server into the connection information. This should only be run
// once directly after Bootstrap. It assumes that there is just one instance
// in the environment - the bootstrap instance.
func SetBootstrapEndpointAddress(store jujuclient.ControllerStore, controllerName string, apiPort int, environ environs.Environ) error {
// bootstrap server, plus the agent version, into the connection information.
// This should only be run once directly after Bootstrap. It assumes that
// there is just one instance in the environment - the bootstrap instance.
func SetBootstrapEndpointAddress(
store jujuclient.ControllerStore,
controllerName string, agentVersion version.Number,
apiPort int, environ environs.Environ,
) error {
instances, err := allInstances(environ)
if err != nil {
return errors.Trace(err)
Expand All @@ -54,7 +59,7 @@ func SetBootstrapEndpointAddress(store jujuclient.ControllerStore, controllerNam
return errors.Annotate(err, "failed to get bootstrap instance addresses")
}
apiHostPorts := network.AddressesWithPort(netAddrs, apiPort)
return juju.UpdateControllerAddresses(store, controllerName, nil, apiHostPorts...)
return juju.UpdateControllerDetailsFromLogin(store, controllerName, agentVersion.String(), nil, apiHostPorts...)
}

var (
Expand Down
3 changes: 2 additions & 1 deletion cmd/juju/controller/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (

// NewListControllersCommandForTest returns a listControllersCommand with the clientstore provided
// as specified.
func NewListControllersCommandForTest(testStore jujuclient.ClientStore) *listControllersCommand {
func NewListControllersCommandForTest(testStore jujuclient.ClientStore, api api.Connection) *listControllersCommand {
return &listControllersCommand{
store: testStore,
api: api,
}
}

Expand Down
43 changes: 40 additions & 3 deletions cmd/juju/controller/listcontrollers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ package controller
import (
"fmt"
"strings"
"sync"

"github.com/juju/cmd"
"github.com/juju/errors"
"github.com/juju/gnuflag"

"github.com/juju/juju/api"
"github.com/juju/juju/cmd/modelcmd"
"github.com/juju/juju/jujuclient"
)
Expand Down Expand Up @@ -51,19 +53,52 @@ func (c *listControllersCommand) Info() *cmd.Info {
// SetFlags implements Command.SetFlags.
func (c *listControllersCommand) SetFlags(f *gnuflag.FlagSet) {
c.JujuCommandBase.SetFlags(f)
f.BoolVar(&c.refresh, "refresh", false, "Connect to each controller to download the latest details")
c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{
"yaml": cmd.FormatYaml,
"json": cmd.FormatJson,
"tabular": formatControllersListTabular,
"tabular": c.formatControllersListTabular,
})
}

func (c *listControllersCommand) getAPI(controllerName string) (api.Connection, error) {
if c.api != nil {
return c.api, nil
}
return c.NewAPIRoot(c.store, controllerName, "")
}

// Run implements Command.Run
func (c *listControllersCommand) Run(ctx *cmd.Context) error {
controllers, err := c.store.AllControllers()
if err != nil {
return errors.Annotate(err, "failed to list controllers")
}
if c.refresh && len(controllers) > 0 {
// For each controller, simply opening an API
// connection is enough to login and refresh the
// cached data.
var wg sync.WaitGroup
wg.Add(len(controllers))
for controllerName := range controllers {
name := controllerName
go func() {
defer wg.Done()
client, err := c.getAPI(name)
if err != nil {
fmt.Fprintf(ctx.GetStderr(), "error updating cached details for %q: %v", name, err)
return
}
client.Close()
}()
}
wg.Wait()
// Reload controller details
controllers, err = c.store.AllControllers()
if err != nil {
return errors.Annotate(err, "failed to list controllers")
}
}
details, errs := c.convertControllerDetails(controllers)
if len(errs) > 0 {
fmt.Fprintln(ctx.Stderr, strings.Join(errs, "\n"))
Expand All @@ -84,6 +119,8 @@ func (c *listControllersCommand) Run(ctx *cmd.Context) error {
type listControllersCommand struct {
modelcmd.JujuCommandBase

out cmd.Output
store jujuclient.ClientStore
out cmd.Output
store jujuclient.ClientStore
api api.Connection
refresh bool
}
Loading

0 comments on commit df3f84c

Please sign in to comment.