[MM-59503] export: enable exporting configuration with mmctl (#28412)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ca72cdb445
Коммит
424ce2b8db
@@ -333,6 +333,27 @@
|
|||||||
##### Permissions
|
##### Permissions
|
||||||
Must have `manage_system` permission.
|
Must have `manage_system` permission.
|
||||||
operationId: GetConfig
|
operationId: GetConfig
|
||||||
|
parameters:
|
||||||
|
- name: remove_masked
|
||||||
|
in: query
|
||||||
|
description: |
|
||||||
|
Remove masked values from the exported configuration.
|
||||||
|
|
||||||
|
__Minimum server version__: 10.4.0
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
- name: remove_defaults
|
||||||
|
in: query
|
||||||
|
description: |
|
||||||
|
Remove default values from the exported configuration.
|
||||||
|
|
||||||
|
__Minimum server version__: 10.4.0
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default: false
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Configuration retrieval successful
|
description: Configuration retrieval successful
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
@@ -66,19 +67,31 @@ func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filterMasked, _ := strconv.ParseBool(r.URL.Query().Get("remove_masked"))
|
||||||
|
filterDefaults, _ := strconv.ParseBool(r.URL.Query().Get("remove_defaults"))
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
|
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
|
||||||
|
filterOpts := model.ConfigFilterOptions{
|
||||||
|
GetConfigOptions: model.GetConfigOptions{
|
||||||
|
RemoveDefaults: filterDefaults,
|
||||||
|
RemoveMasked: filterMasked,
|
||||||
|
},
|
||||||
|
}
|
||||||
if c.App.Channels().License().IsCloud() {
|
if c.App.Channels().License().IsCloud() {
|
||||||
js, jsonErr := cfg.ToJSONFiltered(model.ConfigAccessTagType, model.ConfigAccessTagCloudRestrictable)
|
filterOpts.TagFilters = append(filterOpts.TagFilters, model.FilterTag{
|
||||||
if jsonErr != nil {
|
TagType: model.ConfigAccessTagType,
|
||||||
c.Err = model.NewAppError("getConfig", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
TagName: model.ConfigAccessTagCloudRestrictable,
|
||||||
return
|
})
|
||||||
}
|
}
|
||||||
w.Write(js)
|
m, err := model.FilterConfig(cfg, filterOpts)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("getConfig", "api.filter_config_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(cfg); err != nil {
|
|
||||||
|
if err := json.NewEncoder(w).Encode(m); err != nil {
|
||||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
@@ -27,10 +28,24 @@ func (api *API) InitConfigLocal() {
|
|||||||
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
func localGetConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
auditRec := c.MakeAuditRecord("localGetConfig", audit.Fail)
|
auditRec := c.MakeAuditRecord("localGetConfig", audit.Fail)
|
||||||
defer c.LogAuditRec(auditRec)
|
defer c.LogAuditRec(auditRec)
|
||||||
cfg := c.App.GetSanitizedConfig()
|
filterMasked, _ := strconv.ParseBool(r.URL.Query().Get("remove_masked"))
|
||||||
|
filterDefaults, _ := strconv.ParseBool(r.URL.Query().Get("remove_defaults"))
|
||||||
|
|
||||||
|
filterOpts := model.ConfigFilterOptions{
|
||||||
|
GetConfigOptions: model.GetConfigOptions{
|
||||||
|
RemoveDefaults: filterDefaults,
|
||||||
|
RemoveMasked: filterMasked,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := model.FilterConfig(c.App.Config(), filterOpts)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError("getConfig", "api.filter_config_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
if err := json.NewEncoder(w).Encode(cfg); err != nil {
|
if err := json.NewEncoder(w).Encode(m); err != nil {
|
||||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ func TestGetConfig(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
CheckForbiddenStatus(t, resp)
|
CheckForbiddenStatus(t, resp)
|
||||||
|
|
||||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
t.Run("Get config for system admin client", func(t *testing.T) {
|
||||||
cfg, _, err := client.GetConfig(context.Background())
|
cfg, _, err := th.SystemAdminClient.GetConfig(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.NotEqual(t, "", cfg.TeamSettings.SiteName)
|
require.NotEqual(t, "", cfg.TeamSettings.SiteName)
|
||||||
@@ -59,6 +59,14 @@ func TestGetConfig(t *testing.T) {
|
|||||||
require.FailNow(t, "did not sanitize properly")
|
require.FailNow(t, "did not sanitize properly")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Get config for local client", func(t *testing.T) {
|
||||||
|
cfg, _, err := th.LocalClient.GetConfig(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NotEqual(t, model.FakeSetting, *cfg.SqlSettings.DataSource)
|
||||||
|
require.NotEqual(t, model.FakeSetting, *cfg.FileSettings.PublicLinkSalt)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetConfigWithAccessTag(t *testing.T) {
|
func TestGetConfigWithAccessTag(t *testing.T) {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ type Client interface {
|
|||||||
MoveCommand(ctx context.Context, teamID string, commandID string) (*model.Response, error)
|
MoveCommand(ctx context.Context, teamID string, commandID string) (*model.Response, error)
|
||||||
DeleteCommand(ctx context.Context, commandID string) (*model.Response, error)
|
DeleteCommand(ctx context.Context, commandID string) (*model.Response, error)
|
||||||
GetConfig(ctx context.Context) (*model.Config, *model.Response, error)
|
GetConfig(ctx context.Context) (*model.Config, *model.Response, error)
|
||||||
|
GetConfigWithOptions(ctx context.Context, options model.GetConfigOptions) (map[string]any, *model.Response, error)
|
||||||
GetOldClientConfig(ctx context.Context, etag string) (map[string]string, *model.Response, error)
|
GetOldClientConfig(ctx context.Context, etag string) (map[string]string, *model.Response, error)
|
||||||
UpdateConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
UpdateConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
||||||
PatchConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
PatchConfig(context.Context, *model.Config) (*model.Config, *model.Response, error)
|
||||||
|
|||||||
@@ -120,6 +120,15 @@ var ConfigSubpathCmd = &cobra.Command{
|
|||||||
RunE: configSubpathCmdF,
|
RunE: configSubpathCmdF,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ConfigExportCmd = &cobra.Command{
|
||||||
|
Use: "export",
|
||||||
|
Short: "Export the server configuration",
|
||||||
|
Long: "Export the server configuration in case you want to import somewhere else.",
|
||||||
|
Example: "config export --remove-masked --remove-defaults",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: withClient(configExportCmdF),
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ConfigResetCmd.Flags().Bool("confirm", false, "confirm you really want to reset all configuration settings to its default value")
|
ConfigResetCmd.Flags().Bool("confirm", false, "confirm you really want to reset all configuration settings to its default value")
|
||||||
|
|
||||||
@@ -128,6 +137,9 @@ func init() {
|
|||||||
ConfigSubpathCmd.Flags().StringP("path", "p", "", "path to update the assets with")
|
ConfigSubpathCmd.Flags().StringP("path", "p", "", "path to update the assets with")
|
||||||
_ = ConfigSubpathCmd.MarkFlagRequired("path")
|
_ = ConfigSubpathCmd.MarkFlagRequired("path")
|
||||||
|
|
||||||
|
ConfigExportCmd.Flags().Bool("remove-masked", true, "remove masked values from the exported configuration")
|
||||||
|
ConfigExportCmd.Flags().Bool("remove-defaults", false, "remove default values from the exported configuration")
|
||||||
|
|
||||||
ConfigCmd.AddCommand(
|
ConfigCmd.AddCommand(
|
||||||
ConfigGetCmd,
|
ConfigGetCmd,
|
||||||
ConfigSetCmd,
|
ConfigSetCmd,
|
||||||
@@ -138,6 +150,7 @@ func init() {
|
|||||||
ConfigReloadCmd,
|
ConfigReloadCmd,
|
||||||
ConfigMigrateCmd,
|
ConfigMigrateCmd,
|
||||||
ConfigSubpathCmd,
|
ConfigSubpathCmd,
|
||||||
|
ConfigExportCmd,
|
||||||
)
|
)
|
||||||
RootCmd.AddCommand(ConfigCmd)
|
RootCmd.AddCommand(ConfigCmd)
|
||||||
}
|
}
|
||||||
@@ -581,3 +594,22 @@ func cloudRestrictedR(t reflect.Type, path []string) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configExportCmdF(c client.Client, cmd *cobra.Command, _ []string) error {
|
||||||
|
removeDefaults, _ := cmd.Flags().GetBool("remove-defaults")
|
||||||
|
removeMasked, _ := cmd.Flags().GetBool("remove-masked")
|
||||||
|
config, _, err := c.GetConfigWithOptions(context.TODO(), model.GetConfigOptions{
|
||||||
|
RemoveDefaults: removeDefaults,
|
||||||
|
RemoveMasked: removeMasked,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.SetSingle(true)
|
||||||
|
printer.SetFormat(printer.FormatJSON)
|
||||||
|
|
||||||
|
printer.Print(config)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,3 +233,93 @@ func (s *MmctlE2ETestSuite) TestConfigShowCmdF() {
|
|||||||
s.Require().Len(printer.GetErrorLines(), 0)
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MmctlE2ETestSuite) TestConfigExportCmdF() {
|
||||||
|
s.SetupTestHelper().InitBasic()
|
||||||
|
|
||||||
|
s.RunForSystemAdminAndLocal("Get config normally", func(c client.Client) {
|
||||||
|
printer.Clean()
|
||||||
|
|
||||||
|
err := configExportCmdF(c, &cobra.Command{}, nil)
|
||||||
|
s.Require().Nil(err)
|
||||||
|
s.Require().Len(printer.GetLines(), 1)
|
||||||
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
|
|
||||||
|
m, ok := printer.GetLines()[0].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
if c == s.th.LocalClient {
|
||||||
|
// filter config is used to convert the config to a map[string]any
|
||||||
|
// local client has unrestricted access to the config
|
||||||
|
expectedConfig, err2 := model.FilterConfig(s.th.App.Config(), model.ConfigFilterOptions{GetConfigOptions: model.GetConfigOptions{}})
|
||||||
|
s.Require().NoError(err2)
|
||||||
|
s.Require().Equal(expectedConfig, m)
|
||||||
|
} else {
|
||||||
|
// filter config is used to convert the config to a map[string]any
|
||||||
|
// system admin client has restricted access to the config
|
||||||
|
expectedConfig, err2 := model.FilterConfig(s.th.App.GetSanitizedConfig(), model.ConfigFilterOptions{GetConfigOptions: model.GetConfigOptions{}})
|
||||||
|
s.Require().NoError(err2)
|
||||||
|
s.Require().Equal(expectedConfig, m)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("Should remove masked values for system admin client", func() {
|
||||||
|
printer.Clean()
|
||||||
|
|
||||||
|
exportCmd := &cobra.Command{}
|
||||||
|
exportCmd.Flags().Bool("remove-masked", true, "")
|
||||||
|
err := configExportCmdF(s.th.SystemAdminClient, exportCmd, nil)
|
||||||
|
s.Require().Nil(err)
|
||||||
|
s.Require().Len(printer.GetLines(), 1)
|
||||||
|
m, ok := printer.GetLines()[0].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
ss, ok := m["SqlSettings"].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
_, ok = ss["DataSource"]
|
||||||
|
s.Require().False(ok)
|
||||||
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("Should retrieve configuration as-is with local client", func() {
|
||||||
|
printer.Clean()
|
||||||
|
|
||||||
|
exportCmd := &cobra.Command{}
|
||||||
|
err := configExportCmdF(s.th.LocalClient, exportCmd, nil)
|
||||||
|
s.Require().Nil(err)
|
||||||
|
s.Require().Len(printer.GetLines(), 1)
|
||||||
|
m, ok := printer.GetLines()[0].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
ss, ok := m["SqlSettings"].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
ds, ok := ss["DataSource"]
|
||||||
|
s.Require().True(ok)
|
||||||
|
cfg := s.th.App.Config()
|
||||||
|
s.Require().Equal(*cfg.SqlSettings.DataSource, ds)
|
||||||
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.RunForSystemAdminAndLocal("Should remove default values", func(c client.Client) {
|
||||||
|
printer.Clean()
|
||||||
|
|
||||||
|
exportCmd := &cobra.Command{}
|
||||||
|
exportCmd.Flags().Bool("remove-defaults", true, "")
|
||||||
|
err := configExportCmdF(c, exportCmd, nil)
|
||||||
|
s.Require().Nil(err)
|
||||||
|
s.Require().Len(printer.GetLines(), 1)
|
||||||
|
m, ok := printer.GetLines()[0].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
ss, ok := m["TeamSettings"].(map[string]any)
|
||||||
|
s.Require().True(ok)
|
||||||
|
_, ok = ss["MaxUsersPerTeam"] // it's not being changed by the test suite
|
||||||
|
s.Require().False(ok)
|
||||||
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.Run("Get config value for a given key without permissions", func() {
|
||||||
|
printer.Clean()
|
||||||
|
|
||||||
|
err := configExportCmdF(s.th.Client, &cobra.Command{}, nil)
|
||||||
|
s.Require().NotNil(err)
|
||||||
|
s.Require().Len(printer.GetLines(), 0)
|
||||||
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1002,3 +1002,26 @@ func TestSetConfigValue(t *testing.T) {
|
|||||||
assert.Equal(t, tc.expectedConfig, tc.config, name)
|
assert.Equal(t, tc.expectedConfig, tc.config, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MmctlUnitTestSuite) TestConfigExportCmd() {
|
||||||
|
s.Run("Should get the config as-is", func() {
|
||||||
|
// there is not much to test as the config is returned as-is
|
||||||
|
// adding a test to make sure future changes are not breaking this
|
||||||
|
printer.Clean()
|
||||||
|
|
||||||
|
s.client.
|
||||||
|
EXPECT().
|
||||||
|
GetConfigWithOptions(context.TODO(), model.GetConfigOptions{}).
|
||||||
|
Return(map[string]any{
|
||||||
|
"SqlSettings": map[string]any{
|
||||||
|
"DriverName": "postgres",
|
||||||
|
},
|
||||||
|
}, &model.Response{}, nil).
|
||||||
|
Times(1)
|
||||||
|
|
||||||
|
err := configExportCmdF(s.client, &cobra.Command{}, nil)
|
||||||
|
s.Require().Nil(err)
|
||||||
|
s.Require().Len(printer.GetLines(), 1)
|
||||||
|
s.Require().Len(printer.GetErrorLines(), 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ SEE ALSO
|
|||||||
|
|
||||||
* `mmctl <mmctl.rst>`_ - Remote client for the Open Source, self-hosted Slack-alternative
|
* `mmctl <mmctl.rst>`_ - Remote client for the Open Source, self-hosted Slack-alternative
|
||||||
* `mmctl config edit <mmctl_config_edit.rst>`_ - Edit the config
|
* `mmctl config edit <mmctl_config_edit.rst>`_ - Edit the config
|
||||||
|
* `mmctl config export <mmctl_config_export.rst>`_ - Export the server configuration
|
||||||
* `mmctl config get <mmctl_config_get.rst>`_ - Get config setting
|
* `mmctl config get <mmctl_config_get.rst>`_ - Get config setting
|
||||||
* `mmctl config migrate <mmctl_config_migrate.rst>`_ - Migrate existing config between backends
|
* `mmctl config migrate <mmctl_config_migrate.rst>`_ - Migrate existing config between backends
|
||||||
* `mmctl config patch <mmctl_config_patch.rst>`_ - Patch the config
|
* `mmctl config patch <mmctl_config_patch.rst>`_ - Patch the config
|
||||||
|
|||||||
53
server/cmd/mmctl/docs/mmctl_config_export.rst
Обычный файл
53
server/cmd/mmctl/docs/mmctl_config_export.rst
Обычный файл
@@ -0,0 +1,53 @@
|
|||||||
|
.. _mmctl_config_export:
|
||||||
|
|
||||||
|
mmctl config export
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Export the server configuration
|
||||||
|
|
||||||
|
Synopsis
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
Export the server configuration in case you want to import somewhere else.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
mmctl config export [flags]
|
||||||
|
|
||||||
|
Examples
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
config export --remove-masked --remove-defaults
|
||||||
|
|
||||||
|
Options
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
-h, --help help for export
|
||||||
|
--remove-defaults remove default values from the exported configuration
|
||||||
|
--remove-masked remove masked values from the exported configuration (default true)
|
||||||
|
|
||||||
|
Options inherited from parent commands
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
--config string path to the configuration file (default "$XDG_CONFIG_HOME/mmctl/config")
|
||||||
|
--disable-pager disables paged output
|
||||||
|
--insecure-sha1-intermediate allows to use insecure TLS protocols, such as SHA-1
|
||||||
|
--insecure-tls-version allows to use TLS versions 1.0 and 1.1
|
||||||
|
--json the output format will be in json format
|
||||||
|
--local allows communicating with the server through a unix socket
|
||||||
|
--quiet prevent mmctl to generate output for the commands
|
||||||
|
--strict will only run commands if the mmctl version matches the server one
|
||||||
|
--suppress-warnings disables printing warning messages
|
||||||
|
|
||||||
|
SEE ALSO
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
* `mmctl config <mmctl_config.rst>`_ - Configuration
|
||||||
|
|
||||||
@@ -763,6 +763,22 @@ func (mr *MockClientMockRecorder) GetConfig(arg0 interface{}) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockClient)(nil).GetConfig), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockClient)(nil).GetConfig), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConfigWithOptions mocks base method.
|
||||||
|
func (m *MockClient) GetConfigWithOptions(arg0 context.Context, arg1 model.GetConfigOptions) (map[string]interface{}, *model.Response, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetConfigWithOptions", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(map[string]interface{})
|
||||||
|
ret1, _ := ret[1].(*model.Response)
|
||||||
|
ret2, _ := ret[2].(error)
|
||||||
|
return ret0, ret1, ret2
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigWithOptions indicates an expected call of GetConfigWithOptions.
|
||||||
|
func (mr *MockClientMockRecorder) GetConfigWithOptions(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigWithOptions", reflect.TypeOf((*MockClient)(nil).GetConfigWithOptions), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDeletedChannelsForTeam mocks base method.
|
// GetDeletedChannelsForTeam mocks base method.
|
||||||
func (m *MockClient) GetDeletedChannelsForTeam(arg0 context.Context, arg1 string, arg2, arg3 int, arg4 string) ([]*model.Channel, *model.Response, error) {
|
func (m *MockClient) GetDeletedChannelsForTeam(arg0 context.Context, arg1 string, arg2, arg3 int, arg4 string) ([]*model.Channel, *model.Response, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
@@ -2132,6 +2132,10 @@
|
|||||||
"id": "api.file.write_file.app_error",
|
"id": "api.file.write_file.app_error",
|
||||||
"translation": "Unable to write the file."
|
"translation": "Unable to write the file."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.filter_config_error",
|
||||||
|
"translation": "Unable to filter the configuration."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.getThreadsForUser.bad_only_params",
|
"id": "api.getThreadsForUser.bad_only_params",
|
||||||
"translation": "OnlyThreads and OnlyTotals parameters to getThreadsForUser are mutually exclusive"
|
"translation": "OnlyThreads and OnlyTotals parameters to getThreadsForUser are mutually exclusive"
|
||||||
|
|||||||
@@ -4886,6 +4886,30 @@ func (c *Client4) GetConfig(ctx context.Context) (*Config, *Response, error) {
|
|||||||
return cfg, BuildResponse(r), d.Decode(&cfg)
|
return cfg, BuildResponse(r), d.Decode(&cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConfig will retrieve the server config with some sanitized items.
|
||||||
|
func (c *Client4) GetConfigWithOptions(ctx context.Context, options GetConfigOptions) (map[string]any, *Response, error) {
|
||||||
|
v := url.Values{}
|
||||||
|
if options.RemoveDefaults {
|
||||||
|
v.Set("remove_defaults", "true")
|
||||||
|
}
|
||||||
|
if options.RemoveMasked {
|
||||||
|
v.Set("remove_masked", "true")
|
||||||
|
}
|
||||||
|
url := c.configRoute()
|
||||||
|
if len(v) > 0 {
|
||||||
|
url += "?" + v.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.DoAPIGet(ctx, url, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
var cfg map[string]any
|
||||||
|
return cfg, BuildResponse(r), json.NewDecoder(r.Body).Decode(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// ReloadConfig will reload the server configuration.
|
// ReloadConfig will reload the server configuration.
|
||||||
func (c *Client4) ReloadConfig(ctx context.Context) (*Response, error) {
|
func (c *Client4) ReloadConfig(ctx context.Context) (*Response, error) {
|
||||||
r, err := c.DoAPIPost(ctx, c.configRoute()+"/reload", "")
|
r, err := c.DoAPIPost(ctx, c.configRoute()+"/reload", "")
|
||||||
|
|||||||
@@ -4576,6 +4576,66 @@ func (o *Config) Sanitize(pluginManifests []*Manifest) {
|
|||||||
o.PluginSettings.Sanitize(pluginManifests)
|
o.PluginSettings.Sanitize(pluginManifests)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilterTag struct {
|
||||||
|
TagType string
|
||||||
|
TagName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigFilterOptions struct {
|
||||||
|
GetConfigOptions
|
||||||
|
TagFilters []FilterTag
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetConfigOptions struct {
|
||||||
|
RemoveMasked bool
|
||||||
|
RemoveDefaults bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterConfig returns a map[string]any representation of the configuration.
|
||||||
|
// Also, the function can filter the configuration by the options passed
|
||||||
|
// in the argument. The options are used to remove the default values, the masked
|
||||||
|
// values and to filter the configuration by the tags passed in the TagFilters.
|
||||||
|
func FilterConfig(cfg *Config, opts ConfigFilterOptions) (map[string]any, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultCfg := &Config{}
|
||||||
|
defaultCfg.SetDefaults()
|
||||||
|
|
||||||
|
filteredCfg, err := cfg.StringMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredDefaultCfg, err := defaultCfg.StringMap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range opts.TagFilters {
|
||||||
|
filteredCfg = structToMapFilteredByTag(filteredCfg, opts.TagFilters[i].TagType, opts.TagFilters[i].TagName)
|
||||||
|
filteredDefaultCfg = structToMapFilteredByTag(defaultCfg, opts.TagFilters[i].TagType, opts.TagFilters[i].TagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.RemoveDefaults {
|
||||||
|
filteredCfg = stringMapDiff(filteredCfg, filteredDefaultCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.RemoveMasked {
|
||||||
|
removeFakeSettings(filteredCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only apply this if we applied some filters
|
||||||
|
// the alternative is to remove empty maps and slices during the filters
|
||||||
|
// but having this in a separate step makes it easier to understand
|
||||||
|
if opts.RemoveDefaults || opts.RemoveMasked || len(opts.TagFilters) > 0 {
|
||||||
|
removeEmptyMapsAndSlices(filteredCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
// structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed
|
// structToMapFilteredByTag converts a struct into a map removing those fields that has the tag passed
|
||||||
// as argument
|
// as argument
|
||||||
func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any {
|
func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any {
|
||||||
@@ -4625,6 +4685,90 @@ func structToMapFilteredByTag(t any, typeOfTag, filterTag string) map[string]any
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeEmptyMapsAndSlices removes all the empty maps and slices from a map
|
||||||
|
func removeEmptyMapsAndSlices(m map[string]any) {
|
||||||
|
for k, v := range m {
|
||||||
|
switch vt := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
removeEmptyMapsAndSlices(vt)
|
||||||
|
if len(vt) == 0 {
|
||||||
|
delete(m, k)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
if len(vt) == 0 {
|
||||||
|
delete(m, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMap returns a map[string]any representation of the Config struct
|
||||||
|
func (o *Config) StringMap() (map[string]any, error) {
|
||||||
|
b, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
err = json.Unmarshal(b, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringMapDiff returns the difference between two maps with string keys
|
||||||
|
func stringMapDiff(m1, m2 map[string]any) map[string]any {
|
||||||
|
result := make(map[string]any)
|
||||||
|
|
||||||
|
for k, v := range m1 {
|
||||||
|
if _, ok := m2[k]; !ok {
|
||||||
|
result[k] = v // ideally this should be never reached
|
||||||
|
}
|
||||||
|
|
||||||
|
if reflect.DeepEqual(v, m2[k]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
// this happens during the serialization of the struct to map
|
||||||
|
// so we can safely assume that the type is not matching, there
|
||||||
|
// is a difference in the values
|
||||||
|
casted, ok := m2[k].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
result[k] = v
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res := stringMapDiff(v.(map[string]any), casted)
|
||||||
|
if len(res) > 0 {
|
||||||
|
result[k] = res
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeFakeSettings removes all the fields that have the value of FakeSetting
|
||||||
|
// it's necessary to remove the fields that have been masked to be able to
|
||||||
|
// export the configuration (and make it importable)
|
||||||
|
func removeFakeSettings(m map[string]any) {
|
||||||
|
for k, v := range m {
|
||||||
|
switch vt := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
removeFakeSettings(vt)
|
||||||
|
case string:
|
||||||
|
if v == FakeSetting {
|
||||||
|
delete(m, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isTagPresent(tag string, tags []string) bool {
|
func isTagPresent(tag string, tags []string) bool {
|
||||||
for _, val := range tags {
|
for _, val := range tags {
|
||||||
tagValue := strings.TrimSpace(val)
|
tagValue := strings.TrimSpace(val)
|
||||||
|
|||||||
@@ -1968,3 +1968,124 @@ func TestConfigDefaultConnectedWorkspacesSettings(t *testing.T) {
|
|||||||
require.True(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService)
|
require.True(t, *c.ConnectedWorkspacesSettings.EnableRemoteClusterService)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterConfig(t *testing.T) {
|
||||||
|
t.Run("should clear default values", func(t *testing.T) {
|
||||||
|
cfg := &Config{}
|
||||||
|
cfg.SetDefaults()
|
||||||
|
|
||||||
|
m, err := FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, m)
|
||||||
|
|
||||||
|
cfg.ServiceSettings = ServiceSettings{
|
||||||
|
EnableLocalMode: NewPointer(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err = FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, m)
|
||||||
|
require.Equal(t, true, m["ServiceSettings"].(map[string]any)["EnableLocalMode"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should clear masked config values", func(t *testing.T) {
|
||||||
|
cfg := &Config{}
|
||||||
|
cfg.SetDefaults()
|
||||||
|
|
||||||
|
dsn := "somedb://user:password@localhost:5432/mattermost"
|
||||||
|
cfg.SqlSettings.DataSource = NewPointer(dsn)
|
||||||
|
|
||||||
|
m, err := FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, m)
|
||||||
|
require.Equal(t, dsn, m["SqlSettings"].(map[string]any)["DataSource"])
|
||||||
|
|
||||||
|
cfg.Sanitize(nil)
|
||||||
|
m, err = FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, m)
|
||||||
|
require.Equal(t, FakeSetting, m["SqlSettings"].(map[string]any)["DataSource"])
|
||||||
|
|
||||||
|
cfg.Sanitize(nil)
|
||||||
|
m, err = FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
RemoveMasked: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, m)
|
||||||
|
|
||||||
|
cfg.SqlSettings.DriverName = NewPointer("mysql")
|
||||||
|
m, err = FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
RemoveMasked: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, m)
|
||||||
|
require.Equal(t, "mysql", m["SqlSettings"].(map[string]any)["DriverName"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should not clear non primitive types", func(t *testing.T) {
|
||||||
|
cfg := &Config{}
|
||||||
|
cfg.SetDefaults()
|
||||||
|
|
||||||
|
cfg.TeamSettings.ExperimentalDefaultChannels = []string{"ch-a", "ch-b"}
|
||||||
|
m, err := FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, m)
|
||||||
|
require.ElementsMatch(t, []string{"ch-a", "ch-b"}, m["TeamSettings"].(map[string]any)["ExperimentalDefaultChannels"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should be able to handle nil values", func(t *testing.T) {
|
||||||
|
var cfg *Config
|
||||||
|
|
||||||
|
m, err := FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, m)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should be able to handle float64 values", func(t *testing.T) {
|
||||||
|
cfg := &Config{}
|
||||||
|
cfg.SetDefaults()
|
||||||
|
cfg.PluginSettings.Plugins = map[string]map[string]any{
|
||||||
|
"com.mattermost.plugin-a": {
|
||||||
|
"setting": 1.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := FilterConfig(cfg, ConfigFilterOptions{
|
||||||
|
GetConfigOptions: GetConfigOptions{
|
||||||
|
RemoveDefaults: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1.0, m["PluginSettings"].(map[string]any)["Plugins"].(map[string]any)["com.mattermost.plugin-a"].(map[string]any)["setting"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user