MM-9728: Online migration for advanced permissions phase 2 (#8744)

* MM-9728: Online migration for advanced permissions phase 2

* Add unit tests for new store functions.

* Move migration specific code to own file.

* Add migration state function test.

* Style fixes.

* Add i18n strings.

* Fix mocks.

* Add TestMain to migrations package tests.

* Fix typo.

* Fix review comments.

* Fix up the "Check if migration is done" check to actually work.
Этот коммит содержится в:
George Goldberg
2018-05-14 15:59:04 +01:00
коммит произвёл GitHub
родитель 91557bbd97
Коммит 51bd710ecd
28 изменённых файлов: 1586 добавлений и 4 удалений

Просмотреть файл

@@ -12,6 +12,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
) )
@@ -1890,6 +1892,17 @@ func TestUpdateChannelScheme(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
// Un-mark the migration at the end of the test.
defer func() {
res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
}()
team := &model.Team{ team := &model.Team{
DisplayName: "Name", DisplayName: "Name",
Description: "Some description", Description: "Some description",

Просмотреть файл

@@ -18,6 +18,11 @@ func TestCreateScheme(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
// Basic test of creating a team scheme. // Basic test of creating a team scheme.
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
@@ -113,6 +118,21 @@ func TestCreateScheme(t *testing.T) {
} }
_, r6 := th.SystemAdminClient.CreateScheme(scheme6) _, r6 := th.SystemAdminClient.CreateScheme(scheme6)
CheckNotImplementedStatus(t, r6) CheckNotImplementedStatus(t, r6)
// Mark the migration as not done.
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
th.LoginSystemAdmin()
th.App.SetLicense(model.NewTestLicense(""))
scheme7 := &model.Scheme{
Name: model.NewId(),
Description: model.NewId(),
Scope: model.SCHEME_SCOPE_TEAM,
}
_, r7 := th.SystemAdminClient.CreateScheme(scheme7)
CheckInternalErrorStatus(t, r7)
} }
func TestGetScheme(t *testing.T) { func TestGetScheme(t *testing.T) {
@@ -128,9 +148,17 @@ func TestGetScheme(t *testing.T) {
Scope: model.SCHEME_SCOPE_TEAM, Scope: model.SCHEME_SCOPE_TEAM,
} }
// Mark the migration as done while we create the scheme.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
s1, r1 := th.SystemAdminClient.CreateScheme(scheme1) s1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
CheckNoError(t, r1) CheckNoError(t, r1)
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
assert.Equal(t, s1.Name, scheme1.Name) assert.Equal(t, s1.Name, scheme1.Name)
assert.Equal(t, s1.Description, scheme1.Description) assert.Equal(t, s1.Description, scheme1.Description)
assert.NotZero(t, s1.CreateAt) assert.NotZero(t, s1.CreateAt)
@@ -184,11 +212,19 @@ func TestGetSchemes(t *testing.T) {
Scope: model.SCHEME_SCOPE_CHANNEL, Scope: model.SCHEME_SCOPE_CHANNEL,
} }
// Mark the migration as done while we create the scheme.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
_, r1 := th.SystemAdminClient.CreateScheme(scheme1) _, r1 := th.SystemAdminClient.CreateScheme(scheme1)
CheckNoError(t, r1) CheckNoError(t, r1)
_, r2 := th.SystemAdminClient.CreateScheme(scheme2) _, r2 := th.SystemAdminClient.CreateScheme(scheme2)
CheckNoError(t, r2) CheckNoError(t, r2)
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
l3, r3 := th.SystemAdminClient.GetSchemes("", 0, 100) l3, r3 := th.SystemAdminClient.GetSchemes("", 0, 100)
CheckNoError(t, r3) CheckNoError(t, r3)
@@ -226,6 +262,11 @@ func TestGetTeamsForScheme(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done while we create the scheme.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
Description: model.NewId(), Description: model.NewId(),
@@ -234,6 +275,9 @@ func TestGetTeamsForScheme(t *testing.T) {
scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1) scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
CheckNoError(t, r1) CheckNoError(t, r1)
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
team1 := &model.Team{ team1 := &model.Team{
Name: GenerateTestUsername(), Name: GenerateTestUsername(),
DisplayName: "A Test Team", DisplayName: "A Test Team",
@@ -294,6 +338,10 @@ func TestGetTeamsForScheme(t *testing.T) {
_, ri4 := th.Client.GetTeamsForScheme(model.NewId(), 0, 100) _, ri4 := th.Client.GetTeamsForScheme(model.NewId(), 0, 100)
CheckForbiddenStatus(t, ri4) CheckForbiddenStatus(t, ri4)
// Mark the migration as done again while we create a scheme.
res = <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
scheme2 := &model.Scheme{ scheme2 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
Description: model.NewId(), Description: model.NewId(),
@@ -302,6 +350,9 @@ func TestGetTeamsForScheme(t *testing.T) {
scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2) scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2)
CheckNoError(t, rs2) CheckNoError(t, rs2)
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
_, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100) _, ri5 := th.SystemAdminClient.GetTeamsForScheme(scheme2.Id, 0, 100)
CheckBadRequestStatus(t, ri5) CheckBadRequestStatus(t, ri5)
} }
@@ -312,6 +363,11 @@ func TestGetChannelsForScheme(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done while we create the scheme.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
Description: model.NewId(), Description: model.NewId(),
@@ -320,6 +376,9 @@ func TestGetChannelsForScheme(t *testing.T) {
scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1) scheme1, r1 := th.SystemAdminClient.CreateScheme(scheme1)
CheckNoError(t, r1) CheckNoError(t, r1)
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
channel1 := &model.Channel{ channel1 := &model.Channel{
TeamId: model.NewId(), TeamId: model.NewId(),
DisplayName: "A Name", DisplayName: "A Name",
@@ -382,6 +441,10 @@ func TestGetChannelsForScheme(t *testing.T) {
_, ri4 := th.Client.GetChannelsForScheme(model.NewId(), 0, 100) _, ri4 := th.Client.GetChannelsForScheme(model.NewId(), 0, 100)
CheckForbiddenStatus(t, ri4) CheckForbiddenStatus(t, ri4)
// Mark the migration as done again while we create a scheme.
res = <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
scheme2 := &model.Scheme{ scheme2 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
Description: model.NewId(), Description: model.NewId(),
@@ -390,6 +453,9 @@ func TestGetChannelsForScheme(t *testing.T) {
scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2) scheme2, rs2 := th.SystemAdminClient.CreateScheme(scheme2)
CheckNoError(t, rs2) CheckNoError(t, rs2)
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
_, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100) _, ri5 := th.SystemAdminClient.GetChannelsForScheme(scheme2.Id, 0, 100)
CheckBadRequestStatus(t, ri5) CheckBadRequestStatus(t, ri5)
} }
@@ -400,6 +466,11 @@ func TestPatchScheme(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
// Basic test of creating a team scheme. // Basic test of creating a team scheme.
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
@@ -480,6 +551,16 @@ func TestPatchScheme(t *testing.T) {
th.App.SetLicense(nil) th.App.SetLicense(nil)
_, r11 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch) _, r11 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch)
CheckNotImplementedStatus(t, r11) CheckNotImplementedStatus(t, r11)
// Mark the migration as not done.
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
th.LoginSystemAdmin()
th.App.SetLicense(model.NewTestLicense(""))
_, r12 := th.SystemAdminClient.PatchScheme(s6.Id, schemePatch)
CheckInternalErrorStatus(t, r12)
} }
func TestDeleteScheme(t *testing.T) { func TestDeleteScheme(t *testing.T) {
@@ -489,6 +570,17 @@ func TestDeleteScheme(t *testing.T) {
t.Run("ValidTeamScheme", func(t *testing.T) { t.Run("ValidTeamScheme", func(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
// Un-mark the migration at the end of the test.
defer func() {
res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
}()
// Create a team scheme. // Create a team scheme.
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
@@ -515,7 +607,7 @@ func TestDeleteScheme(t *testing.T) {
assert.Zero(t, role4.DeleteAt) assert.Zero(t, role4.DeleteAt)
// Make sure this scheme is in use by a team. // Make sure this scheme is in use by a team.
res := <-th.App.Srv.Store.Team().Save(&model.Team{ res = <-th.App.Srv.Store.Team().Save(&model.Team{
Name: model.NewId(), Name: model.NewId(),
DisplayName: model.NewId(), DisplayName: model.NewId(),
Email: model.NewId() + "@nowhere.com", Email: model.NewId() + "@nowhere.com",
@@ -571,6 +663,17 @@ func TestDeleteScheme(t *testing.T) {
t.Run("ValidChannelScheme", func(t *testing.T) { t.Run("ValidChannelScheme", func(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
// Un-mark the migration at the end of the test.
defer func() {
res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
}()
// Create a channel scheme. // Create a channel scheme.
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
@@ -591,7 +694,7 @@ func TestDeleteScheme(t *testing.T) {
assert.Zero(t, role4.DeleteAt) assert.Zero(t, role4.DeleteAt)
// Make sure this scheme is in use by a team. // Make sure this scheme is in use by a team.
res := <-th.App.Srv.Store.Channel().Save(&model.Channel{ res = <-th.App.Srv.Store.Channel().Save(&model.Channel{
TeamId: model.NewId(), TeamId: model.NewId(),
DisplayName: model.NewId(), DisplayName: model.NewId(),
Name: model.NewId(), Name: model.NewId(),
@@ -635,6 +738,11 @@ func TestDeleteScheme(t *testing.T) {
t.Run("FailureCases", func(t *testing.T) { t.Run("FailureCases", func(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
scheme1 := &model.Scheme{ scheme1 := &model.Scheme{
Name: model.NewId(), Name: model.NewId(),
Description: model.NewId(), Description: model.NewId(),
@@ -660,5 +768,14 @@ func TestDeleteScheme(t *testing.T) {
th.App.SetLicense(nil) th.App.SetLicense(nil)
_, r5 := th.SystemAdminClient.DeleteScheme(s1.Id) _, r5 := th.SystemAdminClient.DeleteScheme(s1.Id)
CheckNotImplementedStatus(t, r5) CheckNotImplementedStatus(t, r5)
// Test with migration not being done.
res = <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
th.App.SetLicense(model.NewTestLicense(""))
_, r6 := th.SystemAdminClient.DeleteScheme(s1.Id)
CheckInternalErrorStatus(t, r6)
}) })
} }

Просмотреть файл

@@ -2059,6 +2059,17 @@ func TestUpdateTeamScheme(t *testing.T) {
th.App.SetLicense(model.NewTestLicense("")) th.App.SetLicense(model.NewTestLicense(""))
// Mark the migration as done.
<-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
res := <-th.App.Srv.Store.System().Save(&model.System{Name: model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2, Value: "true"})
assert.Nil(t, res.Err)
// Un-mark the migration at the end of the test.
defer func() {
res := <-th.App.Srv.Store.System().PermanentDeleteByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2)
assert.Nil(t, res.Err)
}()
team := &model.Team{ team := &model.Team{
DisplayName: "Name", DisplayName: "Name",
Description: "Some description", Description: "Some description",

Просмотреть файл

@@ -20,6 +20,7 @@ import (
"github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/einterfaces"
ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs" ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs"
"github.com/mattermost/mattermost-server/jobs" "github.com/mattermost/mattermost-server/jobs"
tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
"github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin/pluginenv" "github.com/mattermost/mattermost-server/plugin/pluginenv"
@@ -319,6 +320,12 @@ func RegisterJobsLdapSyncInterface(f func(*App) ejobs.LdapSyncInterface) {
jobsLdapSyncInterface = f jobsLdapSyncInterface = f
} }
var jobsMigrationsInterface func(*App) tjobs.MigrationsJobInterface
func RegisterJobsMigrationsJobInterface(f func(*App) tjobs.MigrationsJobInterface) {
jobsMigrationsInterface = f
}
var ldapInterface func(*App) einterfaces.LdapInterface var ldapInterface func(*App) einterfaces.LdapInterface
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) { func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
@@ -413,6 +420,9 @@ func (a *App) initJobs() {
if jobsLdapSyncInterface != nil { if jobsLdapSyncInterface != nil {
a.Jobs.LdapSync = jobsLdapSyncInterface(a) a.Jobs.LdapSync = jobsLdapSyncInterface(a)
} }
if jobsMigrationsInterface != nil {
a.Jobs.Migrations = jobsMigrationsInterface(a)
}
} }
func (a *App) DiagnosticId() string { func (a *App) DiagnosticId() string {

Просмотреть файл

@@ -3,7 +3,9 @@
package app package app
import "github.com/mattermost/mattermost-server/model" import (
"github.com/mattermost/mattermost-server/model"
)
func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) { func (a *App) GetScheme(id string) (*model.Scheme, *model.AppError) {
if result := <-a.Srv.Store.Scheme().Get(id); result.Err != nil { if result := <-a.Srv.Store.Scheme().Get(id); result.Err != nil {
@@ -109,7 +111,9 @@ func (a *App) GetChannelsForScheme(scheme *model.Scheme, offset int, limit int)
} }
func (a *App) IsPhase2MigrationCompleted() *model.AppError { func (a *App) IsPhase2MigrationCompleted() *model.AppError {
// TODO: Actually check the Phase 2 migration has completed before permitting these actions. if result := <-a.Srv.Store.System().GetByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2); result.Err != nil {
return result.Err
}
return nil return nil
} }

Просмотреть файл

@@ -47,6 +47,54 @@
"id": "September", "id": "September",
"translation": "September" "translation": "September"
}, },
{
"id": "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress",
"translation": "Migration failed due to invalid progress data."
},
{
"id": "migrations.worker.run_migration.unknown_key",
"translation": "Cannot run migration job due to unknown migration key."
},
{
"id": "store.sql_channel.migrate_channel_members.open_transaction.app_error",
"translation": "Failed to open the database transaction"
},
{
"id": "store.sql_channel.migrate_channel_members.select.app_error",
"translation": "Failed to select the batch of channel members"
},
{
"id": "store.sql_channel.migrate_channel_members.rollback_transaction.app_error",
"translation": "Failed to roll back the database transaction"
},
{
"id": "store.sql_channel.migrate_channel_members.update.app_error",
"translation": "Failed to update the channel member"
},
{
"id": "store.sql_channel.migrate_channel_members.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{
"id": "store.sql_team.migrate_team_members.open_transaction.app_error",
"translation": "Failed to open the database transaction"
},
{
"id": "store.sql_team.migrate_team_members.select.app_error",
"translation": " Failed to select the batch of team members"
},
{
"id": "store.sql_team.migrate_team_members.rollback_transaction.app_error",
"translation": "Failed to roll back the database transaction"
},
{
"id": "store.sql_team.migrate_team_members.update.app_error",
"translation": "Failed to update the team member"
},
{
"id": "store.sql_team.migrate_team_members.commit_transaction.app_error",
"translation": "Failed to commit the database transaction"
},
{ {
"id": "api.admin.add_certificate.no_file.app_error", "id": "api.admin.add_certificate.no_file.app_error",
"translation": "No file under 'certificate' in request." "translation": "No file under 'certificate' in request."

Просмотреть файл

@@ -4,3 +4,7 @@
package imports package imports
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty // This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty
import (
_ "github.com/mattermost/mattermost-server/migrations"
)

11
jobs/interfaces/migrations_interface.go Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package interfaces
import "github.com/mattermost/mattermost-server/model"
type MigrationsJobInterface interface {
MakeWorker() model.Worker
MakeScheduler() model.Scheduler
}

Просмотреть файл

@@ -106,6 +106,13 @@ func (srv *JobServer) SetJobCanceled(job *model.Job) *model.AppError {
return result.Err return result.Err
} }
func (srv *JobServer) UpdateInProgressJobData(job *model.Job) *model.AppError {
job.Status = model.JOB_STATUS_IN_PROGRESS
job.LastActivityAt = model.GetMillis()
result := <-srv.Store.Job().UpdateOptimistically(job, model.JOB_STATUS_IN_PROGRESS)
return result.Err
}
func (srv *JobServer) RequestCancellation(jobId string) *model.AppError { func (srv *JobServer) RequestCancellation(jobId string) *model.AppError {
if result := <-srv.Store.Job().UpdateStatusOptimistically(jobId, model.JOB_STATUS_PENDING, model.JOB_STATUS_CANCELED); result.Err != nil { if result := <-srv.Store.Job().UpdateStatusOptimistically(jobId, model.JOB_STATUS_PENDING, model.JOB_STATUS_CANCELED); result.Err != nil {
return result.Err return result.Err

Просмотреть файл

@@ -107,6 +107,13 @@ func (watcher *Watcher) PollAndNotify() {
default: default:
} }
} }
} else if job.Type == model.JOB_TYPE_MIGRATIONS {
if watcher.workers.Migrations != nil {
select {
case watcher.workers.Migrations.JobChannel() <- *job:
default:
}
}
} }
} }
} }

Просмотреть файл

@@ -50,6 +50,10 @@ func (srv *JobServer) InitSchedulers() *Schedulers {
schedulers.schedulers = append(schedulers.schedulers, ldapSyncInterface.MakeScheduler()) schedulers.schedulers = append(schedulers.schedulers, ldapSyncInterface.MakeScheduler())
} }
if migrationsInterface := srv.Migrations; migrationsInterface != nil {
schedulers.schedulers = append(schedulers.schedulers, migrationsInterface.MakeScheduler())
}
schedulers.nextRunTimes = make([]*time.Time, len(schedulers.schedulers)) schedulers.nextRunTimes = make([]*time.Time, len(schedulers.schedulers))
return schedulers return schedulers
} }

Просмотреть файл

@@ -5,6 +5,7 @@ package jobs
import ( import (
ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs" ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs"
tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store"
) )
@@ -34,6 +35,7 @@ type JobServer struct {
ElasticsearchAggregator ejobs.ElasticsearchAggregatorInterface ElasticsearchAggregator ejobs.ElasticsearchAggregatorInterface
ElasticsearchIndexer ejobs.ElasticsearchIndexerInterface ElasticsearchIndexer ejobs.ElasticsearchIndexerInterface
LdapSync ejobs.LdapSyncInterface LdapSync ejobs.LdapSyncInterface
Migrations tjobs.MigrationsJobInterface
} }
func NewJobServer(configService ConfigService, store store.Store) *JobServer { func NewJobServer(configService ConfigService, store store.Store) *JobServer {

Просмотреть файл

@@ -20,6 +20,7 @@ type Workers struct {
ElasticsearchIndexing model.Worker ElasticsearchIndexing model.Worker
ElasticsearchAggregation model.Worker ElasticsearchAggregation model.Worker
LdapSync model.Worker LdapSync model.Worker
Migrations model.Worker
listenerId string listenerId string
} }
@@ -50,6 +51,10 @@ func (srv *JobServer) InitWorkers() *Workers {
workers.LdapSync = ldapSyncInterface.MakeWorker() workers.LdapSync = ldapSyncInterface.MakeWorker()
} }
if migrationsInterface := srv.Migrations; migrationsInterface != nil {
workers.Migrations = migrationsInterface.MakeWorker()
}
return workers return workers
} }
@@ -77,6 +82,10 @@ func (workers *Workers) Start() *Workers {
go workers.LdapSync.Run() go workers.LdapSync.Run()
} }
if workers.Migrations != nil {
go workers.Migrations.Run()
}
go workers.Watcher.Start() go workers.Watcher.Start()
}) })
@@ -152,6 +161,10 @@ func (workers *Workers) Stop() *Workers {
workers.LdapSync.Stop() workers.LdapSync.Stop()
} }
if workers.Migrations != nil {
workers.Migrations.Stop()
}
mlog.Info("Stopped workers") mlog.Info("Stopped workers")
return workers return workers

106
migrations/advanced_permissions_phase_2.go Обычный файл
Просмотреть файл

@@ -0,0 +1,106 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package migrations
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/model"
)
type AdvancedPermissionsPhase2Progress struct {
CurrentTable string `json:"current_table"`
LastTeamId string `json:"last_team_id"`
LastChannelId string `json:"last_channel_id"`
LastUserId string `json:"last_user"`
}
func (p *AdvancedPermissionsPhase2Progress) ToJson() string {
b, _ := json.Marshal(p)
return string(b)
}
func AdvancedPermissionsPhase2ProgressFromJson(data io.Reader) *AdvancedPermissionsPhase2Progress {
var o *AdvancedPermissionsPhase2Progress
json.NewDecoder(data).Decode(&o)
return o
}
func (p *AdvancedPermissionsPhase2Progress) IsValid() bool {
if len(p.LastChannelId) != 26 {
return false
}
if len(p.LastTeamId) != 26 {
return false
}
if len(p.LastUserId) != 26 {
return false
}
switch p.CurrentTable {
case "TeamMembers":
case "ChannelMembers":
default:
return false
}
return true
}
func (worker *Worker) runAdvancedPermissionsPhase2Migration(lastDone string) (bool, string, *model.AppError) {
var progress *AdvancedPermissionsPhase2Progress
if len(lastDone) == 0 {
// Haven't started the migration yet.
progress = new(AdvancedPermissionsPhase2Progress)
progress.CurrentTable = "TeamMembers"
progress.LastChannelId = strings.Repeat("0", 26)
progress.LastTeamId = strings.Repeat("0", 26)
progress.LastUserId = strings.Repeat("0", 26)
} else {
progress = AdvancedPermissionsPhase2ProgressFromJson(strings.NewReader(lastDone))
if !progress.IsValid() {
return false, "", model.NewAppError("MigrationsWorker.runAdvancedPermissionsPhase2Migration", "migrations.worker.run_advanced_permissions_phase_2_migration.invalid_progress", map[string]interface{}{"progress": progress.ToJson()}, "", http.StatusInternalServerError)
}
}
if progress.CurrentTable == "TeamMembers" {
// Run a TeamMembers migration batch.
if result := <-worker.app.Srv.Store.Team().MigrateTeamMembers(progress.LastTeamId, progress.LastUserId); result.Err != nil {
return false, progress.ToJson(), result.Err
} else {
if result.Data == nil {
// We haven't progressed. That means that we've reached the end of this stage of the migration, and should now advance to the next stage.
progress.LastUserId = strings.Repeat("0", 26)
progress.CurrentTable = "ChannelMembers"
return false, progress.ToJson(), nil
}
data := result.Data.(map[string]string)
progress.LastTeamId = data["TeamId"]
progress.LastUserId = data["UserId"]
}
} else if progress.CurrentTable == "ChannelMembers" {
// Run a ChannelMembers migration batch.
if result := <-worker.app.Srv.Store.Channel().MigrateChannelMembers(progress.LastChannelId, progress.LastUserId); result.Err != nil {
return false, progress.ToJson(), result.Err
} else {
if result.Data == nil {
// We haven't progressed. That means we've reached the end of this final stage of the migration.
return true, progress.ToJson(), nil
}
data := result.Data.(map[string]string)
progress.LastChannelId = data["ChannelId"]
progress.LastUserId = data["UserId"]
}
}
return false, progress.ToJson(), nil
}

63
migrations/migrations.go Обычный файл
Просмотреть файл

@@ -0,0 +1,63 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package migrations
import (
"github.com/mattermost/mattermost-server/app"
tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
const (
MIGRATION_STATE_UNSCHEDULED = "unscheduled"
MIGRATION_STATE_IN_PROGRESS = "in_progress"
MIGRATION_STATE_COMPLETED = "completed"
JOB_DATA_KEY_MIGRATION = "migration_key"
JOB_DATA_KEY_MIGRATION_LAST_DONE = "last_done"
)
type MigrationsJobInterfaceImpl struct {
App *app.App
}
func init() {
app.RegisterJobsMigrationsJobInterface(func(a *app.App) tjobs.MigrationsJobInterface {
return &MigrationsJobInterfaceImpl{a}
})
}
func MakeMigrationsList() []string {
return []string{
model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2,
}
}
func GetMigrationState(migration string, store store.Store) (string, *model.Job, *model.AppError) {
if result := <-store.System().GetByName(migration); result.Err == nil {
return MIGRATION_STATE_COMPLETED, nil, nil
}
if result := <-store.Job().GetAllByType(model.JOB_TYPE_MIGRATIONS); result.Err != nil {
return "", nil, result.Err
} else {
for _, job := range result.Data.([]*model.Job) {
if key, ok := job.Data[JOB_DATA_KEY_MIGRATION]; ok {
if key != migration {
continue
}
switch job.Status {
case model.JOB_STATUS_IN_PROGRESS, model.JOB_STATUS_PENDING:
return MIGRATION_STATE_IN_PROGRESS, job, nil
default:
return MIGRATION_STATE_UNSCHEDULED, job, nil
}
}
}
}
return MIGRATION_STATE_UNSCHEDULED, nil, nil
}

140
migrations/migrations_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,140 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package migrations
import (
"flag"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store/storetest"
"github.com/mattermost/mattermost-server/utils"
)
func TestMain(m *testing.M) {
flag.Parse()
// Setup a global logger to catch tests logging outside of app context
// The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{
EnableConsole: true,
ConsoleJson: true,
ConsoleLevel: "error",
EnableFile: false,
}))
utils.TranslationsPreInit()
// In the case where a dev just wants to run a single test, it's faster to just use the default
// store.
if filter := flag.Lookup("test.run").Value.String(); filter != "" && filter != "." {
mlog.Info("-test.run used, not creating temporary containers")
os.Exit(m.Run())
}
status := 0
container, settings, err := storetest.NewMySQLContainer()
if err != nil {
panic(err)
}
UseTestStore(container, settings)
defer func() {
StopTestStore()
os.Exit(status)
}()
status = m.Run()
}
func TestGetMigrationState(t *testing.T) {
th := Setup()
defer th.TearDown()
migrationKey := model.NewId()
th.DeleteAllJobsByTypeAndMigrationKey(model.JOB_TYPE_MIGRATIONS, migrationKey)
// Test with no job yet.
state, job, err := GetMigrationState(migrationKey, th.App.Srv.Store)
assert.Nil(t, err)
assert.Nil(t, job)
assert.Equal(t, "unscheduled", state)
// Test with the system table showing the migration as done.
system := model.System{
Name: migrationKey,
Value: "true",
}
res1 := <-th.App.Srv.Store.System().Save(&system)
assert.Nil(t, res1.Err)
state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
assert.Nil(t, err)
assert.Nil(t, job)
assert.Equal(t, "completed", state)
res2 := <-th.App.Srv.Store.System().PermanentDeleteByName(migrationKey)
assert.Nil(t, res2.Err)
// Test with a job scheduled in "pending" state.
j1 := &model.Job{
Id: model.NewId(),
CreateAt: model.GetMillis(),
Data: map[string]string{
JOB_DATA_KEY_MIGRATION: migrationKey,
},
Status: model.JOB_STATUS_PENDING,
Type: model.JOB_TYPE_MIGRATIONS,
}
j1 = (<-th.App.Srv.Store.Job().Save(j1)).Data.(*model.Job)
state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
assert.Nil(t, err)
assert.Equal(t, j1.Id, job.Id)
assert.Equal(t, "in_progress", state)
// Test with a job scheduled in "in progress" state.
j2 := &model.Job{
Id: model.NewId(),
CreateAt: j1.CreateAt + 1,
Data: map[string]string{
JOB_DATA_KEY_MIGRATION: migrationKey,
},
Status: model.JOB_STATUS_IN_PROGRESS,
Type: model.JOB_TYPE_MIGRATIONS,
}
j2 = (<-th.App.Srv.Store.Job().Save(j2)).Data.(*model.Job)
state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
assert.Nil(t, err)
assert.Equal(t, j2.Id, job.Id)
assert.Equal(t, "in_progress", state)
// Test with a job scheduled in "error" state.
j3 := &model.Job{
Id: model.NewId(),
CreateAt: j2.CreateAt + 1,
Data: map[string]string{
JOB_DATA_KEY_MIGRATION: migrationKey,
},
Status: model.JOB_STATUS_ERROR,
Type: model.JOB_TYPE_MIGRATIONS,
}
j3 = (<-th.App.Srv.Store.Job().Save(j3)).Data.(*model.Job)
state, job, err = GetMigrationState(migrationKey, th.App.Srv.Store)
assert.Nil(t, err)
assert.Equal(t, j3.Id, job.Id)
assert.Equal(t, "unscheduled", state)
}

419
migrations/migrationstestlib.go Обычный файл
Просмотреть файл

@@ -0,0 +1,419 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package migrations
import (
"encoding/json"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/einterfaces"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/plugin"
"github.com/mattermost/mattermost-server/plugin/pluginenv"
"github.com/mattermost/mattermost-server/store"
"github.com/mattermost/mattermost-server/store/sqlstore"
"github.com/mattermost/mattermost-server/store/storetest"
"github.com/mattermost/mattermost-server/utils"
)
type TestHelper struct {
App *app.App
BasicTeam *model.Team
BasicUser *model.User
BasicUser2 *model.User
BasicChannel *model.Channel
BasicPost *model.Post
SystemAdminUser *model.User
tempConfigPath string
tempWorkspace string
pluginHooks map[string]plugin.Hooks
}
type persistentTestStore struct {
store.Store
}
func (*persistentTestStore) Close() {}
var testStoreContainer *storetest.RunningContainer
var testStore *persistentTestStore
var testStoreSqlSupplier *sqlstore.SqlSupplier
var testClusterInterface *FakeClusterInterface
// UseTestStore sets the container and corresponding settings to use for tests. Once the tests are
// complete (e.g. at the end of your TestMain implementation), you should call StopTestStore.
func UseTestStore(container *storetest.RunningContainer, settings *model.SqlSettings) {
testClusterInterface = &FakeClusterInterface{}
testStoreContainer = container
testStoreSqlSupplier = sqlstore.NewSqlSupplier(*settings, nil)
testStore = &persistentTestStore{store.NewLayeredStore(testStoreSqlSupplier, nil, testClusterInterface)}
}
func StopTestStore() {
if testStoreContainer != nil {
testStoreContainer.Stop()
testStoreContainer = nil
}
}
func setupTestHelper(enterprise bool) *TestHelper {
permConfig, err := os.Open(utils.FindConfigFile("config.json"))
if err != nil {
panic(err)
}
defer permConfig.Close()
tempConfig, err := ioutil.TempFile("", "")
if err != nil {
panic(err)
}
_, err = io.Copy(tempConfig, permConfig)
tempConfig.Close()
if err != nil {
panic(err)
}
options := []app.Option{app.ConfigFile(tempConfig.Name()), app.DisableConfigWatch}
if testStore != nil {
options = append(options, app.StoreOverride(testStore))
}
a, err := app.New(options...)
if err != nil {
panic(err)
}
th := &TestHelper{
App: a,
pluginHooks: make(map[string]plugin.Hooks),
tempConfigPath: tempConfig.Name(),
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.RateLimitSettings.Enable = false })
prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress
if testStore != nil {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
}
serverErr := th.App.StartServer()
if serverErr != nil {
panic(serverErr)
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
th.App.DoAdvancedPermissionsMigration()
th.App.Srv.Store.MarkSystemRanUnitTests()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true })
if enterprise {
th.App.SetLicense(model.NewTestLicense())
} else {
th.App.SetLicense(nil)
}
return th
}
func SetupEnterprise() *TestHelper {
return setupTestHelper(true)
}
func Setup() *TestHelper {
return setupTestHelper(false)
}
func (me *TestHelper) InitBasic() *TestHelper {
me.BasicTeam = me.CreateTeam()
me.BasicUser = me.CreateUser()
me.LinkUserToTeam(me.BasicUser, me.BasicTeam)
me.BasicUser2 = me.CreateUser()
me.LinkUserToTeam(me.BasicUser2, me.BasicTeam)
me.BasicChannel = me.CreateChannel(me.BasicTeam)
me.BasicPost = me.CreatePost(me.BasicChannel)
return me
}
func (me *TestHelper) InitSystemAdmin() *TestHelper {
me.SystemAdminUser = me.CreateUser()
me.App.UpdateUserRoles(me.SystemAdminUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_ADMIN_ROLE_ID, false)
me.SystemAdminUser, _ = me.App.GetUser(me.SystemAdminUser.Id)
return me
}
func (me *TestHelper) MakeEmail() string {
return "success_" + model.NewId() + "@simulator.amazonses.com"
}
func (me *TestHelper) CreateTeam() *model.Team {
id := model.NewId()
team := &model.Team{
DisplayName: "dn_" + id,
Name: "name" + id,
Email: "success+" + id + "@simulator.amazonses.com",
Type: model.TEAM_OPEN,
}
utils.DisableDebugLogForTest()
var err *model.AppError
if team, err = me.App.CreateTeam(team); err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
return team
}
func (me *TestHelper) CreateUser() *model.User {
id := model.NewId()
user := &model.User{
Email: "success+" + id + "@simulator.amazonses.com",
Username: "un_" + id,
Nickname: "nn_" + id,
Password: "Password1",
EmailVerified: true,
}
utils.DisableDebugLogForTest()
var err *model.AppError
if user, err = me.App.CreateUser(user); err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
return user
}
func (me *TestHelper) CreateChannel(team *model.Team) *model.Channel {
return me.createChannel(team, model.CHANNEL_OPEN)
}
func (me *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
id := model.NewId()
channel := &model.Channel{
DisplayName: "dn_" + id,
Name: "name_" + id,
Type: channelType,
TeamId: team.Id,
CreatorId: me.BasicUser.Id,
}
utils.DisableDebugLogForTest()
var err *model.AppError
if channel, err = me.App.CreateChannel(channel, true); err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
return channel
}
func (me *TestHelper) CreateDmChannel(user *model.User) *model.Channel {
utils.DisableDebugLogForTest()
var err *model.AppError
var channel *model.Channel
if channel, err = me.App.CreateDirectChannel(me.BasicUser.Id, user.Id); err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
return channel
}
func (me *TestHelper) CreatePost(channel *model.Channel) *model.Post {
id := model.NewId()
post := &model.Post{
UserId: me.BasicUser.Id,
ChannelId: channel.Id,
Message: "message_" + id,
CreateAt: model.GetMillis() - 10000,
}
utils.DisableDebugLogForTest()
var err *model.AppError
if post, err = me.App.CreatePost(post, channel, false); err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
return post
}
func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) {
utils.DisableDebugLogForTest()
err := me.App.JoinUserToTeam(team, user, "")
if err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
}
func (me *TestHelper) AddUserToChannel(user *model.User, channel *model.Channel) *model.ChannelMember {
utils.DisableDebugLogForTest()
member, err := me.App.AddUserToChannel(user, channel)
if err != nil {
mlog.Error(err.Error())
time.Sleep(time.Second)
panic(err)
}
utils.EnableDebugLogForTest()
return member
}
func (me *TestHelper) TearDown() {
me.App.Shutdown()
os.Remove(me.tempConfigPath)
if err := recover(); err != nil {
StopTestStore()
panic(err)
}
if me.tempWorkspace != "" {
os.RemoveAll(me.tempWorkspace)
}
}
type mockPluginSupervisor struct {
hooks plugin.Hooks
}
func (s *mockPluginSupervisor) Start(api plugin.API) error {
return s.hooks.OnActivate(api)
}
func (s *mockPluginSupervisor) Stop() error {
return nil
}
func (s *mockPluginSupervisor) Hooks() plugin.Hooks {
return s.hooks
}
func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks) {
if me.tempWorkspace == "" {
dir, err := ioutil.TempDir("", "apptest")
if err != nil {
panic(err)
}
me.tempWorkspace = dir
}
pluginDir := filepath.Join(me.tempWorkspace, "plugins")
webappDir := filepath.Join(me.tempWorkspace, "webapp")
me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) {
if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok {
return &mockPluginSupervisor{hooks}, nil
}
return pluginenv.DefaultSupervisorProvider(bundle)
})
me.pluginHooks[manifest.Id] = hooks
manifestCopy := *manifest
if manifestCopy.Backend == nil {
manifestCopy.Backend = &model.ManifestBackend{}
}
manifestBytes, err := json.Marshal(&manifestCopy)
if err != nil {
panic(err)
}
if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil {
panic(err)
}
if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil {
panic(err)
}
}
func (me *TestHelper) ResetRoleMigration() {
if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Roles"); err != nil {
panic(err)
}
testClusterInterface.sendClearRoleCacheMessage()
if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Systems where Name = :Name", map[string]interface{}{"Name": app.ADVANCED_PERMISSIONS_MIGRATION_KEY}); err != nil {
panic(err)
}
}
func (me *TestHelper) DeleteAllJobsByTypeAndMigrationKey(jobType string, migrationKey string) {
if res := <-me.App.Srv.Store.Job().GetAllByType(model.JOB_TYPE_MIGRATIONS); res.Err != nil {
panic(res.Err)
} else {
jobs := res.Data.([]*model.Job)
for _, job := range jobs {
if key, ok := job.Data[JOB_DATA_KEY_MIGRATION]; ok && key == migrationKey {
if res := <-me.App.Srv.Store.Job().Delete(job.Id); res.Err != nil {
panic(res.Err)
}
}
}
}
}
type FakeClusterInterface struct {
clusterMessageHandler einterfaces.ClusterMessageHandler
}
func (me *FakeClusterInterface) StartInterNodeCommunication() {}
func (me *FakeClusterInterface) StopInterNodeCommunication() {}
func (me *FakeClusterInterface) RegisterClusterMessageHandler(event string, crm einterfaces.ClusterMessageHandler) {
me.clusterMessageHandler = crm
}
func (me *FakeClusterInterface) GetClusterId() string { return "" }
func (me *FakeClusterInterface) IsLeader() bool { return false }
func (me *FakeClusterInterface) GetMyClusterInfo() *model.ClusterInfo { return nil }
func (me *FakeClusterInterface) GetClusterInfos() []*model.ClusterInfo { return nil }
func (me *FakeClusterInterface) SendClusterMessage(cluster *model.ClusterMessage) {}
func (me *FakeClusterInterface) NotifyMsg(buf []byte) {}
func (me *FakeClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model.AppError) {
return nil, nil
}
func (me *FakeClusterInterface) GetLogs(page, perPage int) ([]string, *model.AppError) {
return []string{}, nil
}
func (me *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError {
return nil
}
func (me *FakeClusterInterface) sendClearRoleCacheMessage() {
me.clusterMessageHandler(&model.ClusterMessage{
Event: model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES,
})
}

110
migrations/scheduler.go Обычный файл
Просмотреть файл

@@ -0,0 +1,110 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package migrations
import (
"time"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store"
)
const (
MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS = 3600000 // 1 hour
)
type Scheduler struct {
App *app.App
allMigrationsCompleted bool
}
func (m *MigrationsJobInterfaceImpl) MakeScheduler() model.Scheduler {
return &Scheduler{m.App, false}
}
func (scheduler *Scheduler) Name() string {
return "MigrationsScheduler"
}
func (scheduler *Scheduler) JobType() string {
return model.JOB_TYPE_MIGRATIONS
}
func (scheduler *Scheduler) Enabled(cfg *model.Config) bool {
return true
}
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
if scheduler.allMigrationsCompleted {
return nil
}
nextTime := time.Now().Add(60 * time.Second)
return &nextTime
}
func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) {
mlog.Debug("Scheduling Job", mlog.String("scheduler", scheduler.Name()))
// Work through the list of migrations in order. Schedule the first one that isn't done (assuming it isn't in progress already).
for _, key := range MakeMigrationsList() {
state, job, err := GetMigrationState(key, scheduler.App.Srv.Store)
if err != nil {
mlog.Error("Failed to determine status of migration: ", mlog.String("scheduler", scheduler.Name()), mlog.String("migration_key", key), mlog.String("error", err.Error()))
return nil, nil
}
if state == MIGRATION_STATE_IN_PROGRESS {
// Check the migration job isn't wedged.
if job != nil && job.LastActivityAt < model.GetMillis()-MIGRATION_JOB_WEDGED_TIMEOUT_MILLISECONDS {
mlog.Warn("Job appears to be wedged. Rescheduling another instance.", mlog.String("scheduler", scheduler.Name()), mlog.String("wedged_job_id", job.Id), mlog.String("migration_key", key))
if err := scheduler.App.Jobs.SetJobError(job, nil); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("scheduler", scheduler.Name()), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
return scheduler.createJob(key, job, scheduler.App.Srv.Store)
}
return nil, nil
}
if state == MIGRATION_STATE_COMPLETED {
// This migration is done. Continue to check the next.
continue
}
if state == MIGRATION_STATE_UNSCHEDULED {
mlog.Debug("Scheduling a new job for migration.", mlog.String("scheduler", scheduler.Name()), mlog.String("migration_key", key))
return scheduler.createJob(key, job, scheduler.App.Srv.Store)
}
mlog.Error("Unknown migration state. Not doing anything.", mlog.String("migration_state", state))
return nil, nil
}
// If we reached here, then there aren't any migrations left to run.
scheduler.allMigrationsCompleted = true
mlog.Debug("All migrations are complete.", mlog.String("scheduler", scheduler.Name()))
return nil, nil
}
func (scheduler *Scheduler) createJob(migrationKey string, lastJob *model.Job, store store.Store) (*model.Job, *model.AppError) {
var lastDone string
if lastJob != nil {
lastDone = lastJob.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE]
}
data := map[string]string{
JOB_DATA_KEY_MIGRATION: migrationKey,
JOB_DATA_KEY_MIGRATION_LAST_DONE: lastDone,
}
if job, err := scheduler.App.Jobs.CreateJob(model.JOB_TYPE_MIGRATIONS, data); err != nil {
return nil, err
} else {
return job, nil
}
}

166
migrations/worker.go Обычный файл
Просмотреть файл

@@ -0,0 +1,166 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package migrations
import (
"context"
"net/http"
"time"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/jobs"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
)
const (
TIME_BETWEEN_BATCHES = 100
)
type Worker struct {
name string
stop chan bool
stopped chan bool
jobs chan model.Job
jobServer *jobs.JobServer
app *app.App
}
func (m *MigrationsJobInterfaceImpl) MakeWorker() model.Worker {
worker := Worker{
name: "Migrations",
stop: make(chan bool, 1),
stopped: make(chan bool, 1),
jobs: make(chan model.Job),
jobServer: m.App.Jobs,
app: m.App,
}
return &worker
}
func (worker *Worker) Run() {
mlog.Debug("Worker started", mlog.String("worker", worker.name))
defer func() {
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
worker.stopped <- true
}()
for {
select {
case <-worker.stop:
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
return
case job := <-worker.jobs:
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
worker.DoJob(&job)
}
}
}
func (worker *Worker) Stop() {
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
worker.stop <- true
<-worker.stopped
}
func (worker *Worker) JobChannel() chan<- model.Job {
return worker.jobs
}
func (worker *Worker) DoJob(job *model.Job) {
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
mlog.Info("Worker experienced an error while trying to claim job",
mlog.String("worker", worker.name),
mlog.String("job_id", job.Id),
mlog.String("error", err.Error()))
return
} else if !claimed {
return
}
cancelCtx, cancelCancelWatcher := context.WithCancel(context.Background())
cancelWatcherChan := make(chan interface{}, 1)
go worker.app.Jobs.CancellationWatcher(cancelCtx, job.Id, cancelWatcherChan)
defer cancelCancelWatcher()
for {
select {
case <-cancelWatcherChan:
mlog.Debug("Worker: Job has been canceled via CancellationWatcher", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobCanceled(job)
return
case <-worker.stop:
mlog.Debug("Worker: Job has been canceled via Worker Stop", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobCanceled(job)
return
case <-time.After(TIME_BETWEEN_BATCHES * time.Millisecond):
done, progress, err := worker.runMigration(job.Data[JOB_DATA_KEY_MIGRATION], job.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE])
if err != nil {
mlog.Error("Worker: Failed to run migration", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
} else if done {
mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
worker.setJobSuccess(job)
return
} else {
job.Data[JOB_DATA_KEY_MIGRATION_LAST_DONE] = progress
if err := worker.app.Jobs.UpdateInProgressJobData(job); err != nil {
mlog.Error("Worker: Failed to update migration status data for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
return
}
}
}
}
}
func (worker *Worker) setJobSuccess(job *model.Job) {
if err := worker.app.Jobs.SetJobSuccess(job); err != nil {
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
worker.setJobError(job, err)
}
}
func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) {
if err := worker.app.Jobs.SetJobError(job, appError); err != nil {
mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
func (worker *Worker) setJobCanceled(job *model.Job) {
if err := worker.app.Jobs.SetJobCanceled(job); err != nil {
mlog.Error("Worker: Failed to mark job as canceled", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
}
}
// Return parameters:
// - whether the migration is completed on this run (true) or still incomplete (false).
// - the updated lastDone string for the migration.
// - any error which may have occurred while running the migration.
func (worker *Worker) runMigration(key string, lastDone string) (bool, string, *model.AppError) {
var done bool
var progress string
var err *model.AppError
switch key {
case model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2:
done, progress, err = worker.runAdvancedPermissionsPhase2Migration(lastDone)
default:
return false, "", model.NewAppError("MigrationsWorker.runMigration", "migrations.worker.run_migration.unknown_key", map[string]interface{}{"key": key}, "", http.StatusInternalServerError)
}
if done {
if result := <-worker.app.Srv.Store.System().Save(&model.System{Name: key, Value: "true"}); result.Err != nil {
return false, "", result.Err
}
}
return done, progress, err
}

Просмотреть файл

@@ -16,6 +16,7 @@ const (
JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing" JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing"
JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION = "elasticsearch_post_aggregation" JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION = "elasticsearch_post_aggregation"
JOB_TYPE_LDAP_SYNC = "ldap_sync" JOB_TYPE_LDAP_SYNC = "ldap_sync"
JOB_TYPE_MIGRATIONS = "migrations"
JOB_STATUS_PENDING = "pending" JOB_STATUS_PENDING = "pending"
JOB_STATUS_IN_PROGRESS = "in_progress" JOB_STATUS_IN_PROGRESS = "in_progress"
@@ -52,6 +53,7 @@ func (j *Job) IsValid() *AppError {
case JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION: case JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION:
case JOB_TYPE_LDAP_SYNC: case JOB_TYPE_LDAP_SYNC:
case JOB_TYPE_MESSAGE_EXPORT: case JOB_TYPE_MESSAGE_EXPORT:
case JOB_TYPE_MIGRATIONS:
default: default:
return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest) return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest)
} }

8
model/migration.go Обычный файл
Просмотреть файл

@@ -0,0 +1,8 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
const (
MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2 = "migration_advanced_permissions_phase_2"
)

Просмотреть файл

@@ -1739,3 +1739,71 @@ func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit
} }
}) })
} }
// This function does the Advanced Permissions Phase 2 migration for ChannelMember objects. It performs the migration
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
var transaction *gorp.Transaction
var err error
if transaction, err = s.GetMaster().Begin(); err != nil {
result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
var channelMembers []channelMember
if _, err := transaction.Select(&channelMembers, "SELECT * from ChannelMembers WHERE (ChannelId, UserId) > (:FromChannelId, :FromUserId) ORDER BY ChannelId, UserId LIMIT 100", map[string]interface{}{"FromChannelId": fromChannelId, "FromUserId": fromUserId}); err != nil {
result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.select.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if len(channelMembers) == 0 {
// No more channel members in query result means that the migration has finished.
return
}
for _, member := range channelMembers {
roles := strings.Fields(member.Roles)
var newRoles []string
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
for _, role := range roles {
if role == model.CHANNEL_ADMIN_ROLE_ID {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.CHANNEL_USER_ROLE_ID {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.Update(&member); err != nil {
if err2 := transaction.Rollback(); err2 != nil {
result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
return
}
result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.update.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
if err := transaction.Commit(); err != nil {
if err2 := transaction.Rollback(); err2 != nil {
result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
return
}
result.Err = model.NewAppError("SqlChannelStore.MigrateChannelMembers", "store.sql_channel.migrate_channel_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
data := make(map[string]string)
data["ChannelId"] = channelMembers[len(channelMembers)-1].ChannelId
data["UserId"] = channelMembers[len(channelMembers)-1].UserId
result.Data = data
})
}

Просмотреть файл

@@ -9,6 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/mattermost/gorp"
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store" "github.com/mattermost/mattermost-server/store"
) )
@@ -725,3 +726,71 @@ func (s SqlTeamStore) GetTeamsByScheme(schemeId string, offset int, limit int) s
} }
}) })
} }
// This function does the Advanced Permissions Phase 2 migration for TeamMember objects. It performs the migration
// in batches as a single transaction per batch to ensure consistency but to also minimise execution time to avoid
// causing unnecessary table locks. **THIS FUNCTION SHOULD NOT BE USED FOR ANY OTHER PURPOSE.** Executing this function
// *after* the new Schemes functionality has been used on an installation will have unintended consequences.
func (s SqlTeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
var transaction *gorp.Transaction
var err error
if transaction, err = s.GetMaster().Begin(); err != nil {
result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.open_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
var teamMembers []teamMember
if _, err := transaction.Select(&teamMembers, "SELECT * from TeamMembers WHERE (TeamId, UserId) > (:FromTeamId, :FromUserId) ORDER BY TeamId, UserId LIMIT 100", map[string]interface{}{"FromTeamId": fromTeamId, "FromUserId": fromUserId}); err != nil {
result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.select.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
if len(teamMembers) == 0 {
// No more team members in query result means that the migration has finished.
return
}
for _, member := range teamMembers {
roles := strings.Fields(member.Roles)
var newRoles []string
member.SchemeAdmin = sql.NullBool{Bool: false, Valid: true}
member.SchemeUser = sql.NullBool{Bool: false, Valid: true}
for _, role := range roles {
if role == model.TEAM_ADMIN_ROLE_ID {
member.SchemeAdmin = sql.NullBool{Bool: true, Valid: true}
} else if role == model.TEAM_USER_ROLE_ID {
member.SchemeUser = sql.NullBool{Bool: true, Valid: true}
} else {
newRoles = append(newRoles, role)
}
}
member.Roles = strings.Join(newRoles, " ")
if _, err := transaction.Update(&member); err != nil {
if err2 := transaction.Rollback(); err2 != nil {
result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
return
}
result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.update.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
}
if err := transaction.Commit(); err != nil {
if err2 := transaction.Rollback(); err2 != nil {
result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.rollback_transaction.app_error", nil, err2.Error(), http.StatusInternalServerError)
return
}
result.Err = model.NewAppError("SqlTeamStore.MigrateTeamMembers", "store.sql_team.migrate_team_members.commit_transaction.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
data := make(map[string]string)
data["TeamId"] = teamMembers[len(teamMembers)-1].TeamId
data["UserId"] = teamMembers[len(teamMembers)-1].UserId
result.Data = data
})
}

Просмотреть файл

@@ -105,6 +105,7 @@ type TeamStore interface {
RemoveAllMembersByUser(userId string) StoreChannel RemoveAllMembersByUser(userId string) StoreChannel
UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel UpdateLastTeamIconUpdate(teamId string, curTime int64) StoreChannel
GetTeamsByScheme(schemeId string, offset int, limit int) StoreChannel GetTeamsByScheme(schemeId string, offset int, limit int) StoreChannel
MigrateTeamMembers(fromTeamId string, fromUserId string) StoreChannel
} }
type ChannelStore interface { type ChannelStore interface {
@@ -163,6 +164,7 @@ type ChannelStore interface {
GetChannelUnread(channelId, userId string) StoreChannel GetChannelUnread(channelId, userId string) StoreChannel
ClearCaches() ClearCaches()
GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel GetChannelsByScheme(schemeId string, offset int, limit int) StoreChannel
MigrateChannelMembers(fromChannelId string, fromUserId string) StoreChannel
} }
type ChannelMemberHistoryStore interface { type ChannelMemberHistoryStore interface {

Просмотреть файл

@@ -5,6 +5,7 @@ package storetest
import ( import (
"sort" "sort"
"strings"
"testing" "testing"
"time" "time"
@@ -52,6 +53,7 @@ func TestChannelStore(t *testing.T, ss store.Store) {
t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) })
t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) })
t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) })
t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) })
} }
@@ -2254,3 +2256,76 @@ func testChannelStoreGetChannelsByScheme(t *testing.T, ss store.Store) {
d3 := res3.Data.(model.ChannelList) d3 := res3.Data.(model.ChannelList)
assert.Len(t, d3, 0) assert.Len(t, d3, 0)
} }
func testChannelStoreMigrateChannelMembers(t *testing.T, ss store.Store) {
s1 := model.NewId()
c1 := &model.Channel{
TeamId: model.NewId(),
DisplayName: "Name",
Name: model.NewId(),
Type: model.CHANNEL_OPEN,
SchemeId: &s1,
}
c1 = (<-ss.Channel().Save(c1, 100)).Data.(*model.Channel)
cm1 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "channel_admin channel_user",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm2 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "channel_user",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm3 := &model.ChannelMember{
ChannelId: c1.Id,
UserId: model.NewId(),
ExplicitRoles: "something_else",
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm1 = (<-ss.Channel().SaveMember(cm1)).Data.(*model.ChannelMember)
cm2 = (<-ss.Channel().SaveMember(cm2)).Data.(*model.ChannelMember)
cm3 = (<-ss.Channel().SaveMember(cm3)).Data.(*model.ChannelMember)
lastDoneChannelId := strings.Repeat("0", 26)
lastDoneUserId := strings.Repeat("0", 26)
for {
res := <-ss.Channel().MigrateChannelMembers(lastDoneChannelId, lastDoneUserId)
if assert.Nil(t, res.Err) {
if res.Data == nil {
break
}
data := res.Data.(map[string]string)
lastDoneChannelId = data["ChannelId"]
lastDoneUserId = data["UserId"]
}
}
ss.Channel().ClearCaches()
res1 := <-ss.Channel().GetMember(cm1.ChannelId, cm1.UserId)
assert.Nil(t, res1.Err)
cm1b := res1.Data.(*model.ChannelMember)
assert.Equal(t, "", cm1b.ExplicitRoles)
assert.True(t, cm1b.SchemeUser)
assert.True(t, cm1b.SchemeAdmin)
res2 := <-ss.Channel().GetMember(cm2.ChannelId, cm2.UserId)
assert.Nil(t, res2.Err)
cm2b := res2.Data.(*model.ChannelMember)
assert.Equal(t, "", cm2b.ExplicitRoles)
assert.True(t, cm2b.SchemeUser)
assert.False(t, cm2b.SchemeAdmin)
res3 := <-ss.Channel().GetMember(cm3.ChannelId, cm3.UserId)
assert.Nil(t, res3.Err)
cm3b := res3.Data.(*model.ChannelMember)
assert.Equal(t, "something_else", cm3b.ExplicitRoles)
assert.False(t, cm3b.SchemeUser)
assert.False(t, cm3b.SchemeAdmin)
}

Просмотреть файл

@@ -583,6 +583,22 @@ func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string)
return r0 return r0
} }
// MigrateChannelMembers provides a mock function with given fields: fromChannelId, fromUserId
func (_m *ChannelStore) MigrateChannelMembers(fromChannelId string, fromUserId string) store.StoreChannel {
ret := _m.Called(fromChannelId, fromUserId)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
r0 = rf(fromChannelId, fromUserId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
}
}
return r0
}
// PermanentDelete provides a mock function with given fields: channelId // PermanentDelete provides a mock function with given fields: channelId
func (_m *ChannelStore) PermanentDelete(channelId string) store.StoreChannel { func (_m *ChannelStore) PermanentDelete(channelId string) store.StoreChannel {
ret := _m.Called(channelId) ret := _m.Called(channelId)

Просмотреть файл

@@ -301,6 +301,22 @@ func (_m *TeamStore) GetTotalMemberCount(teamId string) store.StoreChannel {
return r0 return r0
} }
// MigrateTeamMembers provides a mock function with given fields: fromTeamId, fromUserId
func (_m *TeamStore) MigrateTeamMembers(fromTeamId string, fromUserId string) store.StoreChannel {
ret := _m.Called(fromTeamId, fromUserId)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, string) store.StoreChannel); ok {
r0 = rf(fromTeamId, fromUserId)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
}
}
return r0
}
// PermanentDelete provides a mock function with given fields: teamId // PermanentDelete provides a mock function with given fields: teamId
func (_m *TeamStore) PermanentDelete(teamId string) store.StoreChannel { func (_m *TeamStore) PermanentDelete(teamId string) store.StoreChannel {
ret := _m.Called(teamId) ret := _m.Called(teamId)

Просмотреть файл

@@ -4,6 +4,7 @@
package storetest package storetest
import ( import (
"strings"
"testing" "testing"
"time" "time"
@@ -39,6 +40,7 @@ func TestTeamStore(t *testing.T, ss store.Store) {
t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) }) t.Run("GetChannelUnreadsForTeam", func(t *testing.T) { testGetChannelUnreadsForTeam(t, ss) })
t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) }) t.Run("UpdateLastTeamIconUpdate", func(t *testing.T) { testUpdateLastTeamIconUpdate(t, ss) })
t.Run("GetTeamsByScheme", func(t *testing.T) { testGetTeamsByScheme(t, ss) }) t.Run("GetTeamsByScheme", func(t *testing.T) { testGetTeamsByScheme(t, ss) })
t.Run("MigrateTeamMembers", func(t *testing.T) { testTeamStoreMigrateTeamMembers(t, ss) })
} }
func testTeamStoreSave(t *testing.T, ss store.Store) { func testTeamStoreSave(t *testing.T, ss store.Store) {
@@ -1098,3 +1100,72 @@ func testGetTeamsByScheme(t *testing.T, ss store.Store) {
d3 := res3.Data.([]*model.Team) d3 := res3.Data.([]*model.Team)
assert.Len(t, d3, 0) assert.Len(t, d3, 0)
} }
func testTeamStoreMigrateTeamMembers(t *testing.T, ss store.Store) {
s1 := model.NewId()
t1 := &model.Team{
DisplayName: "Name",
Name: "z-z-z" + model.NewId() + "b",
Email: model.NewId() + "@nowhere.com",
Type: model.TEAM_OPEN,
InviteId: model.NewId(),
SchemeId: &s1,
}
t1 = store.Must(ss.Team().Save(t1)).(*model.Team)
tm1 := &model.TeamMember{
TeamId: t1.Id,
UserId: model.NewId(),
ExplicitRoles: "team_admin team_user",
}
tm2 := &model.TeamMember{
TeamId: t1.Id,
UserId: model.NewId(),
ExplicitRoles: "team_user",
}
tm3 := &model.TeamMember{
TeamId: t1.Id,
UserId: model.NewId(),
ExplicitRoles: "something_else",
}
tm1 = (<-ss.Team().SaveMember(tm1, -1)).Data.(*model.TeamMember)
tm2 = (<-ss.Team().SaveMember(tm2, -1)).Data.(*model.TeamMember)
tm3 = (<-ss.Team().SaveMember(tm3, -1)).Data.(*model.TeamMember)
lastDoneTeamId := strings.Repeat("0", 26)
lastDoneUserId := strings.Repeat("0", 26)
for {
res := <-ss.Team().MigrateTeamMembers(lastDoneTeamId, lastDoneUserId)
if assert.Nil(t, res.Err) {
if res.Data == nil {
break
}
data := res.Data.(map[string]string)
lastDoneTeamId = data["TeamId"]
lastDoneUserId = data["UserId"]
}
}
res1 := <-ss.Team().GetMember(tm1.TeamId, tm1.UserId)
assert.Nil(t, res1.Err)
tm1b := res1.Data.(*model.TeamMember)
assert.Equal(t, "", tm1b.ExplicitRoles)
assert.True(t, tm1b.SchemeUser)
assert.True(t, tm1b.SchemeAdmin)
res2 := <-ss.Team().GetMember(tm2.TeamId, tm2.UserId)
assert.Nil(t, res2.Err)
tm2b := res2.Data.(*model.TeamMember)
assert.Equal(t, "", tm2b.ExplicitRoles)
assert.True(t, tm2b.SchemeUser)
assert.False(t, tm2b.SchemeAdmin)
res3 := <-ss.Team().GetMember(tm3.TeamId, tm3.UserId)
assert.Nil(t, res3.Err)
tm3b := res3.Data.(*model.TeamMember)
assert.Equal(t, "something_else", tm3b.ExplicitRoles)
assert.False(t, tm3b.SchemeUser)
assert.False(t, tm3b.SchemeAdmin)
}