MM-43084 update product limits through webhook and websocket (#20076)

* Define interfaces for updating subscriptions and product limits from CWS webhook.
This commit is contained in:
Nathaniel Allred
2022-05-09 08:05:50 -05:00
committed by GitHub
parent c7fe5e9aab
commit 6f87eb67fc
15 changed files with 203 additions and 0 deletions
+16
View File
@@ -14,6 +14,7 @@ import (
"github.com/mattermost/mattermost-server/v6/audit"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
)
func (api *API) InitCloud() {
@@ -486,6 +487,21 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err = appErr
return
}
case model.EventTypeSubscriptionChanged:
// event.ProductLimits is nil if there was no change
if event.ProductLimits != nil {
if pluginsEnvironment := c.App.GetPluginsEnvironment(); pluginsEnvironment != nil {
pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
hooks.OnCloudLimitsUpdated(event.ProductLimits)
return true
}, plugin.OnCloudLimitsUpdatedID)
}
}
if err := c.App.Cloud().UpdateSubscriptionFromHook(event.ProductLimits, event.Subscription); err != nil {
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.subscription.update_error", nil, err.Error(), http.StatusInternalServerError)
return
}
c.Logger.Info("Updated subscription from webhook event")
default:
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
+6
View File
@@ -1179,3 +1179,9 @@ func (api *PluginAPI) RequestTrialLicense(requesterID string, users int, termsAc
return api.app.Channels().RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)
}
// GetCloudLimits returns any limits associated with the cloud instance
func (api *PluginAPI) GetCloudLimits() (*model.ProductLimits, error) {
limits, err := api.app.Cloud().GetCloudLimits("")
return limits, err
}
+44
View File
@@ -1302,3 +1302,47 @@ func TestHookOnSendDailyTelemetry(t *testing.T) {
require.True(t, hookCalled)
}
func TestHookOnCloudLimitsUpdated(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
tearDown, pluginIDs, _ := SetAppEnvironmentWithPlugins(t,
[]string{
`
package main
import (
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) OnCloudLimitsUpdated(_ *model.ProductLimits) {
return
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`}, th.App, th.NewPluginAPI)
defer tearDown()
require.Len(t, pluginIDs, 1)
pluginID := pluginIDs[0]
require.True(t, th.App.GetPluginsEnvironment().IsActive(pluginID))
hookCalled := false
th.App.GetPluginsEnvironment().RunMultiPluginHook(func(hooks plugin.Hooks) bool {
hooks.OnCloudLimitsUpdated(nil)
hookCalled = true
return hookCalled
}, plugin.OnCloudLimitsUpdatedID)
require.True(t, hookCalled)
}
+1
View File
@@ -10,6 +10,7 @@ import (
type CloudInterface interface {
GetCloudProducts(userID string, includeLegacyProducts bool) ([]*model.Product, error)
GetCloudLimits(userID string) (*model.ProductLimits, error)
UpdateSubscriptionFromHook(*model.ProductLimits, *model.Subscription) error
CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error)
ConfirmCustomerPayment(userID string, confirmRequest *model.ConfirmPaymentMethodRequest) error
+14
View File
@@ -292,3 +292,17 @@ func (_m *CloudInterface) UpdateCloudCustomerAddress(userID string, address *mod
return r0, r1
}
// UpdateSubscriptionFromHook provides a mock function with given fields: _a0, _a1
func (_m *CloudInterface) UpdateSubscriptionFromHook(_a0 *model.ProductLimits, _a1 *model.Subscription) error {
ret := _m.Called(_a0, _a1)
var r0 error
if rf, ok := ret.Get(0).(func(*model.ProductLimits, *model.Subscription) error); ok {
r0 = rf(_a0, _a1)
} else {
r0 = ret.Error(0)
}
return r0
}
+4
View File
@@ -475,6 +475,10 @@
"id": "api.cloud.request_error",
"translation": "Error processing request to CWS."
},
{
"id": "api.cloud.subscription.update_error",
"translation": "Error updating subscription from webhook."
},
{
"id": "api.command.admin_only.app_error",
"translation": "Integrations have been limited to admins only."
+3
View File
@@ -10,6 +10,7 @@ const (
EventTypeFailedPaymentNoCard = "failed-payment-no-card"
EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email"
EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email"
EventTypeSubscriptionChanged = "subscription-changed"
EventTypeTrialWillEnd = "trial-will-end"
EventTypeTrialEnded = "trial-ended"
)
@@ -166,6 +167,8 @@ type CWSWebhookPayload struct {
Event string `json:"event"`
FailedPayment *FailedPayment `json:"failed_payment"`
CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"`
ProductLimits *ProductLimits `json:"product_limits"`
Subscription *Subscription `json:"subscription"`
SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"`
}
+1
View File
@@ -71,6 +71,7 @@ const (
WebsocketWarnMetricStatusReceived = "warn_metric_status_received"
WebsocketWarnMetricStatusRemoved = "warn_metric_status_removed"
WebsocketEventCloudPaymentStatusUpdated = "cloud_payment_status_updated"
WebsocketEventCloudSubscriptionChanged = "cloud_subscription_changed"
WebsocketEventThreadUpdated = "thread_updated"
WebsocketEventThreadFollowChanged = "thread_follow_changed"
WebsocketEventThreadReadChanged = "thread_read_changed"
+5
View File
@@ -1147,6 +1147,11 @@ type API interface {
//
// Minimum server version: 5.36
RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError
// GetCloudLimits gets limits associated with a cloud workspace, if any
//
// Minimum server version: 7.0
GetCloudLimits() (*model.ProductLimits, error)
}
var handshake = plugin.HandshakeConfig{
+7
View File
@@ -1225,3 +1225,10 @@ func (api *apiTimerLayer) RequestTrialLicense(requesterID string, users int, ter
api.recordTime(startTime, "RequestTrialLicense", _returnsA == nil)
return _returnsA
}
func (api *apiTimerLayer) GetCloudLimits() (*model.ProductLimits, error) {
startTime := timePkg.Now()
_returnsA, _returnsB := api.apiImpl.GetCloudLimits()
api.recordTime(startTime, "GetCloudLimits", _returnsB == nil)
return _returnsA, _returnsB
}
+62
View File
@@ -774,6 +774,39 @@ func (s *hooksRPCServer) OnSendDailyTelemetry(args *Z_OnSendDailyTelemetryArgs,
return nil
}
func init() {
hookNameToId["OnCloudLimitsUpdated"] = OnCloudLimitsUpdatedID
}
type Z_OnCloudLimitsUpdatedArgs struct {
A *model.ProductLimits
}
type Z_OnCloudLimitsUpdatedReturns struct {
}
func (g *hooksRPCClient) OnCloudLimitsUpdated(limits *model.ProductLimits) {
_args := &Z_OnCloudLimitsUpdatedArgs{limits}
_returns := &Z_OnCloudLimitsUpdatedReturns{}
if g.implemented[OnCloudLimitsUpdatedID] {
if err := g.client.Call("Plugin.OnCloudLimitsUpdated", _args, _returns); err != nil {
g.log.Error("RPC call OnCloudLimitsUpdated to plugin failed.", mlog.Err(err))
}
}
}
func (s *hooksRPCServer) OnCloudLimitsUpdated(args *Z_OnCloudLimitsUpdatedArgs, returns *Z_OnCloudLimitsUpdatedReturns) error {
if hook, ok := s.impl.(interface {
OnCloudLimitsUpdated(limits *model.ProductLimits)
}); ok {
hook.OnCloudLimitsUpdated(args.A)
} else {
return encodableError(fmt.Errorf("Hook OnCloudLimitsUpdated called but not implemented."))
}
return nil
}
type Z_RegisterCommandArgs struct {
A *model.Command
}
@@ -5616,3 +5649,32 @@ func (s *apiRPCServer) RequestTrialLicense(args *Z_RequestTrialLicenseArgs, retu
}
return nil
}
type Z_GetCloudLimitsArgs struct {
}
type Z_GetCloudLimitsReturns struct {
A *model.ProductLimits
B error
}
func (g *apiRPCClient) GetCloudLimits() (*model.ProductLimits, error) {
_args := &Z_GetCloudLimitsArgs{}
_returns := &Z_GetCloudLimitsReturns{}
if err := g.client.Call("Plugin.GetCloudLimits", _args, _returns); err != nil {
log.Printf("RPC call to GetCloudLimits API failed: %s", err.Error())
}
return _returns.A, _returns.B
}
func (s *apiRPCServer) GetCloudLimits(args *Z_GetCloudLimitsArgs, returns *Z_GetCloudLimitsReturns) error {
if hook, ok := s.impl.(interface {
GetCloudLimits() (*model.ProductLimits, error)
}); ok {
returns.A, returns.B = hook.GetCloudLimits()
returns.B = encodableError(returns.B)
} else {
return encodableError(fmt.Errorf("API GetCloudLimits called but not implemented."))
}
return nil
}
+6
View File
@@ -42,6 +42,7 @@ const (
RunDataRetentionID = 24
OnInstallID = 25
OnSendDailyTelemetryID = 26
OnCloudLimitsUpdatedID = 27
TotalHooksID = iota
)
@@ -264,4 +265,9 @@ type Hooks interface {
//
// Minimum server version: 6.5
OnSendDailyTelemetry()
// OnCloudLimitsUpdated is invoked product limits change, for example when plan tiers change
//
// Minimum server version: 7.0
OnCloudLimitsUpdated(limits *model.ProductLimits)
}
+6
View File
@@ -206,3 +206,9 @@ func (hooks *hooksTimerLayer) OnSendDailyTelemetry() {
hooks.hooksImpl.OnSendDailyTelemetry()
hooks.recordTime(startTime, "OnSendDailyTelemetry", true)
}
func (hooks *hooksTimerLayer) OnCloudLimitsUpdated(limits *model.ProductLimits) {
startTime := timePkg.Now()
hooks.hooksImpl.OnCloudLimitsUpdated(limits)
hooks.recordTime(startTime, "OnCloudLimitsUpdated", true)
}
+23
View File
@@ -980,6 +980,29 @@ func (_m *API) GetChannelsForTeamForUser(teamID string, userID string, includeDe
return r0, r1
}
// GetCloudLimits provides a mock function with given fields:
func (_m *API) GetCloudLimits() (*model.ProductLimits, error) {
ret := _m.Called()
var r0 *model.ProductLimits
if rf, ok := ret.Get(0).(func() *model.ProductLimits); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ProductLimits)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCommand provides a mock function with given fields: commandID
func (_m *API) GetCommand(commandID string) (*model.Command, error) {
ret := _m.Called(commandID)
+5
View File
@@ -166,6 +166,11 @@ func (_m *Hooks) OnActivate() error {
return r0
}
// OnCloudLimitsUpdated provides a mock function with given fields: limits
func (_m *Hooks) OnCloudLimitsUpdated(limits *model.ProductLimits) {
_m.Called(limits)
}
// OnConfigurationChange provides a mock function with given fields:
func (_m *Hooks) OnConfigurationChange() error {
ret := _m.Called()