diff --git a/apiserver/watcher.go b/apiserver/watcher.go index f8a349aa0ca..b8d349f1465 100644 --- a/apiserver/watcher.go +++ b/apiserver/watcher.go @@ -137,7 +137,7 @@ func (aw *SrvAllWatcher) translate(deltas []multiwatcher.Delta) []params.Delta { } func (aw *SrvAllWatcher) translateModel(info multiwatcher.EntityInfo) params.EntityInfo { - orig, ok := info.(*multiwatcher.ModelUpdate) + orig, ok := info.(*multiwatcher.ModelInfo) if !ok { logger.Criticalf("consistency error: %s", pretty.Sprint(info)) return nil diff --git a/core/cache/cachetest/state.go b/core/cache/cachetest/state.go index d46faaa7418..c8e932217ab 100644 --- a/core/cache/cachetest/state.go +++ b/core/cache/cachetest/state.go @@ -4,6 +4,8 @@ package cachetest import ( + "strings" + "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" @@ -12,6 +14,7 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/lxdprofile" "github.com/juju/juju/core/model" + "github.com/juju/juju/core/permission" "github.com/juju/juju/network" "github.com/juju/juju/state" ) @@ -32,13 +35,22 @@ func ModelChange(c *gc.C, model *state.Model) cache.ModelChange { status, err := model.Status() c.Assert(err, jc.ErrorIsNil) + users, err := model.Users() + permissions := make(map[string]permission.Access) + for _, user := range users { + // Cache permission map is always lower case. + permissions[strings.ToLower(user.UserName)] = user.Access + } + return cache.ModelChange{ - ModelUUID: model.UUID(), - Name: model.Name(), - Life: life.Value(model.Life().String()), - Owner: model.Owner().Name(), - Config: cfg.AllAttrs(), - Status: status, + ModelUUID: model.UUID(), + Name: model.Name(), + Life: life.Value(model.Life().String()), + Owner: model.Owner().Name(), + IsController: model.IsControllerModel(), + Config: cfg.AllAttrs(), + Status: status, + UserPermissions: permissions, } } diff --git a/core/cache/changes.go b/core/cache/changes.go index fa1c207bad3..ad928ed8cf7 100644 --- a/core/cache/changes.go +++ b/core/cache/changes.go @@ -11,6 +11,7 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/lxdprofile" "github.com/juju/juju/core/network" + "github.com/juju/juju/core/permission" "github.com/juju/juju/core/settings" "github.com/juju/juju/core/status" ) @@ -18,12 +19,18 @@ import ( // ModelChange represents either a new model, or a change // to an existing model. type ModelChange struct { - ModelUUID string - Name string - Life life.Value - Owner string // tag maybe? - Config map[string]interface{} - Status status.StatusInfo + ModelUUID string + Name string + Life life.Value + Owner string // tag maybe? + IsController bool + Cloud string + CloudRegion string + CloudCredential string + Config map[string]interface{} + Status status.StatusInfo + + UserPermissions map[string]permission.Access } // RemoveModel represents the situation when a model is removed @@ -171,6 +178,44 @@ type RemoveUnit struct { Name string } +// RelationChange represents either a new relation, or a change +// to an existing relation in a model. +type RelationChange struct { + ModelUUID string + Key string + Endpoints []Endpoint +} + +// Endpoint holds all relevant information about a relation endpoint. +type Endpoint struct { + Application string + Name string + Role string + Interface string + Optional bool + Limit int + Scope string +} + +// copy returns a deep copy of the RelationChange. +func (c RelationChange) copy() RelationChange { + if existing := c.Endpoints; existing != nil { + endpoints := make([]Endpoint, len(existing)) + for i, ep := range existing { + endpoints[i] = ep + } + c.Endpoints = existing + } + return c +} + +// RemoveRelation represents the situation when a relation +// is removed from a model in the database. +type RemoveRelation struct { + ModelUUID string + Key string +} + // MachineChange represents either a new machine, or a change // to an existing machine in a model. type MachineChange struct { diff --git a/core/cache/controller.go b/core/cache/controller.go index 967c5126ac7..f7b2c10990c 100644 --- a/core/cache/controller.go +++ b/core/cache/controller.go @@ -146,6 +146,10 @@ func (c *Controller) loop() error { c.updateUnit(ch) case RemoveUnit: err = c.removeUnit(ch) + case RelationChange: + c.updateRelation(ch) + case RemoveRelation: + err = c.removeRelation(ch) case BranchChange: c.updateBranch(ch) case RemoveBranch: @@ -303,6 +307,16 @@ func (c *Controller) removeUnit(ch RemoveUnit) error { return errors.Trace(c.removeResident(ch.ModelUUID, func(m *Model) error { return m.removeUnit(ch) })) } +// updateRelation adds or updates the relation in the specified model. +func (c *Controller) updateRelation(ch RelationChange) { + c.ensureModel(ch.ModelUUID).updateRelation(ch, c.manager) +} + +// removeRelation removes the relation from the cached model. +func (c *Controller) removeRelation(ch RemoveRelation) error { + return errors.Trace(c.removeResident(ch.ModelUUID, func(m *Model) error { return m.removeRelation(ch) })) +} + // updateMachine adds or updates the machine in the specified model. func (c *Controller) updateMachine(ch MachineChange) { c.ensureModel(ch.ModelUUID).updateMachine(ch, c.manager) diff --git a/core/cache/controller_test.go b/core/cache/controller_test.go index 585ed1d025d..a78ff708504 100644 --- a/core/cache/controller_test.go +++ b/core/cache/controller_test.go @@ -57,6 +57,7 @@ func (s *ControllerSuite) TestAddModel(c *gc.C) { "charm-count": 0, "machine-count": 0, "unit-count": 0, + "relation-count": 0, "branch-count": 0, }}) @@ -280,6 +281,38 @@ func (s *ControllerSuite) TestRemoveUnit(c *gc.C) { s.AssertResident(c, unit.CacheId(), false) } +func (s *ControllerSuite) TestAddRelation(c *gc.C) { + controller, events := s.new(c) + s.processChange(c, relationChange, events) + + mod, err := controller.Model(relationChange.ModelUUID) + c.Assert(err, jc.ErrorIsNil) + c.Check(mod.Report()["relation-count"], gc.Equals, 1) + + relation, err := mod.Relation(relationChange.Key) + c.Assert(err, jc.ErrorIsNil) + s.AssertResident(c, relation.CacheId(), true) +} + +func (s *ControllerSuite) TestRemoveRelation(c *gc.C) { + controller, events := s.new(c) + s.processChange(c, relationChange, events) + + mod, err := controller.Model(relationChange.ModelUUID) + c.Assert(err, jc.ErrorIsNil) + relation, err := mod.Relation(relationChange.Key) + c.Assert(err, jc.ErrorIsNil) + + remove := cache.RemoveRelation{ + ModelUUID: modelChange.ModelUUID, + Key: relationChange.Key, + } + s.processChange(c, remove, events) + + c.Check(mod.Report()["relation-count"], gc.Equals, 0) + s.AssertResident(c, relation.CacheId(), false) +} + func (s *ControllerSuite) TestAddBranch(c *gc.C) { controller, events := s.new(c) s.processChange(c, branchChange, events) @@ -380,6 +413,10 @@ func (s *ControllerSuite) captureEvents(c *gc.C) <-chan interface{} { send = true case cache.RemoveUnit: send = true + case cache.RelationChange: + send = true + case cache.RemoveRelation: + send = true case cache.BranchChange: send = true case cache.RemoveBranch: diff --git a/core/cache/model.go b/core/cache/model.go index 338cfb616cb..1ee66ec51a0 100644 --- a/core/cache/model.go +++ b/core/cache/model.go @@ -36,6 +36,7 @@ func newModel(metrics *ControllerGauges, hub *pubsub.SimpleHub, res *Resident) * charms: make(map[string]*Charm), machines: make(map[string]*Machine), units: make(map[string]*Unit), + relations: make(map[string]*Relation), branches: make(map[string]*Branch), } return m @@ -59,6 +60,7 @@ type Model struct { charms map[string]*Charm machines map[string]*Machine units map[string]*Unit + relations map[string]*Relation branches map[string]*Branch } @@ -105,6 +107,7 @@ func (m *Model) Report() map[string]interface{} { "charm-count": len(m.charms), "machine-count": len(m.machines), "unit-count": len(m.units), + "relation-count": len(m.relations), "branch-count": len(m.branches), } } @@ -117,7 +120,7 @@ func (m *Model) Branches() []Branch { i := 0 for _, b := range m.branches { branches[i] = b.copy() - i += 1 + i++ } m.mu.Unlock() @@ -341,6 +344,59 @@ func (m *Model) removeUnit(ch RemoveUnit) error { return nil } +// Relation returns the relation with the specified key. +// If the relation is not found, a NotFoundError is returned. +func (m *Model) Relation(key string) (Relation, error) { + defer m.doLocked()() + + relation, found := m.relations[key] + if !found { + return Relation{}, errors.NotFoundf("relation %q", key) + } + return relation.copy(), nil +} + +// Relations returns all relations in the model. +func (m *Model) Relations() map[string]Relation { + m.mu.Lock() + + relations := make(map[string]Relation, len(m.relations)) + for key, r := range m.relations { + relations[key] = r.copy() + } + + m.mu.Unlock() + return relations +} + +// updateRelation adds or updates the relation in the model. +func (m *Model) updateRelation(ch RelationChange, rm *residentManager) { + m.mu.Lock() + + relation, found := m.relations[ch.Key] + if !found { + relation = newRelation(m, rm.new()) + m.relations[ch.Key] = relation + } + relation.setDetails(ch) + + m.mu.Unlock() +} + +// removeRelation removes the relation from the model. +func (m *Model) removeRelation(ch RemoveRelation) error { + defer m.doLocked()() + + relation, ok := m.relations[ch.Key] + if ok { + if err := relation.evict(); err != nil { + return errors.Trace(err) + } + delete(m.relations, ch.Key) + } + return nil +} + // updateMachine adds or updates the machine in the model. func (m *Model) updateMachine(ch MachineChange, rm *residentManager) { m.mu.Lock() diff --git a/core/cache/model_test.go b/core/cache/model_test.go index 99cf80d0cec..aaf31ce6a4a 100644 --- a/core/cache/model_test.go +++ b/core/cache/model_test.go @@ -15,6 +15,7 @@ import ( "github.com/juju/juju/core/cache" "github.com/juju/juju/core/life" "github.com/juju/juju/core/network" + "github.com/juju/juju/core/permission" "github.com/juju/juju/core/status" "github.com/juju/juju/testing" ) @@ -34,6 +35,7 @@ func (s *ModelSuite) TestReport(c *gc.C) { "charm-count": 0, "machine-count": 0, "unit-count": 0, + "relation-count": 0, "branch-count": 0, }) } @@ -360,10 +362,11 @@ func (s *ModelSuite) TestWaitForUnitCancelClosesChannel(c *gc.C) { } var modelChange = cache.ModelChange{ - ModelUUID: "model-uuid", - Name: "test-model", - Life: life.Alive, - Owner: "model-owner", + ModelUUID: "model-uuid", + Name: "test-model", + Life: life.Alive, + Owner: "model-owner", + IsController: false, Config: map[string]interface{}{ "key": "value", "another": "foo", @@ -372,4 +375,8 @@ var modelChange = cache.ModelChange{ Status: status.StatusInfo{ Status: status.Active, }, + UserPermissions: map[string]permission.Access{ + "model-owner": permission.AdminAccess, + "read-user": permission.ReadAccess, + }, } diff --git a/core/cache/package_test.go b/core/cache/package_test.go index 5c75fced56d..5118979d991 100644 --- a/core/cache/package_test.go +++ b/core/cache/package_test.go @@ -114,6 +114,7 @@ func (*ImportSuite) TestImports(c *gc.C) { "core/life", "core/lxdprofile", "core/network", + "core/permission", "core/settings", "core/status", }) diff --git a/core/cache/relation.go b/core/cache/relation.go new file mode 100644 index 00000000000..b97f93fdcce --- /dev/null +++ b/core/cache/relation.go @@ -0,0 +1,54 @@ +// Copyright 2020 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cache + +// Relation represents a relation in a cached model. +type Relation struct { + // Resident identifies the relation as a type-agnostic cached entity + // and tracks resources that it is responsible for cleaning up. + *Resident + + model *Model + details RelationChange +} + +func newRelation(model *Model, res *Resident) *Relation { + return &Relation{ + Resident: res, + model: model, + } +} + +// Note that these property accessors are not lock-protected. +// They are intended for calling from external packages that have retrieved a +// deep copy from the cache. + +// Key returns the key of this relation. +func (r *Relation) Key() string { + return r.details.Key +} + +// Endpoints returns the endpoints for this relation. +func (r *Relation) Endpoints() []Endpoint { + return r.details.Endpoints +} + +func (r *Relation) setDetails(details RelationChange) { + // If this is the first receipt of details, set the removal message. + if r.removalMessage == nil { + r.removalMessage = RemoveRelation{ + ModelUUID: details.ModelUUID, + Key: details.Key, + } + } + + r.setStale(false) +} + +// copy returns a copy of the unit, ensuring appropriate deep copying. +func (r *Relation) copy() Relation { + cr := *r + cr.details = cr.details.copy() + return cr +} diff --git a/core/cache/relation_test.go b/core/cache/relation_test.go new file mode 100644 index 00000000000..af5ac966e33 --- /dev/null +++ b/core/cache/relation_test.go @@ -0,0 +1,28 @@ +// Copyright 2020 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package cache_test + +import ( + "github.com/juju/juju/core/cache" +) + +// Mostly a placeholder file at this stage. + +var relationChange = cache.RelationChange{ + ModelUUID: "model-uuid", + Key: "provider:ep consumer:ep", + Endpoints: []cache.Endpoint{ + { + Application: "provider", + Name: "ep", + Role: "provider", + Interface: "foo", + }, { + Application: "consumer", + Name: "ep", + Role: "requires", + Interface: "foo", + }, + }, +} diff --git a/core/cache/unit.go b/core/cache/unit.go index 7bf875798ef..947a8e6d196 100644 --- a/core/cache/unit.go +++ b/core/cache/unit.go @@ -169,6 +169,7 @@ func (u *Unit) setDetails(details UnitChange) { u.details = details toPublish := u.copy() if machineChange || u.details.Subordinate { + // TODO thumper: check this, it looks like we are publishing too often. u.model.hub.Publish(modelUnitAdd, toPublish) } // Publish change event for those that may be waiting. diff --git a/core/multiwatcher/types.go b/core/multiwatcher/types.go index e0d987e843f..0856dd66877 100644 --- a/core/multiwatcher/types.go +++ b/core/multiwatcher/types.go @@ -351,19 +351,22 @@ func (i *BlockInfo) EntityID() EntityID { } } -// ModelUpdate holds the information about a model that is +// ModelInfo holds the information about a model that is // tracked by multiwatcherStore. -type ModelUpdate struct { - ModelUUID string - Name string - Life life.Value - Owner string - ControllerUUID string - IsController bool - Config map[string]interface{} - Status StatusInfo - Constraints constraints.Value - SLA ModelSLAInfo +type ModelInfo struct { + ModelUUID string + Name string + Life life.Value + Owner string + ControllerUUID string + IsController bool + Cloud string + CloudRegion string + CloudCredential string + Config map[string]interface{} + Status StatusInfo + Constraints constraints.Value + SLA ModelSLAInfo UserPermissions map[string]permission.Access } @@ -375,7 +378,7 @@ type ModelSLAInfo struct { } // EntityID returns a unique identifier for a model. -func (i *ModelUpdate) EntityID() EntityID { +func (i *ModelInfo) EntityID() EntityID { return EntityID{ Kind: ModelKind, ModelUUID: i.ModelUUID, diff --git a/state/allwatcher.go b/state/allwatcher.go index 70d67046f3e..e42ea2488d2 100644 --- a/state/allwatcher.go +++ b/state/allwatcher.go @@ -141,13 +141,16 @@ func (e *backingModel) updated(ctx *allWatcherContext) error { // Update the context with the model type. ctx.modelType_ = e.Type - info := &multiwatcher.ModelUpdate{ - ModelUUID: e.UUID, - Name: e.Name, - Life: life.Value(e.Life.String()), - Owner: e.Owner, - ControllerUUID: e.ControllerUUID, - IsController: ctx.state.IsController(), + info := &multiwatcher.ModelInfo{ + ModelUUID: e.UUID, + Name: e.Name, + Life: life.Value(e.Life.String()), + Owner: e.Owner, + ControllerUUID: e.ControllerUUID, + IsController: ctx.state.IsController(), + Cloud: e.Cloud, + CloudRegion: e.CloudRegion, + CloudCredential: e.CloudCredential, SLA: multiwatcher.ModelSLAInfo{ Level: e.SLA.Level.String(), Owner: e.SLA.Owner, @@ -200,7 +203,7 @@ func (e *backingModel) updated(ctx *allWatcherContext) error { info.UserPermissions = permissions } else { - oldInfo := oldInfo.(*multiwatcher.ModelUpdate) + oldInfo := oldInfo.(*multiwatcher.ModelInfo) info.Config = oldInfo.Config info.Constraints = oldInfo.Constraints info.Status = oldInfo.Status @@ -247,7 +250,7 @@ func (e *backingPermission) updated(ctx *allWatcherContext) error { return nil } - storeKey := &multiwatcher.ModelUpdate{ + storeKey := &multiwatcher.ModelInfo{ ModelUUID: modelUUID, } @@ -256,7 +259,7 @@ func (e *backingPermission) updated(ctx *allWatcherContext) error { case nil: // The parent info doesn't exist. Ignore the permission until it does. return nil - case *multiwatcher.ModelUpdate: + case *multiwatcher.ModelInfo: // Set the access for the user in the permission map of the model. info.UserPermissions[user] = permission.Access(e.Access) } @@ -274,7 +277,7 @@ func (e *backingPermission) removed(ctx *allWatcherContext) error { return nil } - storeKey := &multiwatcher.ModelUpdate{ + storeKey := &multiwatcher.ModelInfo{ ModelUUID: modelUUID, } @@ -283,7 +286,7 @@ func (e *backingPermission) removed(ctx *allWatcherContext) error { case nil: // The parent info doesn't exist. Nothing to remove from. return nil - case *multiwatcher.ModelUpdate: + case *multiwatcher.ModelInfo: // Remove the user from the permission map. delete(info.UserPermissions, user) } @@ -1036,7 +1039,7 @@ func (s *backingStatus) updated(ctx *allWatcherContext) error { return err } info0 = &newInfo - case *multiwatcher.ModelUpdate: + case *multiwatcher.ModelInfo: newInfo := *info newInfo.Status = s.toStatusInfo() info0 = &newInfo @@ -1164,7 +1167,7 @@ func (c *backingConstraints) updated(ctx *allWatcherContext) error { case *multiwatcher.UnitInfo, *multiwatcher.MachineInfo: // We don't (yet) publish unit or machine constraints. return nil - case *multiwatcher.ModelUpdate: + case *multiwatcher.ModelInfo: newInfo := *info newInfo.Constraints = constraintsDoc(*c).value() info0 = &newInfo @@ -1201,7 +1204,7 @@ func (s *backingSettings) updated(ctx *allWatcherContext) error { case nil: // The parent info doesn't exist. Ignore the status until it does. return nil - case *multiwatcher.ModelUpdate: + case *multiwatcher.ModelInfo: // We need to construct a model config so that coercion // of raw settings values occurs. cfg, err := config.New(config.NoDefaults, s.Settings) @@ -1996,7 +1999,7 @@ func (ctx *allWatcherContext) getOpenedPorts(unit *Unit) ([]corenetwork.PortRang func (ctx *allWatcherContext) entityIDForGlobalKey(key string) (multiwatcher.EntityID, bool) { var result multiwatcher.EntityInfo if key == modelGlobalKey { - result = &multiwatcher.ModelUpdate{ + result = &multiwatcher.ModelInfo{ ModelUUID: ctx.modelUUID, } return result.EntityID(), true diff --git a/state/allwatcher_internal_test.go b/state/allwatcher_internal_test.go index e9da7af510a..06cc329906e 100644 --- a/state/allwatcher_internal_test.go +++ b/state/allwatcher_internal_test.go @@ -80,15 +80,18 @@ func (s *allWatcherBaseSuite) setUpScenario(c *gc.C, st *State, units int) (enti c.Assert(err, jc.ErrorIsNil) modelStatus, err := model.Status() c.Assert(err, jc.ErrorIsNil) - - add(&multiwatcher.ModelUpdate{ - ModelUUID: model.UUID(), - Name: model.Name(), - Life: life.Alive, - Owner: model.Owner().Id(), - ControllerUUID: model.ControllerUUID(), - IsController: model.IsControllerModel(), - Config: modelCfg.AllAttrs(), + credential, _ := model.CloudCredential() + add(&multiwatcher.ModelInfo{ + ModelUUID: model.UUID(), + Name: model.Name(), + Life: life.Alive, + Owner: model.Owner().Id(), + ControllerUUID: model.ControllerUUID(), + IsController: model.IsControllerModel(), + Cloud: model.Cloud(), + CloudRegion: model.CloudRegion(), + CloudCredential: credential.Id(), + Config: modelCfg.AllAttrs(), Status: multiwatcher.StatusInfo{ Current: modelStatus.Status, Message: modelStatus.Message, @@ -1054,7 +1057,7 @@ func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) { func(c *gc.C, st *State) changeTestCase { return changeTestCase{ about: "model is removed if it's not in backing", - initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: "some-uuid", }}, change: watcher.Change{ @@ -1074,6 +1077,8 @@ func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) { cons := constraints.MustParse("mem=4G") err = st.SetModelConstraints(cons) c.Assert(err, jc.ErrorIsNil) + credential, _ := model.CloudCredential() + return changeTestCase{ about: "model is added if it's in backing but not in Store", change: watcher.Change{ @@ -1081,15 +1086,18 @@ func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) { Id: st.ModelUUID(), }, expectContents: []multiwatcher.EntityInfo{ - &multiwatcher.ModelUpdate{ - ModelUUID: model.UUID(), - Name: model.Name(), - Life: life.Alive, - Owner: model.Owner().Id(), - ControllerUUID: model.ControllerUUID(), - IsController: model.IsControllerModel(), - Config: cfg.AllAttrs(), - Constraints: cons, + &multiwatcher.ModelInfo{ + ModelUUID: model.UUID(), + Name: model.Name(), + Life: life.Alive, + Owner: model.Owner().Id(), + ControllerUUID: model.ControllerUUID(), + IsController: model.IsControllerModel(), + Cloud: model.Cloud(), + CloudRegion: model.CloudRegion(), + CloudCredential: credential.Id(), + Config: cfg.AllAttrs(), + Constraints: cons, Status: multiwatcher.StatusInfo{ Current: status.Status, Message: status.Message, @@ -1112,10 +1120,11 @@ func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) { c.Assert(err, jc.ErrorIsNil) status, err := model.Status() c.Assert(err, jc.ErrorIsNil) + credential, _ := model.CloudCredential() return changeTestCase{ about: "model is updated if it's in backing and in Store", initialContents: []multiwatcher.EntityInfo{ - &multiwatcher.ModelUpdate{ + &multiwatcher.ModelInfo{ ModelUUID: model.UUID(), Name: "", Life: life.Alive, @@ -1142,14 +1151,17 @@ func (s *allModelWatcherStateSuite) TestChangeModels(c *gc.C) { Id: model.UUID(), }, expectContents: []multiwatcher.EntityInfo{ - &multiwatcher.ModelUpdate{ - ModelUUID: model.UUID(), - Name: model.Name(), - Life: life.Alive, - Owner: model.Owner().Id(), - ControllerUUID: model.ControllerUUID(), - IsController: model.IsControllerModel(), - Config: cfg.AllAttrs(), + &multiwatcher.ModelInfo{ + ModelUUID: model.UUID(), + Name: model.Name(), + Life: life.Alive, + Owner: model.Owner().Id(), + ControllerUUID: model.ControllerUUID(), + IsController: model.IsControllerModel(), + Cloud: model.Cloud(), + CloudRegion: model.CloudRegion(), + CloudCredential: credential.Id(), + Config: cfg.AllAttrs(), Status: multiwatcher.StatusInfo{ Current: status.Status, Message: status.Message, @@ -1232,7 +1244,7 @@ func (s *allModelWatcherStateSuite) TestModelSettings(c *gc.C) { expectedModelSettings["http-proxy"] = "http://invalid" expectedModelSettings["foo"] = "bar" - all.Update(&multiwatcher.ModelUpdate{ + all.Update(&multiwatcher.ModelInfo{ ModelUUID: s.state.ModelUUID(), Name: "dummy-model", }) @@ -1243,7 +1255,7 @@ func (s *allModelWatcherStateSuite) TestModelSettings(c *gc.C) { c.Assert(err, jc.ErrorIsNil) entities := all.All() assertEntitiesEqual(c, entities, []multiwatcher.EntityInfo{ - &multiwatcher.ModelUpdate{ + &multiwatcher.ModelInfo{ ModelUUID: s.state.ModelUUID(), Name: "dummy-model", Config: expectedModelSettings, @@ -1259,10 +1271,11 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) func(c *gc.C, st *State) changeTestCase { model, err := st.Model() c.Assert(err, jc.ErrorIsNil) + credential, _ := model.CloudCredential() return changeTestCase{ about: "model update keeps permissions", - initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), // Existance doesn't care about the other values, and they are // not entirely relevent to this test. @@ -1275,14 +1288,17 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) C: "models", Id: st.ModelUUID(), }, - expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ - ModelUUID: st.ModelUUID(), - Name: model.Name(), - Life: "alive", - Owner: model.Owner().Id(), - ControllerUUID: testing.ControllerTag.Id(), - IsController: model.IsControllerModel(), - SLA: multiwatcher.ModelSLAInfo{Level: "unsupported"}, + expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ + ModelUUID: st.ModelUUID(), + Name: model.Name(), + Life: "alive", + Owner: model.Owner().Id(), + ControllerUUID: testing.ControllerTag.Id(), + IsController: model.IsControllerModel(), + Cloud: model.Cloud(), + CloudRegion: model.CloudRegion(), + CloudCredential: credential.Id(), + SLA: multiwatcher.ModelSLAInfo{Level: "unsupported"}, UserPermissions: map[string]permission.Access{ "bob": permission.ReadAccess, "mary": permission.AdminAccess, @@ -1303,7 +1319,7 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) return changeTestCase{ about: "adding a model user updates model", - initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), Name: model.Name(), // Existance doesn't care about the other values, and they are @@ -1317,7 +1333,7 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) C: permissionsC, Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("tony@external")), }, - expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), Name: model.Name(), // When the permissions are updated, only the user permissions are changed. @@ -1335,7 +1351,7 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) return changeTestCase{ about: "removing a permission document removes user permission", - initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), Name: model.Name(), // Existance doesn't care about the other values, and they are @@ -1350,7 +1366,7 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("bob")), Revno: -1, }, - expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), Name: model.Name(), // When the permissions are updated, only the user permissions are changed. @@ -1382,7 +1398,7 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) return changeTestCase{ about: "updating a permission document updates user permission", - initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + initialContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), Name: model.Name(), // Existance doesn't care about the other values, and they are @@ -1396,7 +1412,7 @@ func testChangePermissions(c *gc.C, runChangeTests func(*gc.C, []changeTestFunc) C: permissionsC, Id: permissionID(modelKey(st.ModelUUID()), userGlobalKey("bob")), }, - expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelUpdate{ + expectContents: []multiwatcher.EntityInfo{&multiwatcher.ModelInfo{ ModelUUID: st.ModelUUID(), Name: model.Name(), UserPermissions: map[string]permission.Access{ diff --git a/worker/modelcache/worker.go b/worker/modelcache/worker.go index 0928ff6f3d1..8087f28d781 100644 --- a/worker/modelcache/worker.go +++ b/worker/modelcache/worker.go @@ -313,6 +313,8 @@ func (c *cacheWorker) translate(d multiwatcher.Delta) interface{} { return c.translateMachine(d) case multiwatcher.UnitKind: return c.translateUnit(d) + case multiwatcher.RelationKind: + return c.translateRelation(d) case multiwatcher.CharmKind: return c.translateCharm(d) case multiwatcher.BranchKind: @@ -333,20 +335,25 @@ func (c *cacheWorker) translateModel(d multiwatcher.Delta) interface{} { } } - value, ok := e.(*multiwatcher.ModelUpdate) + value, ok := e.(*multiwatcher.ModelInfo) if !ok { c.config.Logger.Errorf("unexpected type %T", e) return nil } return cache.ModelChange{ - ModelUUID: value.ModelUUID, - Name: value.Name, - Life: life.Value(value.Life), - Owner: value.Owner, - Config: value.Config, - Status: coreStatus(value.Status), + ModelUUID: value.ModelUUID, + Name: value.Name, + Life: life.Value(value.Life), + Owner: value.Owner, + IsController: value.IsController, + Cloud: value.Cloud, + CloudRegion: value.CloudRegion, + CloudCredential: value.CloudCredential, + Config: value.Config, + Status: coreStatus(value.Status), // TODO: constraints, sla + UserPermissions: value.UserPermissions, } } @@ -454,6 +461,43 @@ func (c *cacheWorker) translateUnit(d multiwatcher.Delta) interface{} { } } +func (c *cacheWorker) translateRelation(d multiwatcher.Delta) interface{} { + e := d.Entity + id := e.EntityID() + + if d.Removed { + return cache.RemoveRelation{ + ModelUUID: id.ModelUUID, + Key: id.ID, + } + } + + value, ok := e.(*multiwatcher.RelationInfo) + if !ok { + c.config.Logger.Errorf("unexpected type %T", e) + return nil + } + + endpoints := make([]cache.Endpoint, len(value.Endpoints)) + for i, ep := range value.Endpoints { + endpoints[i] = cache.Endpoint{ + Application: ep.ApplicationName, + Name: ep.Relation.Name, + Role: ep.Relation.Role, + Interface: ep.Relation.Interface, + Optional: ep.Relation.Optional, + Limit: ep.Relation.Limit, + Scope: ep.Relation.Scope, + } + } + + return cache.RelationChange{ + ModelUUID: value.ModelUUID, + Key: value.Key, + Endpoints: endpoints, + } +} + func (c *cacheWorker) translateCharm(d multiwatcher.Delta) interface{} { e := d.Entity id := e.EntityID() diff --git a/worker/modelcache/worker_test.go b/worker/modelcache/worker_test.go index 48ec6982556..2e2a8fa724b 100644 --- a/worker/modelcache/worker_test.go +++ b/worker/modelcache/worker_test.go @@ -5,6 +5,7 @@ package modelcache_test import ( "math" + "strings" "time" "github.com/juju/clock" @@ -22,6 +23,7 @@ import ( "github.com/juju/juju/core/cache/cachetest" "github.com/juju/juju/core/life" "github.com/juju/juju/core/multiwatcher" + "github.com/juju/juju/core/permission" "github.com/juju/juju/state" statetesting "github.com/juju/juju/state/testing" "github.com/juju/juju/testing" @@ -176,12 +178,27 @@ func (s *WorkerSuite) checkModel(c *gc.C, obtained interface{}, model *state.Mod c.Check(change.Name, gc.Equals, model.Name()) c.Check(change.Life, gc.Equals, life.Value(model.Life().String())) c.Check(change.Owner, gc.Equals, model.Owner().Name()) + c.Check(change.IsController, gc.Equals, model.IsControllerModel()) + c.Check(change.Cloud, gc.Equals, model.Cloud()) + c.Check(change.CloudRegion, gc.Equals, model.CloudRegion()) + cred, _ := model.CloudCredential() + c.Check(change.CloudCredential, gc.Equals, cred.Id()) + cfg, err := model.Config() c.Assert(err, jc.ErrorIsNil) c.Check(change.Config, jc.DeepEquals, cfg.AllAttrs()) status, err := model.Status() c.Assert(err, jc.ErrorIsNil) c.Check(change.Status, jc.DeepEquals, status) + + users, err := model.Users() + c.Assert(err, jc.ErrorIsNil) + permissions := make(map[string]permission.Access) + for _, user := range users { + // Cache permission map is always lower case. + permissions[strings.ToLower(user.UserName)] = user.Access + } + c.Check(change.UserPermissions, jc.DeepEquals, permissions) } func (s *WorkerSuite) TestInitialModel(c *gc.C) { @@ -592,7 +609,7 @@ func (s *WorkerSuite) TestWatcherErrorCacheMarkSweep(c *gc.C) { if delta.Entity.EntityID().Kind == "model" && !fakeModelSent { fakeModelSent = true return append(deltas, multiwatcher.Delta{ - Entity: &multiwatcher.ModelUpdate{ + Entity: &multiwatcher.ModelInfo{ ModelUUID: "fake-ass-model-uuid", Name: "evict-this-cat", },