From 68da8d038073b9aebef72fe04bb9fa89b940e7d8 Mon Sep 17 00:00:00 2001 From: Ian Booth Date: Tue, 3 Nov 2015 22:11:34 +1000 Subject: [PATCH] Add state model for service directory --- state/allcollections.go | 6 +- state/servicedirectory.go | 243 +++++++++++++++++++++++++++++++++ state/servicedirectory_test.go | 173 +++++++++++++++++++++++ 3 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 state/servicedirectory.go create mode 100644 state/servicedirectory_test.go diff --git a/state/allcollections.go b/state/allcollections.go index d591db8c48ed..68513a855781 100644 --- a/state/allcollections.go +++ b/state/allcollections.go @@ -173,8 +173,9 @@ func allCollections() collectionSchema { // ----- // These collections hold information associated with services. - charmsC: {}, - servicesC: {}, + charmsC: {}, + serviceDirectoryC: {}, + servicesC: {}, unitsC: { indexes: []mgo.Index{{ Key: []string{"env-uuid", "service"}, @@ -367,6 +368,7 @@ const ( restoreInfoC = "restoreInfo" sequenceC = "sequence" servicesC = "services" + serviceDirectoryC = "servicedirectory" settingsC = "settings" settingsrefsC = "settingsrefs" stateServersC = "stateServers" diff --git a/state/servicedirectory.go b/state/servicedirectory.go new file mode 100644 index 000000000000..1b1e4e429c31 --- /dev/null +++ b/state/servicedirectory.go @@ -0,0 +1,243 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state + +import ( + "fmt" + "sort" + + "github.com/juju/errors" + "github.com/juju/names" + jujutxn "github.com/juju/txn" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + "gopkg.in/mgo.v2/txn" +) + +// ServiceDirectoryRecord represents the state of a service hosted +// in an external (remote) environment. +type ServiceDirectoryRecord struct { + st *State + doc serviceDirectoryDoc +} + +// serviceDirectoryDoc represents the internal state of a service directory record in MongoDB. +type serviceDirectoryDoc struct { + DocID string `bson:"_id"` + SourceEnvUUID string `bson:"sourceuuid"` + SourceLabel string `bson:"sourcelabel"` + ServiceName string `bson:"servicename"` + Endpoints []Endpoint `bson:"endpoints"` +} + +func newServiceDirectoryRecord(st *State, doc *serviceDirectoryDoc) *ServiceDirectoryRecord { + record := &ServiceDirectoryRecord{ + st: st, + doc: *doc, + } + return record +} + +// ServiceName returns the service name. +func (s *ServiceDirectoryRecord) ServiceName() string { + return s.doc.ServiceName +} + +// SourceLabel returns the label of the source environment. +func (s *ServiceDirectoryRecord) SourceLabel() string { + return s.doc.SourceLabel +} + +// SourceEnvUUID returns the uuid of the source environment. +func (s *ServiceDirectoryRecord) SourceEnvUUID() string { + return s.doc.SourceEnvUUID +} + +// Destroy deletes the service directory record immediately. +func (s *ServiceDirectoryRecord) Destroy() (err error) { + defer errors.DeferredAnnotatef(&err, "cannot destroy service directory record %q", s) + record := &ServiceDirectoryRecord{st: s.st, doc: s.doc} + buildTxn := func(attempt int) ([]txn.Op, error) { + if attempt > 0 { + if err := record.Refresh(); errors.IsNotFound(err) { + return nil, jujutxn.ErrNoOperations + } else if err != nil { + return nil, err + } + } + return record.destroyOps() + } + return s.st.run(buildTxn) +} + +// destroyOps returns the operations required to destroy the record. +func (s *ServiceDirectoryRecord) destroyOps() ([]txn.Op, error) { + return []txn.Op{ + { + C: serviceDirectoryC, + Id: s.doc.DocID, + Assert: txn.DocExists, + Remove: true, + }, + }, nil +} + +// Endpoints returns the service record's currently available relation endpoints. +func (s *ServiceDirectoryRecord) Endpoints() ([]Endpoint, error) { + eps := make([]Endpoint, len(s.doc.Endpoints)) + for i, ep := range s.doc.Endpoints { + eps[i] = ep + } + sort.Sort(epSlice(eps)) + return eps, nil +} + +// Endpoint returns the relation endpoint with the supplied name, if it exists. +func (s *ServiceDirectoryRecord) Endpoint(relationName string) (Endpoint, error) { + eps, err := s.Endpoints() + if err != nil { + return Endpoint{}, err + } + for _, ep := range eps { + if ep.Name == relationName { + return ep, nil + } + } + return Endpoint{}, fmt.Errorf("service directory record %q has no %q relation", s, relationName) +} + +// String returns the directory record name. +func (s *ServiceDirectoryRecord) String() string { + return fmt.Sprintf("%s-%s", s.doc.SourceEnvUUID, s.doc.ServiceName) +} + +// Refresh refreshes the contents of the ServiceDirectoryRecord from the underlying +// state. It returns an error that satisfies errors.IsNotFound if the +// record has been removed. +func (s *ServiceDirectoryRecord) Refresh() error { + serviceDirectoryCollection, closer := s.st.getCollection(serviceDirectoryC) + defer closer() + + err := serviceDirectoryCollection.FindId(s.doc.DocID).One(&s.doc) + if err == mgo.ErrNotFound { + return errors.NotFoundf("service direcotry record %q", s) + } + if err != nil { + return fmt.Errorf("cannot refresh service directory record %q: %v", s, err) + } + return nil +} + +// AddServiceDirectoryParams defines the parameters used to add a ServiceDirectory record. +type AddServiceDirectoryParams struct { + ServiceName string + Endpoints []Endpoint + SourceEnvUUID string + SourceLabel string +} + +var errDuplicateServiceDirectoryRecord = errors.Errorf("service directory record already exists") + +// AddServiceDirectoryRecord creates a new service directory record, having the supplied parameters, +func (st *State) AddServiceDirectoryRecord(params AddServiceDirectoryParams) (_ *ServiceDirectoryRecord, err error) { + defer errors.DeferredAnnotatef(&err, "cannot add service direcotry record %q", params.ServiceName) + + // Sanity checks. + if params.SourceEnvUUID == "" { + return nil, errors.Errorf("missing source environment UUID") + } + if !names.IsValidService(params.ServiceName) { + return nil, errors.Errorf("invalid service name") + } + env, err := st.Environment() + if err != nil { + return nil, errors.Trace(err) + } else if env.Life() != Alive { + return nil, errors.Errorf("environment is no longer alive") + } + + if _, err := st.ServiceDirectoryRecord(params.ServiceName); err == nil { + return nil, errDuplicateServiceDirectoryRecord + } else if !errors.IsNotFound(err) { + return nil, errors.Trace(err) + } + + docID := st.docID(params.ServiceName) + doc := &serviceDirectoryDoc{ + DocID: docID, + ServiceName: params.ServiceName, + Endpoints: params.Endpoints, + SourceEnvUUID: params.SourceEnvUUID, + SourceLabel: params.SourceLabel, + } + record := newServiceDirectoryRecord(st, doc) + + buildTxn := func(attempt int) ([]txn.Op, error) { + // If we've tried once already and failed, check that + // environment may have been destroyed. + if attempt > 0 { + if err := checkEnvLife(st); err != nil { + return nil, errors.Trace(err) + } + } + ops := []txn.Op{ + env.assertAliveOp(), + { + C: serviceDirectoryC, + Id: docID, + Assert: txn.DocMissing, + Insert: doc, + }, + } + return ops, nil + } + if err = st.run(buildTxn); err == nil { + return record, nil + } + if err != jujutxn.ErrExcessiveContention { + return nil, err + } + // Check the reason for failure - may be because of a name conflict. + if _, err = st.ServiceDirectoryRecord(params.ServiceName); err == nil { + return nil, errDuplicateServiceDirectoryRecord + } else if !errors.IsNotFound(err) { + return nil, errors.Trace(err) + } + return nil, errors.Trace(err) +} + +// ServiceDirectoryRecord returns a service directory record by name. +func (st *State) ServiceDirectoryRecord(serviceName string) (record *ServiceDirectoryRecord, err error) { + serviceDirectoryCollection, closer := st.getCollection(serviceDirectoryC) + defer closer() + + if !names.IsValidService(serviceName) { + return nil, errors.Errorf("%q is not a valid service name", serviceName) + } + doc := &serviceDirectoryDoc{} + err = serviceDirectoryCollection.FindId(serviceName).One(doc) + if err == mgo.ErrNotFound { + return nil, errors.NotFoundf("service directory record %q", serviceName) + } + if err != nil { + return nil, errors.Annotatef(err, "cannot get service directory record %q", serviceName) + } + return newServiceDirectoryRecord(st, doc), nil +} + +// AllServiceDirectoryEntries returns all the service directory entries used by the environment. +func (st *State) AllServiceDirectoryEntries() (records []*ServiceDirectoryRecord, err error) { + serviceDirectoryCollection, closer := st.getCollection(serviceDirectoryC) + defer closer() + + docs := []serviceDirectoryDoc{} + err = serviceDirectoryCollection.Find(bson.D{}).All(&docs) + if err != nil { + return nil, errors.Errorf("cannot get all service directory entries") + } + for _, v := range docs { + records = append(records, newServiceDirectoryRecord(st, &v)) + } + return records, nil +} diff --git a/state/servicedirectory_test.go b/state/servicedirectory_test.go new file mode 100644 index 000000000000..146f7e728e0a --- /dev/null +++ b/state/servicedirectory_test.go @@ -0,0 +1,173 @@ +// Copyright 2015 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package state_test + +import ( + "sort" + + "github.com/juju/errors" + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/juju/charm.v6-unstable" + + "github.com/juju/juju/state" +) + +type serviceDirectorySuite struct { + ConnSuite + record state.ServiceDirectoryRecord +} + +var _ = gc.Suite(&serviceDirectorySuite{}) + +func (s *serviceDirectorySuite) createDirectoryRecord(c *gc.C) *state.ServiceDirectoryRecord { + eps := []state.Endpoint{ + { + ServiceName: "mysql", + Relation: charm.Relation{ + Interface: "mysql", + Name: "db", + Role: charm.RoleProvider, + Scope: charm.ScopeGlobal, + }, + }, + { + ServiceName: "mysql", + Relation: charm.Relation{ + Interface: "mysql-root", + Name: "db-admin", + Role: charm.RoleProvider, + Scope: charm.ScopeGlobal, + }, + }, + } + record, err := s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "mysql", + Endpoints: eps, + SourceEnvUUID: "source-uuid", + SourceLabel: "source", + }) + c.Assert(err, jc.ErrorIsNil) + return record +} + +func (s *serviceDirectorySuite) TestEndpoints(c *gc.C) { + record := s.createDirectoryRecord(c) + _, err := record.Endpoint("foo") + c.Assert(err, gc.ErrorMatches, `service directory record "source-uuid-mysql" has no \"foo\" relation`) + + serverEP, err := record.Endpoint("db") + c.Assert(err, jc.ErrorIsNil) + c.Assert(serverEP, gc.DeepEquals, state.Endpoint{ + ServiceName: "mysql", + Relation: charm.Relation{ + Interface: "mysql", + Name: "db", + Role: charm.RoleProvider, + Scope: charm.ScopeGlobal, + }, + }) + + adminEp := state.Endpoint{ + ServiceName: "mysql", + Relation: charm.Relation{ + Interface: "mysql-root", + Name: "db-admin", + Role: charm.RoleProvider, + Scope: charm.ScopeGlobal, + }, + } + eps, err := record.Endpoints() + c.Assert(err, jc.ErrorIsNil) + c.Assert(eps, gc.DeepEquals, []state.Endpoint{serverEP, adminEp}) +} + +func (s *serviceDirectorySuite) TestDirectoryRecordRefresh(c *gc.C) { + record := s.createDirectoryRecord(c) + s1, err := s.State.ServiceDirectoryRecord(record.ServiceName()) + c.Assert(err, jc.ErrorIsNil) + + err = s1.Destroy() + c.Assert(err, jc.ErrorIsNil) + err = record.Refresh() + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *serviceDirectorySuite) TestDestroy(c *gc.C) { + record := s.createDirectoryRecord(c) + err := record.Destroy() + c.Assert(err, jc.ErrorIsNil) + err = record.Refresh() + c.Assert(err, jc.Satisfies, errors.IsNotFound) +} + +func (s *serviceDirectorySuite) TestAllServiceDirectoryRecordsNone(c *gc.C) { + services, err := s.State.AllServiceDirectoryEntries() + c.Assert(err, jc.ErrorIsNil) + c.Assert(len(services), gc.Equals, 0) +} + +func (s *serviceDirectorySuite) TestAddServiceDirectoryRecords(c *gc.C) { + record := s.createDirectoryRecord(c) + records, err := s.State.AllServiceDirectoryEntries() + c.Assert(err, jc.ErrorIsNil) + c.Assert(len(records), gc.Equals, 1) + c.Assert(records[0], jc.DeepEquals, record) + + _, err = s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "another", + SourceEnvUUID: "uuid", + }) + c.Assert(err, jc.ErrorIsNil) + records, err = s.State.AllServiceDirectoryEntries() + c.Assert(err, jc.ErrorIsNil) + c.Assert(records, gc.HasLen, 2) + + // Check the returned record, order is defined by sorted keys. + names := make([]string, len(records)) + for i, record := range records { + names[i] = record.ServiceName() + } + sort.Strings(names) + c.Assert(names[0], gc.Equals, "another") + c.Assert(names[1], gc.Equals, "mysql") +} + +func (s *serviceDirectorySuite) TestAddServiceDirectoryRecordUUIDRequired(c *gc.C) { + _, err := s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "another", + }) + c.Assert(err, gc.ErrorMatches, `cannot add service direcotry record "another": missing source environment UUID`) +} + +func (s *serviceDirectorySuite) TestAddServiceDirectoryRecordDuplicate(c *gc.C) { + _, err := s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "another", + SourceEnvUUID: "uuid", + }) + c.Assert(err, jc.ErrorIsNil) + _, err = s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "another", + SourceEnvUUID: "another-uuid", + }) + c.Assert(err, gc.ErrorMatches, `cannot add service direcotry record "another": service directory record already exists`) +} + +func (s *remoteServiceSuite) TestAddServiceDirectoryEntryDuplicateAddedAfterInitial(c *gc.C) { + // Check that a record with a name conflict cannot be added if + // there is no conflict initially but a record is added + // before the transaction is run. + defer state.SetBeforeHooks(c, s.State, func() { + _, err := s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "record", + SourceEnvUUID: "uuid", + }) + c.Assert(err, jc.ErrorIsNil) + }).Check() + _, err := s.State.AddServiceDirectoryRecord(state.AddServiceDirectoryParams{ + ServiceName: "record", + SourceEnvUUID: "another-uuid", + }) + c.Assert(err, gc.ErrorMatches, `cannot add service direcotry record "record": service directory record already exists`) +}