From 38f085ad55b7a929ef73433916d69bd3725c278f Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Mon, 19 Jun 2017 16:13:10 -0700 Subject: [PATCH 01/15] Endpoint for managing phoenix channel --- remote/controllers/channels.go | 18 ++++++++++++ remote/controllers/controllers.go | 8 +++++ remote/controllers/router.go | 7 +++++ remote/payloads/channel_payloads.go | 45 +++++++++++++++++++++++++++++ remote/services/queries.go | 4 +++ remote/utils/failures/failures.go | 13 ++++++++- 6 files changed, 94 insertions(+), 1 deletion(-) diff --git a/remote/controllers/channels.go b/remote/controllers/channels.go index f05eec7c10..e9f5cc1730 100644 --- a/remote/controllers/channels.go +++ b/remote/controllers/channels.go @@ -46,3 +46,21 @@ func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerF return responses.NewResponse(http.StatusCreated, resp), nil } } + +// UpdateChannel updates an existing channel. +func (ctrl *Channels) UpdateChannel(id int, payload *payloads.UpdateChannel) ControllerFunc { + return func() (*responses.Response, failures.Failure) { + existingPhxChannel := &phoenix.Channel{} + if err := services.FindChannelByID(ctrl.phxDB, id, existingPhxChannel); err != nil { + return nil, err + } + + phxChannel := payload.PhoenixModel(existingPhxChannel) + if err := services.UpdateChannel(ctrl.phxDB, phxChannel); err != nil { + return nil, err + } + + resp := responses.NewChannel(phxChannel) + return responses.NewResponse(http.StatusOK, resp), nil + } +} diff --git a/remote/controllers/controllers.go b/remote/controllers/controllers.go index 7605e7e2ac..f25ef95521 100644 --- a/remote/controllers/controllers.go +++ b/remote/controllers/controllers.go @@ -36,5 +36,13 @@ func Start() { return fc.Run(channelsCtrl.CreateChannel(&payload)) }) + r.PATCH("/v1/public/channels/:id", func(fc *FoxContext) error { + id := fc.ParamInt("id") + payload := payloads.UpdateChannel{} + fc.BindJSON(&payload) + + return fc.Run(channelsCtrl.UpdateChannel(id, &payload)) + }) + r.Run(config.Port) } diff --git a/remote/controllers/router.go b/remote/controllers/router.go index 957a4cace6..e04911ed26 100644 --- a/remote/controllers/router.go +++ b/remote/controllers/router.go @@ -42,3 +42,10 @@ func (r *Router) POST(uri string, rf RouterFunc) { return rf(fc) }) } + +func (r *Router) PATCH(uri string, rf RouterFunc) { + r.e.PATCH(uri, func(c echo.Context) error { + fc := c.(*FoxContext) + return rf(fc) + }) +} diff --git a/remote/payloads/channel_payloads.go b/remote/payloads/channel_payloads.go index c0e372c698..b1e5f0621f 100644 --- a/remote/payloads/channel_payloads.go +++ b/remote/payloads/channel_payloads.go @@ -41,3 +41,48 @@ func (c CreateChannel) PhoenixModel() *phoenix.Channel { return model } + +// UpdateChannel is the structure of payload needed to update a channel. +type UpdateChannel struct { + Name *string `json:"name"` + PurchaseOnFox *bool `json:"purchaseOnFox"` + CatalogID *int64 `json:"catalogId"` +} + +// Validate ensures that they payload has the correct format. +// For this payload, it's making sure that there's at least one value. +func (c UpdateChannel) Validate() failures.Failure { + if c.Name == nil && c.PurchaseOnFox == nil && c.CatalogID == nil { + return failures.NewEmptyPayloadFailure() + } else if c.Name != nil && (*c.Name) == "" { + return failures.NewFieldEmptyFailure("name") + } + + return nil +} + +func (c UpdateChannel) PhoenixModel(existing *phoenix.Channel) *phoenix.Channel { + newPhxChannel := phoenix.Channel{ + ID: existing.ID, + CreatedAt: existing.CreatedAt, + UpdatedAt: time.Now().UTC(), + } + + if c.Name != nil { + newPhxChannel.Name = *(c.Name) + } else { + newPhxChannel.Name = existing.Name + } + + if c.PurchaseOnFox != nil { + if *(c.PurchaseOnFox) { + newPhxChannel.PurchaseLocation = phoenix.PurchaseOnFox + } else { + newPhxChannel.PurchaseLocation = phoenix.PurchaseOffFox + } + } else { + newPhxChannel.PurchaseLocation = existing.PurchaseLocation + } + + return &newPhxChannel +} diff --git a/remote/services/queries.go b/remote/services/queries.go index 4e9ee1bde4..b8b49087b7 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -18,3 +18,7 @@ func FindChannelByID(phxDB *gorm.DB, id int, phxChannel *phoenix.Channel) failur func InsertChannel(phxDB *gorm.DB, phxChannel *phoenix.Channel) failures.Failure { return failures.New(phxDB.Create(phxChannel).Error, nil) } + +func UpdateChannel(phxDB *gorm.DB, phxChannel *phoenix.Channel) failures.Failure { + return failures.New(phxDB.Save(phxChannel).Error, nil) +} diff --git a/remote/utils/failures/failures.go b/remote/utils/failures/failures.go index 6cd48c9667..58101cabaf 100644 --- a/remote/utils/failures/failures.go +++ b/remote/utils/failures/failures.go @@ -1,6 +1,9 @@ package failures -import "fmt" +import ( + "errors" + "fmt" +) func NewParamNotFound(paramName string) Failure { return &generalFailure{ @@ -26,6 +29,14 @@ func NewBindFailure(err error) Failure { } } +func NewEmptyPayloadFailure() Failure { + return &generalFailure{ + err: errors.New("payload must have contents"), + failureType: FailureBadRequest, + stack: newCallStack(), + } +} + func NewFieldEmptyFailure(paramName string) Failure { return &generalFailure{ err: fmt.Errorf("%s must be non-empty", paramName), From fb951a3a1f5970a2df7655fb6d44bea850acc60c Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Mon, 19 Jun 2017 18:06:32 -0700 Subject: [PATCH 02/15] Creating a channel updates River Rock --- remote/controllers/channels.go | 8 +-- remote/controllers/controllers.go | 8 ++- remote/models/ic/channel.go | 23 ++++++++ remote/models/ic/host_map.go | 10 ++++ remote/models/phoenix/channel.go | 27 ++-------- remote/models/phoenix/organization.go | 14 +++++ remote/payloads/channel_payloads.go | 21 ++++++-- remote/services/connection.go | 17 ++++++ remote/services/queries.go | 75 ++++++++++++++++++++++++--- remote/utils/config.go | 18 +++++++ remote/utils/failures/failure.go | 24 +++------ remote/utils/failures/failures.go | 16 ++++++ 12 files changed, 205 insertions(+), 56 deletions(-) create mode 100644 remote/models/ic/channel.go create mode 100644 remote/models/ic/host_map.go create mode 100644 remote/models/phoenix/organization.go diff --git a/remote/controllers/channels.go b/remote/controllers/channels.go index e9f5cc1730..094141f460 100644 --- a/remote/controllers/channels.go +++ b/remote/controllers/channels.go @@ -12,11 +12,12 @@ import ( ) type Channels struct { + icDB *gorm.DB phxDB *gorm.DB } -func NewChannels(phxDB *gorm.DB) *Channels { - return &Channels{phxDB: phxDB} +func NewChannels(icDB, phxDB *gorm.DB) *Channels { + return &Channels{icDB: icDB, phxDB: phxDB} } // GetChannel finds a single channel by its ID. @@ -36,9 +37,10 @@ func (ctrl *Channels) GetChannel(id int) ControllerFunc { // CreateChannel creates a new channel. func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerFunc { return func() (*responses.Response, failures.Failure) { + icChannel := payload.IntelligenceModel() phxChannel := payload.PhoenixModel() - if err := services.InsertChannel(ctrl.phxDB, phxChannel); err != nil { + if err := services.InsertChannel(ctrl.icDB, ctrl.phxDB, icChannel, phxChannel, payload.Hosts); err != nil { return nil, err } diff --git a/remote/controllers/controllers.go b/remote/controllers/controllers.go index f25ef95521..8221b6556b 100644 --- a/remote/controllers/controllers.go +++ b/remote/controllers/controllers.go @@ -15,13 +15,19 @@ func Start() { log.Fatal(err) } + icDB, err := services.NewIntelligenceConnection(config) + if err != nil { + log.Fatal(err) + } + phxDB, err := services.NewPhoenixConnection(config) if err != nil { log.Fatal(err) } + defer icDB.Close() defer phxDB.Close() - channelsCtrl := NewChannels(phxDB) + channelsCtrl := NewChannels(icDB, phxDB) r := NewRouter() diff --git a/remote/models/ic/channel.go b/remote/models/ic/channel.go new file mode 100644 index 0000000000..abdd818770 --- /dev/null +++ b/remote/models/ic/channel.go @@ -0,0 +1,23 @@ +package ic + +// Channel is the representation of what a channel looks like in River Rock. +type Channel struct { + ID int + OrganizationID int +} + +func (c Channel) HostMaps(hosts []string, scope string) []*HostMap { + hostMaps := make([]*HostMap, len(hosts)) + + for idx, host := range hosts { + hostMap := &HostMap{ + Host: host, + ChannelID: c.ID, + Scope: scope, + } + + hostMaps[idx] = hostMap + } + + return hostMaps +} diff --git a/remote/models/ic/host_map.go b/remote/models/ic/host_map.go new file mode 100644 index 0000000000..67dd13872f --- /dev/null +++ b/remote/models/ic/host_map.go @@ -0,0 +1,10 @@ +package ic + +// HostMap stores the relationship been site hostnames and channels. +// Used by River Rock to determine which channel to use for a given request. +type HostMap struct { + ID int + Host string + ChannelID int + Scope string +} diff --git a/remote/models/phoenix/channel.go b/remote/models/phoenix/channel.go index ea90c0bd75..44fa99c86b 100644 --- a/remote/models/phoenix/channel.go +++ b/remote/models/phoenix/channel.go @@ -10,15 +10,17 @@ import ( // be a website (theperfectgourmet.com), third-party (Amazon), or sale type (B2B). type Channel struct { ID int + Scope string Name string PurchaseLocation int CreatedAt time.Time UpdatedAt time.Time } -func NewChannel(name string, purchaseLocation int) *Channel { +func NewChannel(scope string, name string, purchaseLocation int) *Channel { return &Channel{ ID: 0, + Scope: scope, Name: name, PurchaseLocation: purchaseLocation, CreatedAt: time.Now().UTC(), @@ -26,29 +28,6 @@ func NewChannel(name string, purchaseLocation int) *Channel { } } -func (c Channel) Table() string { - return "channels" -} - -func (c Channel) Fields() map[string]interface{} { - return map[string]interface{}{ - "name": c.Name, - "purchase_location": c.PurchaseLocation, - "created_at": c.CreatedAt, - "updated_at": c.UpdatedAt, - } -} - -func (c Channel) FieldRefs() []interface{} { - return []interface{}{ - &c.ID, - &c.Name, - &c.PurchaseLocation, - &c.CreatedAt, - &c.UpdatedAt, - } -} - func (c Channel) Validate() error { if c.Name == "" { return errors.New("Channel name must not be empty") diff --git a/remote/models/phoenix/organization.go b/remote/models/phoenix/organization.go new file mode 100644 index 0000000000..0b438998d2 --- /dev/null +++ b/remote/models/phoenix/organization.go @@ -0,0 +1,14 @@ +package phoenix + +import "time" + +type Organization struct { + ID int + Name string + Kind string + ParentID int + ScopeID int + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time +} diff --git a/remote/payloads/channel_payloads.go b/remote/payloads/channel_payloads.go index b1e5f0621f..912691ebe9 100644 --- a/remote/payloads/channel_payloads.go +++ b/remote/payloads/channel_payloads.go @@ -3,15 +3,19 @@ package payloads import ( "time" + "github.com/FoxComm/highlander/remote/models/ic" "github.com/FoxComm/highlander/remote/models/phoenix" "github.com/FoxComm/highlander/remote/utils/failures" ) // CreateChannel is the structure of payload needed to create a channel. type CreateChannel struct { - Name string `json:"name"` - PurchaseOnFox *bool `json:"purchaseOnFox"` - CatalogID *int64 `json:"catalogId"` + Name string `json:"name"` + Scope string `json:"scope"` + OrganizationID int `json:"organizationId"` + Hosts []string `json:"hosts"` + PurchaseOnFox *bool `json:"purchaseOnFox"` + CatalogID *int64 `json:"catalogId"` } // Validate ensures that the has the correct format. @@ -20,11 +24,22 @@ func (c CreateChannel) Validate() failures.Failure { return failures.NewFieldEmptyFailure("name") } else if c.PurchaseOnFox == nil { return failures.NewFieldEmptyFailure("purchaseOnFox") + } else if c.Scope == "" { + return failures.NewFieldEmptyFailure("scope") + } else if c.OrganizationID < 1 { + return failures.NewFieldGreaterThanZero("organizationId", c.OrganizationID) } return nil } +// IntelligenceModel returns the IC model for this payload. +func (c CreateChannel) IntelligenceModel() *ic.Channel { + return &ic.Channel{ + OrganizationID: c.OrganizationID, + } +} + // PhoenixModel returns the phoenix model for this payload. func (c CreateChannel) PhoenixModel() *phoenix.Channel { model := &phoenix.Channel{ diff --git a/remote/services/connection.go b/remote/services/connection.go index cb9dd0b4d3..6842b6266c 100644 --- a/remote/services/connection.go +++ b/remote/services/connection.go @@ -8,6 +8,23 @@ import ( _ "github.com/lib/pq" ) +func NewIntelligenceConnection(config *utils.Config) (*gorm.DB, error) { + connStr := fmt.Sprintf( + "host=%s user=%s password=%s dbname=%s sslmode=%s", + config.ICDatabaseHost, + config.ICDatabaseUser, + config.ICDatabasePassword, + config.ICDatabaseName, + config.ICDatabaseSSL) + + icDB, err := gorm.Open("postgres", connStr) + if err != nil { + return nil, fmt.Errorf("Unable to connect to IC DB with error %s", err.Error()) + } + + return icDB, nil +} + func NewPhoenixConnection(config *utils.Config) (*gorm.DB, error) { connStr := fmt.Sprintf( "host=%s user=%s password=%s dbname=%s sslmode=%s", diff --git a/remote/services/queries.go b/remote/services/queries.go index b8b49087b7..801e637841 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -1,24 +1,83 @@ package services import ( + "fmt" + + "github.com/FoxComm/highlander/remote/models/ic" "github.com/FoxComm/highlander/remote/models/phoenix" "github.com/FoxComm/highlander/remote/utils/failures" "github.com/jinzhu/gorm" ) func FindChannelByID(phxDB *gorm.DB, id int, phxChannel *phoenix.Channel) failures.Failure { - params := map[string]interface{}{ - "model": "channel", - "id": id, + return findByID(phxDB, "channel", id, phxChannel) +} + +func InsertChannel(icDB *gorm.DB, phxDB *gorm.DB, icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) failures.Failure { + var phxOrganization phoenix.Organization + + phxTxn := phxDB.Begin() + icTxn := icDB.Begin() + + if fail := internalFindByID(phxTxn, "organization", icChannel.OrganizationID, &phxOrganization); fail != nil { + phxTxn.Rollback() + icTxn.Rollback() + return fail } - return failures.New(phxDB.First(phxChannel, id).Error, params) -} + if fail := failures.New(phxTxn.Create(phxChannel).Error); fail != nil { + phxTxn.Rollback() + icTxn.Rollback() + return fail + } + + if fail := failures.New(icTxn.Create(icChannel).Error); fail != nil { + phxTxn.Rollback() + icTxn.Rollback() + return fail + } -func InsertChannel(phxDB *gorm.DB, phxChannel *phoenix.Channel) failures.Failure { - return failures.New(phxDB.Create(phxChannel).Error, nil) + hostMaps := icChannel.HostMaps(hosts, "1") + // We have to iterate through each insert manually because of a limitation in + // Gorm. Since there will rarely be many hosts created at a time, this should + // be a workable solution for now. + for idx := range hostMaps { + if fail := failures.New(icTxn.Table("host_map").Create(hostMaps[idx]).Error); fail != nil { + phxTxn.Rollback() + icTxn.Rollback() + return fail + } + } + + if fail := failures.New(icTxn.Commit().Error); fail != nil { + phxTxn.Rollback() + return fail + } + + return failures.New(phxTxn.Commit().Error) } func UpdateChannel(phxDB *gorm.DB, phxChannel *phoenix.Channel) failures.Failure { - return failures.New(phxDB.Save(phxChannel).Error, nil) + return failures.New(phxDB.Save(phxChannel).Error) +} + +func findByID(phxDB *gorm.DB, tableName string, id int, model interface{}) failures.Failure { + res := phxDB.First(model, id) + + if res.RecordNotFound() { + return failures.NewModelNotFoundFailure(tableName, id) + } + + return failures.New(res.Error) +} + +func internalFindByID(db *gorm.DB, table string, id, model interface{}) failures.Failure { + res := db.First(model, id) + + if res.RecordNotFound() { + err := fmt.Errorf("%s with id %d was not found", table, id) + return failures.NewGeneralFailure(err, failures.FailureBadRequest) + } + + return failures.New(res.Error) } diff --git a/remote/utils/config.go b/remote/utils/config.go index 5ab5627f98..f62595fa53 100644 --- a/remote/utils/config.go +++ b/remote/utils/config.go @@ -9,6 +9,12 @@ import ( const ( errEnvVarNotFound = "%s not found in the environment" + icDatabaseName = "IC_DATABASE_NAME" + icDatabaseHost = "IC_DATABASE_HOST" + icDatabaseUser = "IC_DATABASE_USER" + icDatabasePassword = "IC_DATABASE_PASSWORD" + icDatabaseSSL = "IC_DATABASE_SSL" + phoenixDatabaseName = "PHX_DATABASE_NAME" phoenixDatabaseHost = "PHX_DATABASE_HOST" phoenixDatabaseUser = "PHX_DATABASE_USER" @@ -19,6 +25,12 @@ const ( ) type Config struct { + ICDatabaseName string + ICDatabaseHost string + ICDatabaseUser string + ICDatabasePassword string + ICDatabaseSSL string + PhxDatabaseName string PhxDatabaseHost string PhxDatabaseUser string @@ -32,6 +44,12 @@ func NewConfig() (*Config, error) { config := &Config{} var err error + config.ICDatabaseName, err = parseEnvVar(icDatabaseName, nil) + config.ICDatabaseHost, err = parseEnvVar(icDatabaseHost, err) + config.ICDatabaseUser, err = parseEnvVar(icDatabaseUser, err) + config.ICDatabaseSSL, err = parseEnvVar(icDatabaseSSL, err) + config.ICDatabasePassword = os.Getenv(icDatabasePassword) + config.PhxDatabaseName, err = parseEnvVar(phoenixDatabaseName, nil) config.PhxDatabaseHost, err = parseEnvVar(phoenixDatabaseHost, err) config.PhxDatabaseUser, err = parseEnvVar(phoenixDatabaseUser, err) diff --git a/remote/utils/failures/failure.go b/remote/utils/failures/failure.go index f170a828b0..fd9fd78bd3 100644 --- a/remote/utils/failures/failure.go +++ b/remote/utils/failures/failure.go @@ -10,7 +10,7 @@ const ( FailureNotFound FailureServiceError - recordNotFound = "record not found" + RecordNotFound = "record not found" ) // Failure is a wrapper around the standard Golang error type that gives us more @@ -27,34 +27,24 @@ type Failure interface { } // New creates a new Failure. It determines the best failure based on the error -// and arguments passed in. If no error occurred, the response will be nil. -func New(err error, params map[string]interface{}) Failure { +// passed in. If no error occurred, the response will be nil. +func New(err error) Failure { if err == nil { return nil } stack := newCallStack() - if err.Error() == recordNotFound { - return newNotFoundFailure(stack, err, params) + if err.Error() == RecordNotFound { + return newNotFoundFailure(stack, err) } return newServiceFailure(stack, err) } -func newNotFoundFailure(stack *callStack, originalErr error, params map[string]interface{}) Failure { - model, modelOk := params["model"] - id, idOk := params["id"] - - var notFoundErr error - if !modelOk || !idOk { - notFoundErr = originalErr - } else { - notFoundErr = fmt.Errorf("%s with id %d was not found", model, id) - } - +func newNotFoundFailure(stack *callStack, originalErr error) Failure { return &generalFailure{ - err: notFoundErr, + err: originalErr, failureType: FailureNotFound, stack: stack, } diff --git a/remote/utils/failures/failures.go b/remote/utils/failures/failures.go index 58101cabaf..7aa687f87a 100644 --- a/remote/utils/failures/failures.go +++ b/remote/utils/failures/failures.go @@ -44,3 +44,19 @@ func NewFieldEmptyFailure(paramName string) Failure { stack: newCallStack(), } } + +func NewFieldGreaterThanZero(paramName string, value int) Failure { + return &generalFailure{ + err: fmt.Errorf("Expected %s to be greater than 0, got %d", paramName, value), + failureType: FailureBadRequest, + stack: newCallStack(), + } +} + +func NewModelNotFoundFailure(modelName string, id int) Failure { + return &generalFailure{ + err: fmt.Errorf("%s with id %d was not found", modelName, id), + failureType: FailureNotFound, + stack: newCallStack(), + } +} From 01ba9ae64b00bfb2b4338769b68f8180fbf63aa9 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Mon, 19 Jun 2017 18:13:03 -0700 Subject: [PATCH 03/15] Clean up working with the DB a bit --- remote/services/queries.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/remote/services/queries.go b/remote/services/queries.go index 801e637841..f7981d695e 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -25,13 +25,13 @@ func InsertChannel(icDB *gorm.DB, phxDB *gorm.DB, icChannel *ic.Channel, phxChan return fail } - if fail := failures.New(phxTxn.Create(phxChannel).Error); fail != nil { + if fail := create(phxTxn, phxChannel); fail != nil { phxTxn.Rollback() icTxn.Rollback() return fail } - if fail := failures.New(icTxn.Create(icChannel).Error); fail != nil { + if fail := create(icTxn, icChannel); fail != nil { phxTxn.Rollback() icTxn.Rollback() return fail @@ -49,18 +49,26 @@ func InsertChannel(icDB *gorm.DB, phxDB *gorm.DB, icChannel *ic.Channel, phxChan } } - if fail := failures.New(icTxn.Commit().Error); fail != nil { + if fail := commit(icTxn); fail != nil { phxTxn.Rollback() return fail } - return failures.New(phxTxn.Commit().Error) + return commit(phxTxn) } func UpdateChannel(phxDB *gorm.DB, phxChannel *phoenix.Channel) failures.Failure { return failures.New(phxDB.Save(phxChannel).Error) } +//////////////////////////////////////////////////////////// +// DB Convenience Methods +//////////////////////////////////////////////////////////// + +func create(db *gorm.DB, model interface{}) failures.Failure { + return failures.New(db.Create(model).Error) +} + func findByID(phxDB *gorm.DB, tableName string, id int, model interface{}) failures.Failure { res := phxDB.First(model, id) @@ -81,3 +89,7 @@ func internalFindByID(db *gorm.DB, table string, id, model interface{}) failures return failures.New(res.Error) } + +func commit(db *gorm.DB) failures.Failure { + return failures.New(db.Commit().Error) +} From 2e91669c17e895a12d3ada68a67368ad28f209fa Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Mon, 19 Jun 2017 18:45:44 -0700 Subject: [PATCH 04/15] Wrapped the DB usage to make it easier to work with --- remote/controllers/channels.go | 16 ++--- remote/controllers/controllers.go | 11 +--- remote/services/db.go | 103 ++++++++++++++++++++++++++++++ remote/services/queries.go | 76 +++++----------------- 4 files changed, 127 insertions(+), 79 deletions(-) create mode 100644 remote/services/db.go diff --git a/remote/controllers/channels.go b/remote/controllers/channels.go index 094141f460..4416b4bc2e 100644 --- a/remote/controllers/channels.go +++ b/remote/controllers/channels.go @@ -8,16 +8,14 @@ import ( "github.com/FoxComm/highlander/remote/responses" "github.com/FoxComm/highlander/remote/services" "github.com/FoxComm/highlander/remote/utils/failures" - "github.com/jinzhu/gorm" ) type Channels struct { - icDB *gorm.DB - phxDB *gorm.DB + dbs *services.RemoteDBs } -func NewChannels(icDB, phxDB *gorm.DB) *Channels { - return &Channels{icDB: icDB, phxDB: phxDB} +func NewChannels(dbs *services.RemoteDBs) *Channels { + return &Channels{dbs: dbs} } // GetChannel finds a single channel by its ID. @@ -25,7 +23,7 @@ func (ctrl *Channels) GetChannel(id int) ControllerFunc { return func() (*responses.Response, failures.Failure) { channel := &phoenix.Channel{} - if err := services.FindChannelByID(ctrl.phxDB, id, channel); err != nil { + if err := services.FindChannelByID(ctrl.dbs, id, channel); err != nil { return nil, err } @@ -40,7 +38,7 @@ func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerF icChannel := payload.IntelligenceModel() phxChannel := payload.PhoenixModel() - if err := services.InsertChannel(ctrl.icDB, ctrl.phxDB, icChannel, phxChannel, payload.Hosts); err != nil { + if err := services.InsertChannel(ctrl.dbs, icChannel, phxChannel, payload.Hosts); err != nil { return nil, err } @@ -53,12 +51,12 @@ func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerF func (ctrl *Channels) UpdateChannel(id int, payload *payloads.UpdateChannel) ControllerFunc { return func() (*responses.Response, failures.Failure) { existingPhxChannel := &phoenix.Channel{} - if err := services.FindChannelByID(ctrl.phxDB, id, existingPhxChannel); err != nil { + if err := services.FindChannelByID(ctrl.dbs, id, existingPhxChannel); err != nil { return nil, err } phxChannel := payload.PhoenixModel(existingPhxChannel) - if err := services.UpdateChannel(ctrl.phxDB, phxChannel); err != nil { + if err := services.UpdateChannel(ctrl.dbs, phxChannel); err != nil { return nil, err } diff --git a/remote/controllers/controllers.go b/remote/controllers/controllers.go index 8221b6556b..26b7796ad4 100644 --- a/remote/controllers/controllers.go +++ b/remote/controllers/controllers.go @@ -15,19 +15,12 @@ func Start() { log.Fatal(err) } - icDB, err := services.NewIntelligenceConnection(config) + dbs, err := services.NewRemoteDBs(config) if err != nil { log.Fatal(err) } - phxDB, err := services.NewPhoenixConnection(config) - if err != nil { - log.Fatal(err) - } - - defer icDB.Close() - defer phxDB.Close() - channelsCtrl := NewChannels(icDB, phxDB) + channelsCtrl := NewChannels(dbs) r := NewRouter() diff --git a/remote/services/db.go b/remote/services/db.go new file mode 100644 index 0000000000..618517a039 --- /dev/null +++ b/remote/services/db.go @@ -0,0 +1,103 @@ +package services + +import ( + "fmt" + + "github.com/FoxComm/highlander/remote/utils" + "github.com/FoxComm/highlander/remote/utils/failures" + "github.com/jinzhu/gorm" +) + +type RemoteDBs struct { + icDB *RemoteDB + phxDB *RemoteDB +} + +func NewRemoteDBs(config *utils.Config) (*RemoteDBs, error) { + icDB, err := NewIntelligenceConnection(config) + if err != nil { + return nil, err + } + + phxDB, err := NewPhoenixConnection(config) + if err != nil { + return nil, err + } + + return &RemoteDBs{ + icDB: &RemoteDB{icDB}, + phxDB: &RemoteDB{phxDB}, + }, nil +} + +func (r RemoteDBs) Begin() *RemoteDBs { + return &RemoteDBs{ + icDB: r.icDB.Begin(), + phxDB: r.phxDB.Begin(), + } +} + +func (r RemoteDBs) Commit() failures.Failure { + if fail := r.icDB.Commit(); fail != nil { + r.phxDB.Rollback() + return fail + } + + return r.phxDB.Commit() +} + +func (r RemoteDBs) Rollback() { + r.icDB.Rollback() + r.phxDB.Rollback() +} + +func (r RemoteDBs) IC() *RemoteDB { + return r.icDB +} + +func (r RemoteDBs) Phx() *RemoteDB { + return r.phxDB +} + +type RemoteDB struct { + db *gorm.DB +} + +func (r RemoteDB) Begin() *RemoteDB { + return &RemoteDB{db: r.db.Begin()} +} + +func (r RemoteDB) Commit() failures.Failure { + return failures.New(r.db.Commit().Error) +} + +func (r RemoteDB) Rollback() { + r.db.Rollback() +} + +func (r RemoteDB) Create(model interface{}) failures.Failure { + return failures.New(r.db.Create(model).Error) +} + +func (r RemoteDB) CreateWithTable(model interface{}, table string) failures.Failure { + return failures.New(r.db.Table(table).Create(model).Error) +} + +func (r RemoteDB) FindByID(table string, id int, model interface{}) failures.Failure { + return r.FindByIDWithFailure(table, id, model, failures.FailureNotFound) +} + +func (r RemoteDB) FindByIDWithFailure(table string, id int, model interface{}, failure int) failures.Failure { + res := r.db.First(model, id) + + if res.RecordNotFound() { + err := fmt.Errorf("%s with id %d was not found", table, id) + return failures.NewGeneralFailure(err, failure) + } + + return failures.New(res.Error) +} + +func (r RemoteDB) Save(model interface{}) failures.Failure { + return failures.New(r.db.Save(model).Error) +} diff --git a/remote/services/queries.go b/remote/services/queries.go index f7981d695e..a383911cfe 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -1,39 +1,32 @@ package services import ( - "fmt" - "github.com/FoxComm/highlander/remote/models/ic" "github.com/FoxComm/highlander/remote/models/phoenix" "github.com/FoxComm/highlander/remote/utils/failures" - "github.com/jinzhu/gorm" ) -func FindChannelByID(phxDB *gorm.DB, id int, phxChannel *phoenix.Channel) failures.Failure { - return findByID(phxDB, "channel", id, phxChannel) +func FindChannelByID(dbs *RemoteDBs, id int, phxChannel *phoenix.Channel) failures.Failure { + return dbs.Phx().FindByID("channel", id, phxChannel) } -func InsertChannel(icDB *gorm.DB, phxDB *gorm.DB, icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) failures.Failure { +func InsertChannel(dbs *RemoteDBs, icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) failures.Failure { var phxOrganization phoenix.Organization - phxTxn := phxDB.Begin() - icTxn := icDB.Begin() + txn := dbs.Begin() - if fail := internalFindByID(phxTxn, "organization", icChannel.OrganizationID, &phxOrganization); fail != nil { - phxTxn.Rollback() - icTxn.Rollback() + if fail := txn.Phx().FindByIDWithFailure("organization", icChannel.OrganizationID, &phxOrganization, failures.FailureBadRequest); fail != nil { + txn.Rollback() return fail } - if fail := create(phxTxn, phxChannel); fail != nil { - phxTxn.Rollback() - icTxn.Rollback() + if fail := txn.Phx().Create(phxChannel); fail != nil { + txn.Rollback() return fail } - if fail := create(icTxn, icChannel); fail != nil { - phxTxn.Rollback() - icTxn.Rollback() + if fail := txn.IC().Create(icChannel); fail != nil { + txn.Rollback() return fail } @@ -42,54 +35,15 @@ func InsertChannel(icDB *gorm.DB, phxDB *gorm.DB, icChannel *ic.Channel, phxChan // Gorm. Since there will rarely be many hosts created at a time, this should // be a workable solution for now. for idx := range hostMaps { - if fail := failures.New(icTxn.Table("host_map").Create(hostMaps[idx]).Error); fail != nil { - phxTxn.Rollback() - icTxn.Rollback() + if fail := txn.IC().CreateWithTable(hostMaps[idx], "host_map"); fail != nil { + txn.Rollback() return fail } } - if fail := commit(icTxn); fail != nil { - phxTxn.Rollback() - return fail - } - - return commit(phxTxn) -} - -func UpdateChannel(phxDB *gorm.DB, phxChannel *phoenix.Channel) failures.Failure { - return failures.New(phxDB.Save(phxChannel).Error) -} - -//////////////////////////////////////////////////////////// -// DB Convenience Methods -//////////////////////////////////////////////////////////// - -func create(db *gorm.DB, model interface{}) failures.Failure { - return failures.New(db.Create(model).Error) -} - -func findByID(phxDB *gorm.DB, tableName string, id int, model interface{}) failures.Failure { - res := phxDB.First(model, id) - - if res.RecordNotFound() { - return failures.NewModelNotFoundFailure(tableName, id) - } - - return failures.New(res.Error) -} - -func internalFindByID(db *gorm.DB, table string, id, model interface{}) failures.Failure { - res := db.First(model, id) - - if res.RecordNotFound() { - err := fmt.Errorf("%s with id %d was not found", table, id) - return failures.NewGeneralFailure(err, failures.FailureBadRequest) - } - - return failures.New(res.Error) + return txn.Commit() } -func commit(db *gorm.DB) failures.Failure { - return failures.New(db.Commit().Error) +func UpdateChannel(dbs *RemoteDBs, phxChannel *phoenix.Channel) failures.Failure { + return dbs.Phx().Save(phxChannel) } From 37e699c7ccb5e721dfd8c6df172a5fc5ebb3455f Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Mon, 19 Jun 2017 20:19:33 -0700 Subject: [PATCH 05/15] SQL for channels in the Phoenix DB --- .../sql/V5.20170619200953__create_channels_table.sql | 8 ++++++++ .../V5.20170619201616__create_channels_search_view.sql | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 phoenix-scala/sql/V5.20170619200953__create_channels_table.sql create mode 100644 phoenix-scala/sql/V5.20170619201616__create_channels_search_view.sql diff --git a/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql b/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql new file mode 100644 index 0000000000..af40753fe1 --- /dev/null +++ b/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql @@ -0,0 +1,8 @@ +create table channels ( + id serial primary key, + scope exts.ltree not null, + name generic_string not null, + purchase_location integer not null, + created_at generic_timestamp not null, + updated_at generic_timestamp not null +); diff --git a/phoenix-scala/sql/V5.20170619201616__create_channels_search_view.sql b/phoenix-scala/sql/V5.20170619201616__create_channels_search_view.sql new file mode 100644 index 0000000000..68a70f5f3c --- /dev/null +++ b/phoenix-scala/sql/V5.20170619201616__create_channels_search_view.sql @@ -0,0 +1,10 @@ +create table channels_search_view ( + id bigint primary key, + scope exts.ltree not null, + name generic_string not null, + hosts jsonb default '[]', + organization_name generic_string not null, + purchase_location generic_string not null, + created_at json_timestamp, + updated_at json_timestamp +); From e78589524767fe3bb85b1b1506098dccd5ed1a53 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Mon, 19 Jun 2017 20:48:01 -0700 Subject: [PATCH 06/15] WIP handling scope --- remote/controllers/fox_context.go | 18 ++++++++++++++++++ remote/glide.lock | 11 ++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/remote/controllers/fox_context.go b/remote/controllers/fox_context.go index a56cbf5749..14a8fe102b 100644 --- a/remote/controllers/fox_context.go +++ b/remote/controllers/fox_context.go @@ -17,6 +17,7 @@ import ( type FoxContext struct { echo.Context failure failures.Failure + // scope string } // NewFoxContext creates a new FoxContext from an existing echo.Context. @@ -24,6 +25,23 @@ func NewFoxContext(c echo.Context) *FoxContext { return &FoxContext{c, nil} } +func getJWT(c echo.Context) (string, failures.Failure) { + // Try to get from the header first. + req := c.Request() + jwt, ok := req.Header["Jwt"] + if ok && len(jwt) > 0 { + return jwt[0], nil + } + + // Try a cookie. + cookie, err := req.Cookie("JWT") + if err != nil { + return "", failures.New(err) + } + + return cookie.Value, nil +} + // BindJSON grabs the JSON payload and unmarshals it into the interface provided. func (fc *FoxContext) BindJSON(payload payloads.Payload) { if fc.failure != nil { diff --git a/remote/glide.lock b/remote/glide.lock index c7ea5081ce..cba53557e9 100644 --- a/remote/glide.lock +++ b/remote/glide.lock @@ -1,12 +1,12 @@ hash: ecd68d1efcdac724b47d7da0a0c8fccf88a44ebf3888e32c6e1ab4eb8aef61ea -updated: 2017-06-16T17:47:29.132040807-07:00 +updated: 2017-06-19T20:46:05.130532456-07:00 imports: - name: github.com/jinzhu/gorm version: caa792644ce60fd7b429e0616afbdbccdf011be2 - name: github.com/jinzhu/inflection version: 74387dc39a75e970e7a3ae6a3386b5bd2e5c5cff - name: github.com/labstack/echo - version: 2d7cce467709a9e54ae3f8d77863e6d01859e134 + version: 7676f85ef9d4a50797e17b30c4b0b1cb3c7c532a - name: github.com/labstack/gommon version: 1121fd3e243c202482226a7afe4dcd07ffc4139a subpackages: @@ -20,12 +20,17 @@ imports: version: 941b50ebc6efddf4c41c8e4537a5f68a4e686b24 - name: github.com/mattn/go-isatty version: fc9e8d8ef48496124e79ae0df75490096eccf6fe +- name: github.com/SermoDigital/jose + version: b10f12c5188d355a3eacc61aba9abd3eb6b3b787 + subpackages: + - crypto + - jwt - name: github.com/valyala/bytebufferpool version: e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7 - name: github.com/valyala/fasttemplate version: dcecefd839c4193db0d35b88ec65b4c12d360ab0 - name: golang.org/x/crypto - version: 850760c427c516be930bc91280636328f1a62286 + version: adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d subpackages: - acme - acme/autocert From b2ea7e7dd2d4b92d4c7bfee129ce5fd63250369f Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Tue, 20 Jun 2017 10:49:42 -0700 Subject: [PATCH 07/15] Configuration for Remote in Marathon --- tabernacle/ansible/group_vars/all | 2 + .../roles/dev/marathon/tasks/app_remote.yml | 25 +++++++++ .../roles/dev/marathon/tasks/backend.yml | 4 ++ .../roles/dev/marathon/templates/remote.json | 53 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 tabernacle/ansible/roles/dev/marathon/tasks/app_remote.yml create mode 100644 tabernacle/ansible/roles/dev/marathon/templates/remote.json diff --git a/tabernacle/ansible/group_vars/all b/tabernacle/ansible/group_vars/all index 81b64626eb..a1ef086a3d 100644 --- a/tabernacle/ansible/group_vars/all +++ b/tabernacle/ansible/group_vars/all @@ -84,6 +84,7 @@ docker_tags: data_import: "{{ lookup('env', 'DOCKER_TAG_DATA_IMPORT') | default('master', true) }}" onboarding_service: "{{ lookup('env', 'DOCKER_TAG_ONBOARING_SERVICE') | default('master', true) }}" onboarding_ui: "{{ lookup('env', 'DOCKER_TAG_ONBOARING_UI') | default('master', true) }}" + remote: "{{ lookup('env', 'DOCKER_TAG_REMOTE') | default('master', true) }}" # Configurable Marathon re-deploys marathon_restart: @@ -119,6 +120,7 @@ marathon_restart: neo4j: "{{ lookup('env', 'MARATHON_NEO4J') | default(true, true) | bool }}" neo4j_reset: "{{ lookup('env', 'MARATHON_NEO4J_RESET') | default(true, true) | bool }}" suggester: "{{ lookup('env', 'MARATHON_SUGGESTER') | default(true, true) | bool }}" + remote: "{{ lookup('env', 'MARATHON_REMOTE') | default('master', true) | bool }}" # Should we do seeding for appliance detach_seeders: "{{ lookup('env', 'DETACH_SEEDERS') | default(false, true) | bool }}" diff --git a/tabernacle/ansible/roles/dev/marathon/tasks/app_remote.yml b/tabernacle/ansible/roles/dev/marathon/tasks/app_remote.yml new file mode 100644 index 0000000000..ae6f334699 --- /dev/null +++ b/tabernacle/ansible/roles/dev/marathon/tasks/app_remote.yml @@ -0,0 +1,25 @@ +--- + +- name: Copy Remote Marathon JSON + template: src=remote.json dest=/marathon/applications mode="u+rw,g+rw,o+r" + +- name: Kill Remote Tasks in Marathon + shell: 'curl -sS -XDELETE http://{{marathon_server}}/v2/apps/remote/tasks?scale=true' + when: is_redeploy + +- name: Pause for a bit... + pause: seconds=15 + when: is_redeploy + +- name: Update Remote in Marathon + shell: 'curl -sS -XPUT -d@/marathon/applications/remote.json -H "Content-Type: application/json" http://{{marathon_server}}/v2/apps/remote' + +- name: Restart Remote in Marathon + shell: 'curl -sS -XPOST http://{{marathon_server}}/v2/apps/remote/restart' + +- name: Get Remote Marathon tasks in `healthy` state + shell: curl -sS -XGET http://{{marathon_server}}/v2/apps/remote | jq '.app.tasksHealthy > 0' + register: healthy_tasks_available + until: healthy_tasks_available.stdout == 'true' + retries: "{{marathon_retries}}" + delay: "{{marathon_delay}}" diff --git a/tabernacle/ansible/roles/dev/marathon/tasks/backend.yml b/tabernacle/ansible/roles/dev/marathon/tasks/backend.yml index 38eced6b1c..79cdad2194 100644 --- a/tabernacle/ansible/roles/dev/marathon/tasks/backend.yml +++ b/tabernacle/ansible/roles/dev/marathon/tasks/backend.yml @@ -20,6 +20,10 @@ include: app_hyperion.yml when: marathon_restart.hyperion +- name: Start Remote + include: app_remote.yml + when: marathon_restart.remote + - name: Start Onboarding Service include: app_onboarding_service.yml when: with_onboarding and marathon_restart.onboarding_service diff --git a/tabernacle/ansible/roles/dev/marathon/templates/remote.json b/tabernacle/ansible/roles/dev/marathon/templates/remote.json new file mode 100644 index 0000000000..9aa5b4ec76 --- /dev/null +++ b/tabernacle/ansible/roles/dev/marathon/templates/remote.json @@ -0,0 +1,53 @@ +{ + "id": "/remote", + "cmd": null, + "cpus": 0.25, + "mem": 64, + "disk": 0, + "instances": 1, + "labels": { + "LAYER": "backend", + "LANG": "go", + "consul": "remote", + "TAG": "{{docker_tags.remote}}" + }, + "container": { + "type": "DOCKER", + "volumes": [ + { + "containerPath": "{{docker_logs_dir}}", + "hostPath": "{{docker_logs_host_dir}}", + "mode": "RW" + } + ], + "docker": { + "image": "{{docker_registry}}:5000/remote:{{docker_tags.remote}}", + "network": "HOST", + "privileged": false, + "parameters": [], + "forcePullImage": true + } + }, + "env": { + "IC_DATABASE_HOST": "{{docker_db_host}}", + "IC_DATABASE_NAME": "{{bernardo_db_name}}", + "IC_DATABASE_USER": "{{bernardo_db_user}}", + "IC_DATABASE_SSL": "disable", + "PHX_DATABASE_HOST": "{{docker_db_host}}", + "PHX_DATABASE_NAME": "{{phoenix_db_name}}", + "PHX_DATABASE_USER": "{{phoenix_db_user}}", + "PHX_DATABASE_SSL": "disable" + }, + "healthChecks": [ + { + "path": "/v1/public/health", + "protocol": "HTTP", + "gracePeriodSeconds": 300, + "intervalSeconds": 30, + "timeoutSeconds": 20, + "maxConsecutiveFailures": 3, + "ignoreHttp1xx": false, + "portIndex": 0 + } + ] +} From 0d40f0512ecd24f0d194c1c21de60145bcbb7132 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Tue, 20 Jun 2017 13:18:44 -0700 Subject: [PATCH 08/15] Health check endpoint --- remote/controllers/ping.go | 45 ++++++++++++++++++++++++++++++++++++++ remote/responses/health.go | 7 ++++++ remote/services/db.go | 4 ++++ 3 files changed, 56 insertions(+) create mode 100644 remote/controllers/ping.go create mode 100644 remote/responses/health.go diff --git a/remote/controllers/ping.go b/remote/controllers/ping.go new file mode 100644 index 0000000000..f7f91ae068 --- /dev/null +++ b/remote/controllers/ping.go @@ -0,0 +1,45 @@ +package controllers + +import ( + "net/http" + + "github.com/FoxComm/highlander/remote/responses" + "github.com/FoxComm/highlander/remote/services" + "github.com/FoxComm/highlander/remote/utils/failures" +) + +// Ping is a really simple controller used for health checks. +type Ping struct { + dbs *services.RemoteDBs +} + +type health struct { + Intelligence string `json:"intelligence"` + Phoenix string `json:"phoenix"` +} + +// GetHealth tests the connection to the databases and returns the status. +func (ctrl *Ping) GetHealth() ControllerFunc { + return func() (*responses.Response, failures.Failure) { + icPingErr := ctrl.dbs.IC().Ping() + phxPingErr := ctrl.dbs.Phx().Ping() + + statusCode := http.StatusOK + h := health{ + Intelligence: "passed", + Phoenix: "passed", + } + + if icPingErr != nil { + statusCode = http.StatusInternalServerError + h.Intelligence = "failed" + } + + if phxPingErr != nil { + statusCode = http.StatusInternalServerError + h.Phoenix = "failed" + } + + return responses.NewResponse(statusCode, h), nil + } +} diff --git a/remote/responses/health.go b/remote/responses/health.go new file mode 100644 index 0000000000..c2c3218b60 --- /dev/null +++ b/remote/responses/health.go @@ -0,0 +1,7 @@ +package responses + +// Health is the response that determines +type Health struct { + Intelligence string `json:"intelligence"` + Phoenix string `json:"phoenix"` +} diff --git a/remote/services/db.go b/remote/services/db.go index 618517a039..87791b1179 100644 --- a/remote/services/db.go +++ b/remote/services/db.go @@ -101,3 +101,7 @@ func (r RemoteDB) FindByIDWithFailure(table string, id int, model interface{}, f func (r RemoteDB) Save(model interface{}) failures.Failure { return failures.New(r.db.Save(model).Error) } + +func (r RemoteDB) Ping() error { + return r.db.DB().Ping() +} From a6327ddcf4dc6dc443467570af2e6eb449e8330e Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Wed, 21 Jun 2017 16:33:02 -0700 Subject: [PATCH 09/15] Dockerize remote --- remote/Dockerfile | 13 ++++ remote/controllers/fox_context.go | 40 ++++++++++++- .../templates/core-backend/remote.json.j2 | 59 +++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 remote/Dockerfile create mode 100644 tabernacle/ansible/roles/dev/marathon_groups/templates/core-backend/remote.json.j2 diff --git a/remote/Dockerfile b/remote/Dockerfile new file mode 100644 index 0000000000..7faf20340e --- /dev/null +++ b/remote/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:alpine + +RUN apk add --no-cache ca-certificates + +RUN mkdir -p /remote +ADD . /go/src/github.com/FoxComm/highlander/remote +WORKDIR /go/src/github.com/FoxComm/highlander/remote +RUN go build -o remote main.go && \ + cp remote /remote && \ + rm -rf /go +WORKDIR /remote + +CMD /remote/remote 2>&1 | tee /logs/remote.log diff --git a/remote/controllers/fox_context.go b/remote/controllers/fox_context.go index 14a8fe102b..76d43f3f3f 100644 --- a/remote/controllers/fox_context.go +++ b/remote/controllers/fox_context.go @@ -17,15 +17,27 @@ import ( type FoxContext struct { echo.Context failure failures.Failure - // scope string + scope string } // NewFoxContext creates a new FoxContext from an existing echo.Context. func NewFoxContext(c echo.Context) *FoxContext { - return &FoxContext{c, nil} + var scope string + + jwtStr, fail := getJWTToken(c) + if fail == nil { + jwt, err := NewJWT(jwtStr) + if err != nil { + fail = failures.New(err) + } else { + scope = jwt.Scope() + } + } + + return &FoxContext{c, fail, scope} } -func getJWT(c echo.Context) (string, failures.Failure) { +func getJWTToken(c echo.Context) (string, failures.Failure) { // Try to get from the header first. req := c.Request() jwt, ok := req.Header["Jwt"] @@ -42,6 +54,28 @@ func getJWT(c echo.Context) (string, failures.Failure) { return cookie.Value, nil } +func getJWT(c echo.Context) (string, failures.Failure) { + // Try to get from the header first. + req := c.Request() + jwt, ok := req.Header["Jwt"] + if ok && len(jwt) > 0 { + return jwt[0], nil + } + + // Try a cookie. + cookie, err := req.Cookie("JWT") + if err != nil { + return nil, failures.New(err) + } + + jwt, err := NewJWT(cookie.Value) + if err != nil { + return nil, failures.New(err) + } + + return jwt, nil +} + // BindJSON grabs the JSON payload and unmarshals it into the interface provided. func (fc *FoxContext) BindJSON(payload payloads.Payload) { if fc.failure != nil { diff --git a/tabernacle/ansible/roles/dev/marathon_groups/templates/core-backend/remote.json.j2 b/tabernacle/ansible/roles/dev/marathon_groups/templates/core-backend/remote.json.j2 new file mode 100644 index 0000000000..0d32ac22d9 --- /dev/null +++ b/tabernacle/ansible/roles/dev/marathon_groups/templates/core-backend/remote.json.j2 @@ -0,0 +1,59 @@ +{ + "id": "remote", + "cmd": null, + "cpus": 0.25, + "mem": 64, + "disk": 0, + "instances": 1, + "constraints": [], + "labels": { + "MARATHON_SINGLE_INSTANCE_APP": "false", + "LANG": "go", + "consul": "remote", + "overrideTaskName": "remote", + "TAG": "{{docker_tags.remote}}" + }, + "upgradeStrategy": { + "minimumHealthCapacity": 0, + "maximumOverCapacity": 0 + }, + "container": { + "type": "DOCKER", + "volumes": [ + { + "containerPath": "{{docker_logs_dir}}", + "hostPath": "{{docker_logs_host_dir}}", + "mode": "RW" + } + ], + "docker": { + "image": "{{docker_registry}}:5000/remote:{{docker_tags.remote}}", + "network": "HOST", + "privileged": false, + "parameters": [], + "forcePullImage": true + } + }, + "env": { + "IC_DATABASE_HOST": "{{docker_db_host}}", + "IC_DATABASE_NAME": "{{bernardo_db_name}}", + "IC_DATABASE_USER": "{{bernardo_db_user}}", + "IC_DATABASE_SSL": "disable", + "PHX_DATABASE_HOST": "{{docker_db_host}}", + "PHX_DATABASE_NAME": "{{phoenix_db_name}}", + "PHX_DATABASE_USER": "{{phoenix_db_user}}", + "PHX_DATABASE_SSL": "disable" + }, + "healthChecks": [ + { + "path": "/v1/public/health", + "protocol": "HTTP", + "gracePeriodSeconds": 300, + "intervalSeconds": 30, + "timeoutSeconds": 20, + "maxConsecutiveFailures": 3, + "ignoreHttp1xx": false, + "portIndex": 0 + } + ] +} From 7cf652e09fb1ab7f142df5f6e63f74068e004d95 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Wed, 21 Jun 2017 16:33:25 -0700 Subject: [PATCH 10/15] Dockerize remote --- remote/controllers/controllers.go | 5 +++++ remote/controllers/ping.go | 5 +++++ tabernacle/ansible/group_vars/all | 4 ++++ .../roles/app/config_gen/templates/goldrush.cfg.j2 | 2 ++ .../roles/dev/balancer/templates/service_locations.j2 | 10 ++++++++++ .../ansible/roles/dev/balancer/templates/services.j2 | 6 ++++++ .../dev/marathon_groups/templates/highlander.json.j2 | 1 + 7 files changed, 33 insertions(+) diff --git a/remote/controllers/controllers.go b/remote/controllers/controllers.go index 26b7796ad4..ce74a9e7cd 100644 --- a/remote/controllers/controllers.go +++ b/remote/controllers/controllers.go @@ -21,9 +21,14 @@ func Start() { } channelsCtrl := NewChannels(dbs) + pingCtrl := NewPing(dbs) r := NewRouter() + r.GET("/v1/public/health", func(fc *FoxContext) error { + return fc.Run(pingCtrl.GetHealth()) + }) + r.GET("/v1/public/channels/:id", func(fc *FoxContext) error { id := fc.ParamInt("id") return fc.Run(channelsCtrl.GetChannel(id)) diff --git a/remote/controllers/ping.go b/remote/controllers/ping.go index f7f91ae068..9dd4012c69 100644 --- a/remote/controllers/ping.go +++ b/remote/controllers/ping.go @@ -13,6 +13,11 @@ type Ping struct { dbs *services.RemoteDBs } +// NewPing creates a new ping controller. +func NewPing(dbs *services.RemoteDBs) *Ping { + return &Ping{dbs: dbs} +} + type health struct { Intelligence string `json:"intelligence"` Phoenix string `json:"phoenix"` diff --git a/tabernacle/ansible/group_vars/all b/tabernacle/ansible/group_vars/all index a1ef086a3d..428ec59c77 100644 --- a/tabernacle/ansible/group_vars/all +++ b/tabernacle/ansible/group_vars/all @@ -226,6 +226,10 @@ hyperion_host: "hyperion.{{consul_suffix}}" hyperion_port: 8880 hyperion_server: "{{hyperion_host}}:{{hyperion_port}}" +remote_host: "remote.{{consul_suffix}}" +remote_port: 9898 +remote_server: "{{remote_host}}:{{remote_port}}" + # Database & bottledwater db_user: phoenix db_name: phoenix diff --git a/tabernacle/ansible/roles/app/config_gen/templates/goldrush.cfg.j2 b/tabernacle/ansible/roles/app/config_gen/templates/goldrush.cfg.j2 index 212210ec46..31e5a7eb96 100644 --- a/tabernacle/ansible/roles/app/config_gen/templates/goldrush.cfg.j2 +++ b/tabernacle/ansible/roles/app/config_gen/templates/goldrush.cfg.j2 @@ -24,6 +24,7 @@ export DOCKER_TAG_MESSAGING:=master export DOCKER_TAG_ISAAC:=master export DOCKER_TAG_SOLOMON:=master export DOCKER_TAG_HYPERION:=master +export DOCKER_TAG_REMOTE:=master # Consumers export DOCKER_TAG_CAPTURE_CONSUMER:=master @@ -74,6 +75,7 @@ export MARATHON_MESSAGING:=false export MARATHON_ISAAC:=false export MARATHON_SOLOMON:=false export MARATHON_HYPERION:=false +export MARATHON_REMOTE:=false # Consumers export MARATHON_CAPTURE_CONSUMER:=false diff --git a/tabernacle/ansible/roles/dev/balancer/templates/service_locations.j2 b/tabernacle/ansible/roles/dev/balancer/templates/service_locations.j2 index e3f60ed127..3a0d7e41fc 100644 --- a/tabernacle/ansible/roles/dev/balancer/templates/service_locations.j2 +++ b/tabernacle/ansible/roles/dev/balancer/templates/service_locations.j2 @@ -270,6 +270,16 @@ location /api/v1/hyperion/ { break; } +location /api/v1/tenant/ { + auth_request /internal-auth; + proxy_pass http://remote/v1/public/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + break; +} + location = /admin/styleguide { rewrite ^ https://{{storefront_server_name}}/admin/styleguide/ permanent; } diff --git a/tabernacle/ansible/roles/dev/balancer/templates/services.j2 b/tabernacle/ansible/roles/dev/balancer/templates/services.j2 index c4a3f36e54..66d25c0e95 100644 --- a/tabernacle/ansible/roles/dev/balancer/templates/services.j2 +++ b/tabernacle/ansible/roles/dev/balancer/templates/services.j2 @@ -20,6 +20,7 @@ upstream eggcrate { << else >> server {{eggcrate_server}} fail_timeout=30s max_fails=10; << end >> } + upstream anthill { << range service "anthill" >> server << .Address >>:<< .Port >> max_fails=10 fail_timeout=30s weight=1; << else >> server {{anthill_server}} fail_timeout=30s max_fails=10; << end >> @@ -63,6 +64,11 @@ upstream hyperion { << else >> server {{hyperion_server}} fail_timeout=30s max_fails=10; << end >> } +upstream remote { + << range service "remote" >> server << .Address >>:<< .Port >> max_fails=10 fail_timeout=30s weight=1; + << else >> server {{remote_server}} fail_timeout=30s max_fails=10; << end >> +} + upstream ashes { << range service "ashes" >> server << .Address >>:<< .Port >> max_fails=10 fail_timeout=30s weight=1; << else >> server {{ashes_server}} fail_timeout=30s max_fails=10; << end >> diff --git a/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 b/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 index a38dbc77e8..f8f4956666 100644 --- a/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 +++ b/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 @@ -8,6 +8,7 @@ {% include "core-backend/phoenix.json.j2" %}, {% include "core-backend/isaac.json.j2" %}, {% include "core-backend/solomon.json.j2" %}, + {% include "core-backend/remote.json.j2" %}, {% include "core-backend/middlewarehouse.json.j2" %} ] }, From 749ee05ac9f0dbb6dbb46902c5bea42a9d8ccbd6 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Wed, 21 Jun 2017 16:33:46 -0700 Subject: [PATCH 11/15] Parse the scope from the JWT --- remote/controllers/jwt.go | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 remote/controllers/jwt.go diff --git a/remote/controllers/jwt.go b/remote/controllers/jwt.go new file mode 100644 index 0000000000..0adc9c816c --- /dev/null +++ b/remote/controllers/jwt.go @@ -0,0 +1,56 @@ +package controllers + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strings" +) + +// JWT is a simplified JWT wrapper for our usage. Since we rely on Isaac for +// validation, we just get customer and scope information. +type JWT struct { + Header map[string]interface{} + Payload map[string]interface{} +} + +// NewJWT parses the JWT from a string. +func NewJWT(jwtStr string) (*JWT, error) { + parts := strings.Split(jwtStr, ".") + if len(parts) != 3 { + return nil, errors.New("JWT is malformed") + } + + headerBytes, err := base64.URLEncoding.DecodeString(parts[0]) + if err != nil { + return nil, fmt.Errorf("Error decoding header with error: %s", err) + } + + header := map[string]interface{}{} + if err := json.Unmarshal(headerBytes, &header); err != nil { + return nil, fmt.Errorf("Error marshalling header with error: %s", err) + } + + payloadBytes, err := base64.URLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, fmt.Errorf("Error decoding payload with error: %s", err) + } + + payload := map[string]interface{}{} + if err := json.Unmarshal(payloadBytes, &payload); err != nil { + return nil, fmt.Errorf("Error marshalling payload with error: %s", err) + } + + return &JWT{Header: header, Payload: payload}, nil +} + +// Scope gets the scope string passed in the JWT. +func (j JWT) Scope() string { + scope, ok := j.Payload["Scope"] + if !ok { + return "" + } + + return scope.(string) +} From 65e975ebea3c18f04781ea4f8651db6621b6fcdf Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 22 Jun 2017 10:46:53 -0700 Subject: [PATCH 12/15] Fix scope build errors --- remote/controllers/#fox_context.go# | 164 ++++++++++++++++++++++++++++ remote/controllers/.#fox_context.go | 1 + remote/controllers/fox_context.go | 26 +---- remote/payloads/channel_payloads.go | 10 ++ remote/payloads/scoped.go | 6 + 5 files changed, 183 insertions(+), 24 deletions(-) create mode 100644 remote/controllers/#fox_context.go# create mode 120000 remote/controllers/.#fox_context.go create mode 100644 remote/payloads/scoped.go diff --git a/remote/controllers/#fox_context.go# b/remote/controllers/#fox_context.go# new file mode 100644 index 0000000000..2a10539f13 --- /dev/null +++ b/remote/controllers/#fox_context.go# @@ -0,0 +1,164 @@ +package controllers + +import ( + "errors" + "fmt" + "log" + "net/http" + "strconv" + + "github.com/FoxComm/highlander/remote/payloads" + "github.com/FoxComm/highlander/remote/utils/failures" + "github.com/labstack/echo" +) + +// FoxContext is a wrapper around echo.Context that eases error handling, +// provides helper methods, and ensures we have consistent response handling. +type FoxContext struct { + echo.Context + Scope string + failure failures.Failure +} + +// NewFoxContext creates a new FoxContext from an existing echo.Context. +func NewFoxContext(c echo.Context) *FoxContext { + var scope string + + jwtStr, fail := getJWTToken(c) + if fail == nil { + jwt, err := NewJWT(jwtStr) + if err != nil { + fail = failures.New(err) + } else { + scope = jwt.Scope() + } + } + + return &FoxContext{c, scope, fail} +} + +func getJWTToken(c echo.Context) (string, failures.Failure) { + // Try to get from the header first. + req := c.Request() + jwt, ok := req.Header["Jwt"] + if ok && len(jwt) > 0 { + return jwt[0], nil + } + + // Try a cookie. + cookie, err := req.Cookie("JWT") + if err != nil { + return "", failures.New(err) + } + + return cookie.Value, nil +} + +// BindJSON grabs the JSON payload and unmarshals it into the interface provided. +func (fc *FoxContext) BindJSON(payload payloads.Payload) { + if fc.failure != nil { + return + } + + if err := fc.Bind(payload); err != nil { + fc.failure = failures.NewBindFailure(err) + return + } + + if err := payload.Validate(); err != nil { + fc.failure = err + return + } + + if +} + +// ParamInt parses an integer from the parameters list (as defined by the URI). +func (fc *FoxContext) ParamInt(name string) int { + if fc.failure != nil { + return 0 + } + + param := fc.Param(name) + if param == "" { + fc.failure = failures.NewParamNotFound(name) + return 0 + } + + paramInt, err := strconv.Atoi(param) + if err != nil { + fc.failure = failures.NewParamInvalidType(name, "number") + return 0 + } + + return paramInt +} + +// ParamString parses an string from the parameters list (as defined by the URI). +func (fc *FoxContext) ParamString(name string) string { + if fc.failure != nil { + return "" + } + + param := fc.Param(name) + if param == "" { + fc.failure = failures.NewParamNotFound(name) + return "" + } + + return param +} + +// Run executes the primary controller method and returns the response. +func (fc *FoxContext) Run(ctrlFn ControllerFunc) error { + if fc.failure != nil { + return fc.handleFailure(fc.failure) + } + + resp, failure := ctrlFn() + if failure != nil { + return fc.handleFailure(failure) + } + + return fc.JSON(resp.StatusCode, resp.Body) +} + +func (fc *FoxContext) handleFailure(failure failures.Failure) error { + if failure == nil { + return errors.New("handleFailure must receive a failure") + } else if !failure.HasError() { + return errors.New("handleFailure must receive a failure with an error") + } + + trace, err := failure.Trace() + if err != nil { + return fmt.Errorf("Error trying to log trace with error: %s", err.Error()) + } + + log.Printf("%s: %s\n\nError: %s\n", fc.Request().Method, fc.Request().URL.Path, trace) + + errString := failure.Error() + failureType, err := failure.Type() + if err != nil { + return err + } + + var statusCode int + switch failureType { + case failures.FailureBadRequest: + statusCode = http.StatusBadRequest + case failures.FailureNotFound: + statusCode = http.StatusNotFound + case failures.FailureServiceError: + statusCode = http.StatusInternalServerError + errString = "Unexpected error occurred" + default: + return fmt.Errorf("Invalid failure type, got %d", failureType) + } + + errResp := map[string][]string{ + "errors": []string{errString}, + } + + return fc.JSON(statusCode, errResp) +} diff --git a/remote/controllers/.#fox_context.go b/remote/controllers/.#fox_context.go new file mode 120000 index 0000000000..b56fe5e6c0 --- /dev/null +++ b/remote/controllers/.#fox_context.go @@ -0,0 +1 @@ +jeff@fox.local.36720 \ No newline at end of file diff --git a/remote/controllers/fox_context.go b/remote/controllers/fox_context.go index 76d43f3f3f..121ae7eab9 100644 --- a/remote/controllers/fox_context.go +++ b/remote/controllers/fox_context.go @@ -16,8 +16,8 @@ import ( // provides helper methods, and ensures we have consistent response handling. type FoxContext struct { echo.Context + Scope string failure failures.Failure - scope string } // NewFoxContext creates a new FoxContext from an existing echo.Context. @@ -34,7 +34,7 @@ func NewFoxContext(c echo.Context) *FoxContext { } } - return &FoxContext{c, fail, scope} + return &FoxContext{c, scope, fail} } func getJWTToken(c echo.Context) (string, failures.Failure) { @@ -54,28 +54,6 @@ func getJWTToken(c echo.Context) (string, failures.Failure) { return cookie.Value, nil } -func getJWT(c echo.Context) (string, failures.Failure) { - // Try to get from the header first. - req := c.Request() - jwt, ok := req.Header["Jwt"] - if ok && len(jwt) > 0 { - return jwt[0], nil - } - - // Try a cookie. - cookie, err := req.Cookie("JWT") - if err != nil { - return nil, failures.New(err) - } - - jwt, err := NewJWT(cookie.Value) - if err != nil { - return nil, failures.New(err) - } - - return jwt, nil -} - // BindJSON grabs the JSON payload and unmarshals it into the interface provided. func (fc *FoxContext) BindJSON(payload payloads.Payload) { if fc.failure != nil { diff --git a/remote/payloads/channel_payloads.go b/remote/payloads/channel_payloads.go index 912691ebe9..da752bd551 100644 --- a/remote/payloads/channel_payloads.go +++ b/remote/payloads/channel_payloads.go @@ -18,6 +18,16 @@ type CreateChannel struct { CatalogID *int64 `json:"catalogId"` } +// Scope gets the current scope on the payload. +func (c CreateChannel) Scope() string { + return c.Scope +} + +// SetScope sets the scope on the payload. +func (c *CreateChannel) SetScope(scope string) { + c.Scope = scope +} + // Validate ensures that the has the correct format. func (c CreateChannel) Validate() failures.Failure { if c.Name == "" { diff --git a/remote/payloads/scoped.go b/remote/payloads/scoped.go new file mode 100644 index 0000000000..02252549c4 --- /dev/null +++ b/remote/payloads/scoped.go @@ -0,0 +1,6 @@ +package payloads + +type Scoped interface { + Scope() string + SetScope(string) +} From 10502c1bb95475228ddd5be07139936ddb6f5585 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 22 Jun 2017 12:20:33 -0700 Subject: [PATCH 13/15] Scoped payloads in remote! --- remote/controllers/#fox_context.go# | 164 ------------------ remote/controllers/.#fox_context.go | 1 - remote/controllers/fox_context.go | 40 +++-- remote/payloads/channel_payloads.go | 14 +- remote/payloads/scoped.go | 3 +- remote/services/queries.go | 2 +- .../remote.json.j2 | 0 .../templates/highlander.json.j2 | 2 +- 8 files changed, 35 insertions(+), 191 deletions(-) delete mode 100644 remote/controllers/#fox_context.go# delete mode 120000 remote/controllers/.#fox_context.go rename tabernacle/ansible/roles/dev/marathon_groups/templates/{core-backend => core-integrations}/remote.json.j2 (100%) diff --git a/remote/controllers/#fox_context.go# b/remote/controllers/#fox_context.go# deleted file mode 100644 index 2a10539f13..0000000000 --- a/remote/controllers/#fox_context.go# +++ /dev/null @@ -1,164 +0,0 @@ -package controllers - -import ( - "errors" - "fmt" - "log" - "net/http" - "strconv" - - "github.com/FoxComm/highlander/remote/payloads" - "github.com/FoxComm/highlander/remote/utils/failures" - "github.com/labstack/echo" -) - -// FoxContext is a wrapper around echo.Context that eases error handling, -// provides helper methods, and ensures we have consistent response handling. -type FoxContext struct { - echo.Context - Scope string - failure failures.Failure -} - -// NewFoxContext creates a new FoxContext from an existing echo.Context. -func NewFoxContext(c echo.Context) *FoxContext { - var scope string - - jwtStr, fail := getJWTToken(c) - if fail == nil { - jwt, err := NewJWT(jwtStr) - if err != nil { - fail = failures.New(err) - } else { - scope = jwt.Scope() - } - } - - return &FoxContext{c, scope, fail} -} - -func getJWTToken(c echo.Context) (string, failures.Failure) { - // Try to get from the header first. - req := c.Request() - jwt, ok := req.Header["Jwt"] - if ok && len(jwt) > 0 { - return jwt[0], nil - } - - // Try a cookie. - cookie, err := req.Cookie("JWT") - if err != nil { - return "", failures.New(err) - } - - return cookie.Value, nil -} - -// BindJSON grabs the JSON payload and unmarshals it into the interface provided. -func (fc *FoxContext) BindJSON(payload payloads.Payload) { - if fc.failure != nil { - return - } - - if err := fc.Bind(payload); err != nil { - fc.failure = failures.NewBindFailure(err) - return - } - - if err := payload.Validate(); err != nil { - fc.failure = err - return - } - - if -} - -// ParamInt parses an integer from the parameters list (as defined by the URI). -func (fc *FoxContext) ParamInt(name string) int { - if fc.failure != nil { - return 0 - } - - param := fc.Param(name) - if param == "" { - fc.failure = failures.NewParamNotFound(name) - return 0 - } - - paramInt, err := strconv.Atoi(param) - if err != nil { - fc.failure = failures.NewParamInvalidType(name, "number") - return 0 - } - - return paramInt -} - -// ParamString parses an string from the parameters list (as defined by the URI). -func (fc *FoxContext) ParamString(name string) string { - if fc.failure != nil { - return "" - } - - param := fc.Param(name) - if param == "" { - fc.failure = failures.NewParamNotFound(name) - return "" - } - - return param -} - -// Run executes the primary controller method and returns the response. -func (fc *FoxContext) Run(ctrlFn ControllerFunc) error { - if fc.failure != nil { - return fc.handleFailure(fc.failure) - } - - resp, failure := ctrlFn() - if failure != nil { - return fc.handleFailure(failure) - } - - return fc.JSON(resp.StatusCode, resp.Body) -} - -func (fc *FoxContext) handleFailure(failure failures.Failure) error { - if failure == nil { - return errors.New("handleFailure must receive a failure") - } else if !failure.HasError() { - return errors.New("handleFailure must receive a failure with an error") - } - - trace, err := failure.Trace() - if err != nil { - return fmt.Errorf("Error trying to log trace with error: %s", err.Error()) - } - - log.Printf("%s: %s\n\nError: %s\n", fc.Request().Method, fc.Request().URL.Path, trace) - - errString := failure.Error() - failureType, err := failure.Type() - if err != nil { - return err - } - - var statusCode int - switch failureType { - case failures.FailureBadRequest: - statusCode = http.StatusBadRequest - case failures.FailureNotFound: - statusCode = http.StatusNotFound - case failures.FailureServiceError: - statusCode = http.StatusInternalServerError - errString = "Unexpected error occurred" - default: - return fmt.Errorf("Invalid failure type, got %d", failureType) - } - - errResp := map[string][]string{ - "errors": []string{errString}, - } - - return fc.JSON(statusCode, errResp) -} diff --git a/remote/controllers/.#fox_context.go b/remote/controllers/.#fox_context.go deleted file mode 120000 index b56fe5e6c0..0000000000 --- a/remote/controllers/.#fox_context.go +++ /dev/null @@ -1 +0,0 @@ -jeff@fox.local.36720 \ No newline at end of file diff --git a/remote/controllers/fox_context.go b/remote/controllers/fox_context.go index 121ae7eab9..2bfe4e7dfb 100644 --- a/remote/controllers/fox_context.go +++ b/remote/controllers/fox_context.go @@ -16,30 +16,31 @@ import ( // provides helper methods, and ensures we have consistent response handling. type FoxContext struct { echo.Context - Scope string failure failures.Failure } // NewFoxContext creates a new FoxContext from an existing echo.Context. func NewFoxContext(c echo.Context) *FoxContext { - var scope string - - jwtStr, fail := getJWTToken(c) - if fail == nil { - jwt, err := NewJWT(jwtStr) - if err != nil { - fail = failures.New(err) - } else { - scope = jwt.Scope() - } + return &FoxContext{c, nil} +} + +func (fc *FoxContext) getJWT() (*JWT, failures.Failure) { + jwtStr, fail := fc.getJWTString() + if fail != nil { + return nil, fail + } + + jwt, err := NewJWT(jwtStr) + if err != nil { + return nil, failures.New(err) } - return &FoxContext{c, scope, fail} + return jwt, nil } -func getJWTToken(c echo.Context) (string, failures.Failure) { +func (fc *FoxContext) getJWTString() (string, failures.Failure) { // Try to get from the header first. - req := c.Request() + req := fc.Request() jwt, ok := req.Header["Jwt"] if ok && len(jwt) > 0 { return jwt[0], nil @@ -69,6 +70,17 @@ func (fc *FoxContext) BindJSON(payload payloads.Payload) { fc.failure = err return } + + scoped, ok := payload.(payloads.Scoped) + if ok { + jwt, fail := fc.getJWT() + if fail != nil { + fc.failure = fail + return + } + + scoped.EnsureScope(jwt.Scope()) + } } // ParamInt parses an integer from the parameters list (as defined by the URI). diff --git a/remote/payloads/channel_payloads.go b/remote/payloads/channel_payloads.go index da752bd551..04421d0677 100644 --- a/remote/payloads/channel_payloads.go +++ b/remote/payloads/channel_payloads.go @@ -18,14 +18,12 @@ type CreateChannel struct { CatalogID *int64 `json:"catalogId"` } -// Scope gets the current scope on the payload. -func (c CreateChannel) Scope() string { - return c.Scope -} - -// SetScope sets the scope on the payload. -func (c *CreateChannel) SetScope(scope string) { - c.Scope = scope +// EnsureScope guarantees that a scope is set, either by using the scope that +// already exists on the payload, or setting a default scope from the caller. +func (c *CreateChannel) EnsureScope(scope string) { + if c.Scope == "" { + c.Scope = scope + } } // Validate ensures that the has the correct format. diff --git a/remote/payloads/scoped.go b/remote/payloads/scoped.go index 02252549c4..38b431221c 100644 --- a/remote/payloads/scoped.go +++ b/remote/payloads/scoped.go @@ -1,6 +1,5 @@ package payloads type Scoped interface { - Scope() string - SetScope(string) + EnsureScope(string) } diff --git a/remote/services/queries.go b/remote/services/queries.go index a383911cfe..3230527b33 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -30,7 +30,7 @@ func InsertChannel(dbs *RemoteDBs, icChannel *ic.Channel, phxChannel *phoenix.Ch return fail } - hostMaps := icChannel.HostMaps(hosts, "1") + hostMaps := icChannel.HostMaps(hosts, phxChannel.Scope) // We have to iterate through each insert manually because of a limitation in // Gorm. Since there will rarely be many hosts created at a time, this should // be a workable solution for now. diff --git a/tabernacle/ansible/roles/dev/marathon_groups/templates/core-backend/remote.json.j2 b/tabernacle/ansible/roles/dev/marathon_groups/templates/core-integrations/remote.json.j2 similarity index 100% rename from tabernacle/ansible/roles/dev/marathon_groups/templates/core-backend/remote.json.j2 rename to tabernacle/ansible/roles/dev/marathon_groups/templates/core-integrations/remote.json.j2 diff --git a/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 b/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 index f8f4956666..6d60f25349 100644 --- a/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 +++ b/tabernacle/ansible/roles/dev/marathon_groups/templates/highlander.json.j2 @@ -8,7 +8,6 @@ {% include "core-backend/phoenix.json.j2" %}, {% include "core-backend/isaac.json.j2" %}, {% include "core-backend/solomon.json.j2" %}, - {% include "core-backend/remote.json.j2" %}, {% include "core-backend/middlewarehouse.json.j2" %} ] }, @@ -51,6 +50,7 @@ {% include "core-integrations/shipstation.json.j2" %}, {% endif %} {% include "core-integrations/messaging.json.j2" %}, + {% include "core-integrations/remote.json.j2" %}, {% include "core-integrations/hyperion.json.j2" %} ] }, From 60836f6d8a13e970209507f57268bbbab8c77de7 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Thu, 22 Jun 2017 12:44:29 -0700 Subject: [PATCH 14/15] Integrate all IC services into responses --- remote/controllers/channels.go | 31 +++++++++++++++++-------------- remote/responses/channel.go | 27 +++++++++++++++++---------- remote/services/queries.go | 12 ++++++++++-- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/remote/controllers/channels.go b/remote/controllers/channels.go index 4416b4bc2e..aa8fe62985 100644 --- a/remote/controllers/channels.go +++ b/remote/controllers/channels.go @@ -3,6 +3,7 @@ package controllers import ( "net/http" + "github.com/FoxComm/highlander/remote/models/ic" "github.com/FoxComm/highlander/remote/models/phoenix" "github.com/FoxComm/highlander/remote/payloads" "github.com/FoxComm/highlander/remote/responses" @@ -21,13 +22,14 @@ func NewChannels(dbs *services.RemoteDBs) *Channels { // GetChannel finds a single channel by its ID. func (ctrl *Channels) GetChannel(id int) ControllerFunc { return func() (*responses.Response, failures.Failure) { - channel := &phoenix.Channel{} + icChannel := &ic.Channel{} + phxChannel := &phoenix.Channel{} - if err := services.FindChannelByID(ctrl.dbs, id, channel); err != nil { + if err := services.FindChannelByID(ctrl.dbs, id, icChannel, phxChannel); err != nil { return nil, err } - resp := responses.NewChannel(channel) + resp := responses.NewChannel(icChannel, phxChannel, []string{}) return responses.NewResponse(http.StatusOK, resp), nil } } @@ -42,7 +44,7 @@ func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerF return nil, err } - resp := responses.NewChannel(phxChannel) + resp := responses.NewChannel(icChannel, phxChannel, payload.Hosts) return responses.NewResponse(http.StatusCreated, resp), nil } } @@ -50,17 +52,18 @@ func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerF // UpdateChannel updates an existing channel. func (ctrl *Channels) UpdateChannel(id int, payload *payloads.UpdateChannel) ControllerFunc { return func() (*responses.Response, failures.Failure) { - existingPhxChannel := &phoenix.Channel{} - if err := services.FindChannelByID(ctrl.dbs, id, existingPhxChannel); err != nil { - return nil, err - } + // existingPhxChannel := &phoenix.Channel{} + // if err := services.FindChannelByID(ctrl.dbs, id, existingPhxChannel); err != nil { + // return nil, err + // } - phxChannel := payload.PhoenixModel(existingPhxChannel) - if err := services.UpdateChannel(ctrl.dbs, phxChannel); err != nil { - return nil, err - } + // phxChannel := payload.PhoenixModel(existingPhxChannel) + // if err := services.UpdateChannel(ctrl.dbs, phxChannel); err != nil { + // return nil, err + // } - resp := responses.NewChannel(phxChannel) - return responses.NewResponse(http.StatusOK, resp), nil + // resp := responses.NewChannel(phxChannel) + // return responses.NewResponse(http.StatusOK, resp), nil + return nil, nil } } diff --git a/remote/responses/channel.go b/remote/responses/channel.go index 5001200a27..e03b2aeada 100644 --- a/remote/responses/channel.go +++ b/remote/responses/channel.go @@ -3,23 +3,30 @@ package responses import ( "time" + "github.com/FoxComm/highlander/remote/models/ic" "github.com/FoxComm/highlander/remote/models/phoenix" ) type Channel struct { - ID int `json:"id"` - Name string `json:"name"` - PurchaseOnFox bool `json:"purchaseOnFox"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID int `json:"id"` + OrganizationID int `json:"organizationId"` + RiverRockChannelID int `json:"riverRockChannelId"` + Name string `json:"name"` + PurchaseOnFox bool `json:"purchaseOnFox"` + Hosts []string `json:"hosts"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } -func NewChannel(phxChannel *phoenix.Channel) *Channel { +func NewChannel(icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) *Channel { c := Channel{ - ID: phxChannel.ID, - Name: phxChannel.Name, - CreatedAt: phxChannel.CreatedAt, - UpdatedAt: phxChannel.UpdatedAt, + ID: phxChannel.ID, + OrganizationID: icChannel.OrganizationID, + RiverRockChannelID: icChannel.ID, + Name: phxChannel.Name, + Hosts: hosts, + CreatedAt: phxChannel.CreatedAt, + UpdatedAt: phxChannel.UpdatedAt, } if phxChannel.PurchaseLocation == phoenix.PurchaseOnFox { diff --git a/remote/services/queries.go b/remote/services/queries.go index 3230527b33..938367fc66 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -6,8 +6,16 @@ import ( "github.com/FoxComm/highlander/remote/utils/failures" ) -func FindChannelByID(dbs *RemoteDBs, id int, phxChannel *phoenix.Channel) failures.Failure { - return dbs.Phx().FindByID("channel", id, phxChannel) +func FindChannelByID(dbs *RemoteDBs, id int, icChannel *ic.Channel, phxChannel *phoenix.Channel) failures.Failure { + if fail := dbs.Phx().FindByID("channel", id, phxChannel); fail != nil { + return fail + } + + if fail := dbs.IC().FindByIDWithFailure("channel", phxChannel.RiverRockChannelID, icChannel, failures.FailureBadRequest); fail != nil { + return fail + } + + return nil } func InsertChannel(dbs *RemoteDBs, icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) failures.Failure { From 9303957be1ca10cec147c8aa7ed1bdbbbe3bff63 Mon Sep 17 00:00:00 2001 From: Jeff Mataya Date: Fri, 5 Jan 2018 23:58:08 -0500 Subject: [PATCH 15/15] WIP --- ....20170619200953__create_channels_table.sql | 1 + remote/controllers/channels.go | 21 +-- remote/models/phoenix/channel.go | 16 ++- remote/payloads/channel_payloads.go | 7 +- remote/responses/channel.go | 35 ++--- remote/services/db.go | 26 +++- remote/services/queries.go | 121 +++++++++++++++--- 7 files changed, 167 insertions(+), 60 deletions(-) diff --git a/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql b/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql index af40753fe1..aeb39f3896 100644 --- a/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql +++ b/phoenix-scala/sql/V5.20170619200953__create_channels_table.sql @@ -1,5 +1,6 @@ create table channels ( id serial primary key, + intelligence_channel_id integer not null, scope exts.ltree not null, name generic_string not null, purchase_location integer not null, diff --git a/remote/controllers/channels.go b/remote/controllers/channels.go index aa8fe62985..4c3733c60e 100644 --- a/remote/controllers/channels.go +++ b/remote/controllers/channels.go @@ -3,8 +3,6 @@ package controllers import ( "net/http" - "github.com/FoxComm/highlander/remote/models/ic" - "github.com/FoxComm/highlander/remote/models/phoenix" "github.com/FoxComm/highlander/remote/payloads" "github.com/FoxComm/highlander/remote/responses" "github.com/FoxComm/highlander/remote/services" @@ -22,14 +20,11 @@ func NewChannels(dbs *services.RemoteDBs) *Channels { // GetChannel finds a single channel by its ID. func (ctrl *Channels) GetChannel(id int) ControllerFunc { return func() (*responses.Response, failures.Failure) { - icChannel := &ic.Channel{} - phxChannel := &phoenix.Channel{} - - if err := services.FindChannelByID(ctrl.dbs, id, icChannel, phxChannel); err != nil { - return nil, err + resp, fail := services.FindChannelByID(ctrl.dbs, id) + if fail != nil { + return nil, fail } - resp := responses.NewChannel(icChannel, phxChannel, []string{}) return responses.NewResponse(http.StatusOK, resp), nil } } @@ -37,14 +32,11 @@ func (ctrl *Channels) GetChannel(id int) ControllerFunc { // CreateChannel creates a new channel. func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerFunc { return func() (*responses.Response, failures.Failure) { - icChannel := payload.IntelligenceModel() - phxChannel := payload.PhoenixModel() - - if err := services.InsertChannel(ctrl.dbs, icChannel, phxChannel, payload.Hosts); err != nil { - return nil, err + resp, fail := services.InsertChannel(ctrl.dbs, payload) + if fail != nil { + return nil, fail } - resp := responses.NewChannel(icChannel, phxChannel, payload.Hosts) return responses.NewResponse(http.StatusCreated, resp), nil } } @@ -52,6 +44,7 @@ func (ctrl *Channels) CreateChannel(payload *payloads.CreateChannel) ControllerF // UpdateChannel updates an existing channel. func (ctrl *Channels) UpdateChannel(id int, payload *payloads.UpdateChannel) ControllerFunc { return func() (*responses.Response, failures.Failure) { + // existingPhxChannel := &phoenix.Channel{} // if err := services.FindChannelByID(ctrl.dbs, id, existingPhxChannel); err != nil { // return nil, err diff --git a/remote/models/phoenix/channel.go b/remote/models/phoenix/channel.go index 44fa99c86b..524b1803ce 100644 --- a/remote/models/phoenix/channel.go +++ b/remote/models/phoenix/channel.go @@ -9,17 +9,19 @@ import ( // Channel represents an avenue for purchasing on the Fox Platform. This could // be a website (theperfectgourmet.com), third-party (Amazon), or sale type (B2B). type Channel struct { - ID int - Scope string - Name string - PurchaseLocation int - CreatedAt time.Time - UpdatedAt time.Time + ID int + IntelligenceChannelID int + Scope string + Name string + PurchaseLocation int + CreatedAt time.Time + UpdatedAt time.Time } func NewChannel(scope string, name string, purchaseLocation int) *Channel { return &Channel{ - ID: 0, + ID: 0, + IntelligenceChannelID: 0, Scope: scope, Name: name, PurchaseLocation: purchaseLocation, diff --git a/remote/payloads/channel_payloads.go b/remote/payloads/channel_payloads.go index 04421d0677..de817a282b 100644 --- a/remote/payloads/channel_payloads.go +++ b/remote/payloads/channel_payloads.go @@ -67,9 +67,10 @@ func (c CreateChannel) PhoenixModel() *phoenix.Channel { // UpdateChannel is the structure of payload needed to update a channel. type UpdateChannel struct { - Name *string `json:"name"` - PurchaseOnFox *bool `json:"purchaseOnFox"` - CatalogID *int64 `json:"catalogId"` + Name *string `json:"name"` + Hosts *[]string `json:"hosts"` + PurchaseOnFox *bool `json:"purchaseOnFox"` + CatalogID *int64 `json:"catalogId"` } // Validate ensures that they payload has the correct format. diff --git a/remote/responses/channel.go b/remote/responses/channel.go index e03b2aeada..9942c4b6c1 100644 --- a/remote/responses/channel.go +++ b/remote/responses/channel.go @@ -8,25 +8,28 @@ import ( ) type Channel struct { - ID int `json:"id"` - OrganizationID int `json:"organizationId"` - RiverRockChannelID int `json:"riverRockChannelId"` - Name string `json:"name"` - PurchaseOnFox bool `json:"purchaseOnFox"` - Hosts []string `json:"hosts"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID int `json:"id"` + OrganizationID int `json:"organizationId"` + Name string `json:"name"` + PurchaseOnFox bool `json:"purchaseOnFox"` + Hosts []string `json:"hosts"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } -func NewChannel(icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) *Channel { +func NewChannel(icChannel *ic.Channel, phxChannel *phoenix.Channel, hostMaps []*ic.HostMap) *Channel { + hosts := make([]string, len(hostMaps)) + for idx, hostMap := range hostMaps { + hosts[idx] = hostMap.Host + } + c := Channel{ - ID: phxChannel.ID, - OrganizationID: icChannel.OrganizationID, - RiverRockChannelID: icChannel.ID, - Name: phxChannel.Name, - Hosts: hosts, - CreatedAt: phxChannel.CreatedAt, - UpdatedAt: phxChannel.UpdatedAt, + ID: phxChannel.ID, + OrganizationID: icChannel.OrganizationID, + Name: phxChannel.Name, + Hosts: hosts, + CreatedAt: phxChannel.CreatedAt, + UpdatedAt: phxChannel.UpdatedAt, } if phxChannel.PurchaseLocation == phoenix.PurchaseOnFox { diff --git a/remote/services/db.go b/remote/services/db.go index 87791b1179..e689bf69be 100644 --- a/remote/services/db.go +++ b/remote/services/db.go @@ -2,6 +2,7 @@ package services import ( "fmt" + "reflect" "github.com/FoxComm/highlander/remote/utils" "github.com/FoxComm/highlander/remote/utils/failures" @@ -83,25 +84,42 @@ func (r RemoteDB) CreateWithTable(model interface{}, table string) failures.Fail return failures.New(r.db.Table(table).Create(model).Error) } -func (r RemoteDB) FindByID(table string, id int, model interface{}) failures.Failure { - return r.FindByIDWithFailure(table, id, model, failures.FailureNotFound) +func (r RemoteDB) FindByID(id int, model interface{}) failures.Failure { + return r.FindByIDWithFailure(id, model, failures.FailureNotFound) } -func (r RemoteDB) FindByIDWithFailure(table string, id int, model interface{}, failure int) failures.Failure { +func (r RemoteDB) FindByIDWithFailure(id int, model interface{}, failure int) failures.Failure { res := r.db.First(model, id) if res.RecordNotFound() { - err := fmt.Errorf("%s with id %d was not found", table, id) + err := fmt.Errorf("%s with id %d was not found", typeName(model), id) return failures.NewGeneralFailure(err, failure) } return failures.New(res.Error) } +func (r RemoteDB) FindWhere(field string, value interface{}, models interface{}) failures.Failure { + res := r.db.Where(fmt.Sprintf("%s = ?", field), value).Find(models) + return failures.New(res.Error) +} + func (r RemoteDB) Save(model interface{}) failures.Failure { return failures.New(r.db.Save(model).Error) } +func (r RemoteDB) Delete(model interface{}) failures.Failure { + return failures.New(r.db.Delete(model).Error) +} + func (r RemoteDB) Ping() error { return r.db.DB().Ping() } + +func typeName(i interface{}) string { + if t := reflect.TypeOf(i); t.Kind() == reflect.Ptr { + return t.Elem().Name() + } else { + return t.Name() + } +} diff --git a/remote/services/queries.go b/remote/services/queries.go index 938367fc66..919dad1a3a 100644 --- a/remote/services/queries.go +++ b/remote/services/queries.go @@ -3,55 +3,144 @@ package services import ( "github.com/FoxComm/highlander/remote/models/ic" "github.com/FoxComm/highlander/remote/models/phoenix" + "github.com/FoxComm/highlander/remote/payloads" + "github.com/FoxComm/highlander/remote/responses" "github.com/FoxComm/highlander/remote/utils/failures" ) -func FindChannelByID(dbs *RemoteDBs, id int, icChannel *ic.Channel, phxChannel *phoenix.Channel) failures.Failure { - if fail := dbs.Phx().FindByID("channel", id, phxChannel); fail != nil { - return fail +func FindChannelByID(dbs *RemoteDBs, id int) (*responses.Channel, failures.Failure) { + var phxChannel *phoenix.Channel + if fail := dbs.Phx().FindByID(id, phxChannel); fail != nil { + return nil, fail } - if fail := dbs.IC().FindByIDWithFailure("channel", phxChannel.RiverRockChannelID, icChannel, failures.FailureBadRequest); fail != nil { - return fail + var icChannel *ic.Channel + fail := dbs.IC().FindByIDWithFailure( + phxChannel.IntelligenceChannelID, + icChannel, + failures.FailureBadRequest) + + if fail != nil { + return nil, fail + } + + var hostMaps []*ic.HostMap + if fail := dbs.IC().FindWhere("channel_id", icChannel.ID, &hostMaps); fail != nil { + return nil, fail } - return nil + return responses.NewChannel(icChannel, phxChannel, hostMaps), nil } -func InsertChannel(dbs *RemoteDBs, icChannel *ic.Channel, phxChannel *phoenix.Channel, hosts []string) failures.Failure { +func InsertChannel(dbs *RemoteDBs, payload *payloads.CreateChannel) (*responses.Channel, failures.Failure) { + icChannel := payload.IntelligenceModel() + phxChannel := payload.PhoenixModel() + hostMaps := icChannel.HostMaps(payload.Hosts, phxChannel.Scope) + var phxOrganization phoenix.Organization txn := dbs.Begin() - if fail := txn.Phx().FindByIDWithFailure("organization", icChannel.OrganizationID, &phxOrganization, failures.FailureBadRequest); fail != nil { + if fail := txn.Phx().FindByIDWithFailure(icChannel.OrganizationID, &phxOrganization, failures.FailureBadRequest); fail != nil { txn.Rollback() - return fail + return nil, fail } if fail := txn.Phx().Create(phxChannel); fail != nil { txn.Rollback() - return fail + return nil, fail } if fail := txn.IC().Create(icChannel); fail != nil { txn.Rollback() - return fail + return nil, fail } - hostMaps := icChannel.HostMaps(hosts, phxChannel.Scope) // We have to iterate through each insert manually because of a limitation in // Gorm. Since there will rarely be many hosts created at a time, this should // be a workable solution for now. for idx := range hostMaps { if fail := txn.IC().CreateWithTable(hostMaps[idx], "host_map"); fail != nil { txn.Rollback() - return fail + return nil, fail } } - return txn.Commit() + if fail := txn.Commit(); fail != nil { + return nil, fail + } + + return responses.NewChannel(icChannel, phxChannel, hostMaps), nil } -func UpdateChannel(dbs *RemoteDBs, phxChannel *phoenix.Channel) failures.Failure { - return dbs.Phx().Save(phxChannel) +func compareHosts(currentHMs []*ic.HostMap, newHMs []string) ([]*ic.HostMap, []*ic.HostMap) { + existing := map[string]*ic.HostMap{} + for _, host := range currentHMs { + existing[host.Host] = host + } + + toAdd := []*ic.HostMap{} + for _, newHost := range newHMs { + if _, ok := existing[newHost]; !ok { + hm := &ic.HostMap{Host: newHost} + toAdd = append(toAdd, hm) + delete(existing, newHost) + } + } + + toRemove := []*ic.HostMap{} + for _, oldHost := range existing { + toRemove = append(toRemove, oldHost) + } + + return toAdd, toRemove +} + +func UpdateChannel(dbs *RemoteDBs, id int, payload *payloads.UpdateChannel) (*responses.Channel, failures.Failure) { + var origPhx *phoenix.Channel + if fail := dbs.Phx().FindByID(id, origPhx); fail != nil { + return nil, fail + } + + txn := dbs.Begin() + + newPhx := payload.PhoenixModel(origPhx) + if fail := txn.Phx().Save(newPhx); fail != nil { + txn.Rollback() + return fail + } + + if payload.Hosts != nil { + var origHost []*ic.HostMap + + } + + if fail := dbs.Commit(); fail != nil { + return nil, fail + } + + // if payload.Hosts != nil { + // newHost := *(payload.Hosts) + // toAdd, toDelete := compareHosts(origHost, newHost) + + // for _, host := range toAdd { + // host.ChannelID = origIC.ID + // host.Scope = newPhx.Scope + + // if fail := txn.IC().CreateWithTable(host, "host_map"); fail != nil { + // txn.Rollback() + // return fail + // } + // } + + // for _, host := range toDelete { + // if fail := txn.IC().Delete(host); fail != nil { + // txn.Rollback() + // return fail + // } + // } + // } + + // return dbs.Commit() + return nil, nil }