MM-11384: Add system install date information to the client config (#9218)

* MM-11384: Add system install date information to the client config

* Fixing translation text

* Fixes from Peer Review
Этот коммит содержится в:
Jesús Espino
2018-08-08 12:04:36 +02:00
коммит произвёл Carlos Tadeu Panato Junior
родитель 3b640cd51b
Коммит 6bf09e2c34
37 изменённых файлов: 203 добавлений и 38 удалений

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

@@ -11,6 +11,7 @@ import (
"net/http"
"path"
"reflect"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -212,6 +213,10 @@ func New(options ...Option) (outApp *App, outErr error) {
return nil, errors.Wrapf(err, "unable to ensure asymmetric signing key")
}
if err := app.ensureInstallationDate(); err != nil {
return nil, errors.Wrapf(err, "unable to ensure installation date")
}
app.EnsureDiagnosticId()
app.regenerateClientConfig()
@@ -740,3 +745,16 @@ func (a *App) StartElasticsearch() {
}
})
}
func (a *App) getSystemInstallDate() (int64, *model.AppError) {
result := <-a.Srv.Store.System().GetByName(model.SYSTEM_INSTALLATION_DATE_KEY)
if result.Err != nil {
return 0, result.Err
}
systemData := result.Data.(*model.System)
value, err := strconv.ParseInt(systemData.Value, 10, 64)
if err != nil {
return 0, model.NewAppError("getSystemInstallDate", "app.system_install_date.parse_int.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return value, nil
}

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

@@ -16,6 +16,7 @@ import (
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
@@ -208,6 +209,30 @@ func (a *App) ensureAsymmetricSigningKey() error {
return nil
}
func (a *App) ensureInstallationDate() error {
_, err := a.getSystemInstallDate()
if err == nil {
return nil
}
result := <-a.Srv.Store.User().InferSystemInstallDate()
var installationDate int64
if result.Err == nil && result.Data.(int64) > 0 {
installationDate = result.Data.(int64)
} else {
installationDate = utils.MillisFromTime(time.Now())
}
result = <-a.Srv.Store.System().SaveOrUpdate(&model.System{
Name: model.SYSTEM_INSTALLATION_DATE_KEY,
Value: strconv.FormatInt(installationDate, 10),
})
if result.Err != nil {
return result.Err
}
return nil
}
// AsymmetricSigningKey will return a private key that can be used for asymmetric signing.
func (a *App) AsymmetricSigningKey() *ecdsa.PrivateKey {
return a.asymmetricSigningKey
@@ -296,6 +321,10 @@ func (a *App) ClientConfigWithComputed() map[string]string {
// by the client.
respCfg["NoAccounts"] = strconv.FormatBool(a.IsFirstUserAccount())
respCfg["MaxPostSize"] = strconv.Itoa(a.MaxPostSize())
respCfg["InstallationDate"] = ""
if installationDate, err := a.getSystemInstallDate(); err == nil {
respCfg["InstallationDate"] = strconv.FormatInt(installationDate, 10)
}
return respCfg
}

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

@@ -4,11 +4,15 @@
package app
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/store/sqlstore"
"github.com/mattermost/mattermost-server/utils"
)
func TestConfigListener(t *testing.T) {
@@ -76,3 +80,80 @@ func TestClientConfigWithComputed(t *testing.T) {
t.Fatal("expected MaxPostSize in returned config")
}
}
func TestEnsureInstallationDate(t *testing.T) {
th := Setup()
defer th.TearDown()
tt := []struct {
Name string
PrevInstallationDate *int64
UsersCreationDates []int64
ExpectedInstallationDate *int64
}{
{
Name: "New installation: no users, no installation date",
PrevInstallationDate: nil,
UsersCreationDates: nil,
ExpectedInstallationDate: model.NewInt64(utils.MillisFromTime(time.Now())),
},
{
Name: "Old installation: users, no installation date",
PrevInstallationDate: nil,
UsersCreationDates: []int64{10000000000, 30000000000, 20000000000},
ExpectedInstallationDate: model.NewInt64(10000000000),
},
{
Name: "New installation, second run: no users, installation date",
PrevInstallationDate: model.NewInt64(80000000000),
UsersCreationDates: []int64{10000000000, 30000000000, 20000000000},
ExpectedInstallationDate: model.NewInt64(80000000000),
},
{
Name: "Old installation already updated: users, installation date",
PrevInstallationDate: model.NewInt64(90000000000),
UsersCreationDates: []int64{10000000000, 30000000000, 20000000000},
ExpectedInstallationDate: model.NewInt64(90000000000),
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
sqlStore := th.App.Srv.Store.User().(*sqlstore.SqlUserStore)
sqlStore.GetMaster().Exec("DELETE FROM Users")
var users []*model.User
for _, createAt := range tc.UsersCreationDates {
user := th.CreateUser()
user.CreateAt = createAt
sqlStore.GetMaster().Exec("UPDATE Users SET CreateAt = :CreateAt WHERE Id = :UserId", map[string]interface{}{"CreateAt": createAt, "UserId": user.Id})
users = append(users, user)
}
if tc.PrevInstallationDate == nil {
<-th.App.Srv.Store.System().PermanentDeleteByName(model.SYSTEM_INSTALLATION_DATE_KEY)
} else {
<-th.App.Srv.Store.System().SaveOrUpdate(&model.System{
Name: model.SYSTEM_INSTALLATION_DATE_KEY,
Value: strconv.FormatInt(*tc.PrevInstallationDate, 10),
})
}
err := th.App.ensureInstallationDate()
if tc.ExpectedInstallationDate == nil {
assert.Error(t, err)
} else {
assert.NoError(t, err)
result := <-th.App.Srv.Store.System().GetByName(model.SYSTEM_INSTALLATION_DATE_KEY)
assert.Nil(t, result.Err)
data, _ := result.Data.(*model.System)
value, _ := strconv.ParseInt(data.Value, 10, 64)
assert.True(t, *tc.ExpectedInstallationDate <= value && *tc.ExpectedInstallationDate+1000 >= value)
}
sqlStore.GetMaster().Exec("DELETE FROM Users")
})
}
}

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

@@ -3110,6 +3110,10 @@
"id": "app.schemes.is_phase_2_migration_completed.not_completed.app_error",
"translation": "This API endpoint is not accessible as required migrations have not yet completed."
},
{
"id": "app.system_install_date.parse_int.app_error",
"translation": "Failed to parse installation date"
},
{
"id": "app.team.join_user_to_team.max_accounts.app_error",
"translation": "This team has reached the maximum number of allowed accounts. Contact your systems administrator to set a higher limit."
@@ -5938,6 +5942,10 @@
"id": "store.sql_user.get_for_login.multiple_users",
"translation": "We found multiple users matching your credentials and were unable to log you in. Please contact an administrator."
},
{
"id": "store.sql_user.get_system_install_date.app_error",
"translation": "Unable to infer the system date based on the first user creation date."
},
{
"id": "store.sql_user.get_new_users.app_error",
"translation": "We encountered an error while finding the new users"

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

@@ -16,6 +16,7 @@ const (
SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
)
type System struct {

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

@@ -1316,3 +1316,14 @@ func (us SqlUserStore) ClearAllCustomRoleAssignments() store.StoreChannel {
}
})
}
func (us SqlUserStore) InferSystemInstallDate() store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
createAt, err := us.GetReplica().SelectInt("SELECT CreateAt FROM Users WHERE CreateAt IS NOT NULL ORDER BY CreateAt ASC LIMIT 1")
if err != nil {
result.Err = model.NewAppError("SqlUserStore.GetSystemInstallDate", "store.sql_user.get_system_install_date.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
result.Data = createAt
})
}

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

@@ -263,6 +263,7 @@ type UserStore interface {
GetProfilesNotInTeam(teamId string, offset int, limit int) StoreChannel
GetEtagForProfilesNotInTeam(teamId string) StoreChannel
ClearAllCustomRoleAssignments() StoreChannel
InferSystemInstallDate() StoreChannel
}
type SessionStore interface {

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
@@ -45,9 +45,9 @@ func (_m *ChannelStore) AnalyticsTypeCount(teamId string, channelType string) st
return r0
}
// AutocompleteInTeam provides a mock function with given fields: teamId, term
// AutocompleteInTeam provides a mock function with given fields: teamId, term, includeDeleted
func (_m *ChannelStore) AutocompleteInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel {
ret := _m.Called(teamId, term)
ret := _m.Called(teamId, term, includeDeleted)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, string, bool) store.StoreChannel); ok {
@@ -146,9 +146,9 @@ func (_m *ChannelStore) GetAll(teamId string) store.StoreChannel {
return r0
}
// GetAllChannelMembersForUser provides a mock function with given fields: userId, allowFromCache
// GetAllChannelMembersForUser provides a mock function with given fields: userId, allowFromCache, includeDeleted
func (_m *ChannelStore) GetAllChannelMembersForUser(userId string, allowFromCache bool, includeDeleted bool) store.StoreChannel {
ret := _m.Called(userId, allowFromCache)
ret := _m.Called(userId, allowFromCache, includeDeleted)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, bool, bool) store.StoreChannel); ok {
@@ -258,9 +258,9 @@ func (_m *ChannelStore) GetChannelUnread(channelId string, userId string) store.
return r0
}
// GetChannels provides a mock function with given fields: teamId, userId
// GetChannels provides a mock function with given fields: teamId, userId, includeDeleted
func (_m *ChannelStore) GetChannels(teamId string, userId string, includeDeleted bool) store.StoreChannel {
ret := _m.Called(teamId, userId)
ret := _m.Called(teamId, userId, includeDeleted)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, string, bool) store.StoreChannel); ok {
@@ -775,9 +775,9 @@ func (_m *ChannelStore) SaveMember(member *model.ChannelMember) store.StoreChann
return r0
}
// SearchInTeam provides a mock function with given fields: teamId, term
// SearchInTeam provides a mock function with given fields: teamId, term, includeDeleted
func (_m *ChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) store.StoreChannel {
ret := _m.Called(teamId, term)
ret := _m.Called(teamId, term, includeDeleted)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string, string, bool) store.StoreChannel); ok {

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
@@ -514,6 +514,22 @@ func (_m *UserStore) GetUnreadCountForChannel(userId string, channelId string) s
return r0
}
// InferSystemInstallDate provides a mock function with given fields:
func (_m *UserStore) InferSystemInstallDate() store.StoreChannel {
ret := _m.Called()
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func() store.StoreChannel); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
}
}
return r0
}
// InvalidatProfileCacheForUser provides a mock function with given fields: userId
func (_m *UserStore) InvalidatProfileCacheForUser(userId string) {
_m.Called(userId)

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

@@ -1,4 +1,4 @@
// Code generated by mockery v1.0.0
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.