mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
616 lines
17 KiB
Go
616 lines
17 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
|
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const defaultEditor = "vi"
|
|
|
|
var ErrConfigInvalidPath = errors.New("selected path object is not valid")
|
|
|
|
var ConfigCmd = &cobra.Command{
|
|
Use: "config",
|
|
Short: "Configuration",
|
|
}
|
|
|
|
var ConfigGetCmd = &cobra.Command{
|
|
Use: "get",
|
|
Short: "Get config setting",
|
|
Long: "Gets the value of a config setting by its name in dot notation.",
|
|
Example: `config get SqlSettings.DriverName`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: withClient(configGetCmdF),
|
|
}
|
|
|
|
var ConfigSetCmd = &cobra.Command{
|
|
Use: "set",
|
|
Short: "Set config setting",
|
|
Long: "Sets the value of a config setting by its name in dot notation. Accepts multiple values for array settings",
|
|
Example: "config set SqlSettings.DriverName mysql\nconfig set SqlSettings.DataSourceReplicas \"replica1\" \"replica2\"",
|
|
Args: cobra.MinimumNArgs(2),
|
|
RunE: withClient(configSetCmdF),
|
|
}
|
|
|
|
var ConfigPatchCmd = &cobra.Command{
|
|
Use: "patch <config-file>",
|
|
Short: "Patch the config",
|
|
Long: "Patches config settings with the given config file.",
|
|
Example: "config patch /path/to/config.json",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: withClient(configPatchCmdF),
|
|
}
|
|
|
|
var ConfigEditCmd = &cobra.Command{
|
|
Use: "edit",
|
|
Short: "Edit the config",
|
|
Long: "Opens the editor defined in the EDITOR environment variable to modify the server's configuration and then uploads it",
|
|
Example: "config edit",
|
|
Args: cobra.NoArgs,
|
|
RunE: withClient(configEditCmdF),
|
|
}
|
|
|
|
var ConfigResetCmd = &cobra.Command{
|
|
Use: "reset",
|
|
Short: "Reset config setting",
|
|
Long: "Resets the value of a config setting by its name in dot notation or a setting section. Accepts multiple values for array settings.",
|
|
Example: "config reset SqlSettings.DriverName LogSettings",
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: withClient(configResetCmdF),
|
|
}
|
|
|
|
var ConfigShowCmd = &cobra.Command{
|
|
Use: "show",
|
|
Short: "Writes the server configuration to STDOUT",
|
|
Long: "Prints the server configuration and writes to STDOUT in JSON format.",
|
|
Example: "config show",
|
|
Args: cobra.NoArgs,
|
|
RunE: withClient(configShowCmdF),
|
|
}
|
|
|
|
var ConfigReloadCmd = &cobra.Command{
|
|
Use: "reload",
|
|
Short: "Reload the server configuration",
|
|
Long: "Reload the server configuration in case you want to new settings to be applied.",
|
|
Example: "config reload",
|
|
Args: cobra.NoArgs,
|
|
RunE: withClient(configReloadCmdF),
|
|
}
|
|
|
|
var ConfigMigrateCmd = &cobra.Command{
|
|
Use: "migrate [from_config] [to_config]",
|
|
Short: "Migrate existing config between backends",
|
|
Long: "Migrate a file-based configuration to (or from) a database-based configuration. Point the Mattermost server at the target configuration to start using it. Note that this command is only available in `--local` mode.",
|
|
Example: `config migrate path/to/config.json "postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable&connect_timeout=10"`,
|
|
Args: cobra.ExactArgs(2),
|
|
RunE: withClient(configMigrateCmdF),
|
|
}
|
|
|
|
var ConfigSubpathCmd = &cobra.Command{
|
|
Use: "subpath",
|
|
Short: "Update client asset loading to use the configured subpath",
|
|
Long: "Update the hard-coded production client asset paths to take into account Mattermost running on a subpath. This command needs access to the Mattermost assets directory to be able to rewrite the paths.",
|
|
Example: ` # you can rewrite the assets to use a subpath
|
|
mmctl config subpath --assets-dir /opt/mattermost/client --path /mattermost
|
|
|
|
# the subpath can have multiple steps
|
|
mmctl config subpath --assets-dir /opt/mattermost/client --path /my/custom/subpath
|
|
|
|
# or you can fallback to the root path passing /
|
|
mmctl config subpath --assets-dir /opt/mattermost/client --path /`,
|
|
Args: cobra.NoArgs,
|
|
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() {
|
|
ConfigResetCmd.Flags().Bool("confirm", false, "confirm you really want to reset all configuration settings to its default value")
|
|
|
|
ConfigSubpathCmd.Flags().StringP("assets-dir", "a", "", "directory of the Mattermost assets in the local filesystem")
|
|
_ = ConfigSubpathCmd.MarkFlagRequired("assets-dir")
|
|
ConfigSubpathCmd.Flags().StringP("path", "p", "", "path to update the assets with")
|
|
_ = 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(
|
|
ConfigGetCmd,
|
|
ConfigSetCmd,
|
|
ConfigPatchCmd,
|
|
ConfigEditCmd,
|
|
ConfigResetCmd,
|
|
ConfigShowCmd,
|
|
ConfigReloadCmd,
|
|
ConfigMigrateCmd,
|
|
ConfigSubpathCmd,
|
|
ConfigExportCmd,
|
|
)
|
|
RootCmd.AddCommand(ConfigCmd)
|
|
}
|
|
|
|
func getValue(path []string, obj any) (any, bool) {
|
|
r := reflect.ValueOf(obj)
|
|
var val reflect.Value
|
|
if r.Kind() == reflect.Map {
|
|
val = r.MapIndex(reflect.ValueOf(path[0]))
|
|
if val.IsValid() {
|
|
val = val.Elem()
|
|
}
|
|
} else {
|
|
val = r.FieldByName(path[0])
|
|
}
|
|
|
|
if !val.IsValid() {
|
|
return nil, false
|
|
}
|
|
|
|
switch {
|
|
case len(path) == 1:
|
|
return val.Interface(), true
|
|
case val.Kind() == reflect.Struct:
|
|
return getValue(path[1:], val.Interface())
|
|
case val.Kind() == reflect.Map:
|
|
remainingPath := strings.Join(path[1:], ".")
|
|
mapIter := val.MapRange()
|
|
for mapIter.Next() {
|
|
key := mapIter.Key().String()
|
|
if strings.HasPrefix(remainingPath, key) {
|
|
i := strings.Count(key, ".") + 2 // number of dots + a dot on each side
|
|
mapVal := mapIter.Value()
|
|
// if no sub field path specified, return the object
|
|
if len(path[i:]) == 0 {
|
|
return mapVal.Interface(), true
|
|
}
|
|
data := mapVal.Interface()
|
|
if mapVal.Kind() == reflect.Ptr {
|
|
data = mapVal.Elem().Interface() // if value is a pointer, dereference it
|
|
}
|
|
// pass subpath
|
|
return getValue(path[i:], data)
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func setValueWithConversion(val reflect.Value, newValue any) error {
|
|
switch val.Kind() {
|
|
case reflect.Struct:
|
|
val.Set(reflect.ValueOf(newValue))
|
|
return nil
|
|
case reflect.Slice:
|
|
v := reflect.ValueOf(newValue)
|
|
if v.Kind() != reflect.Slice {
|
|
// Special case when setting a string to a byte slice
|
|
if val.Type().Elem().Kind() == reflect.Uint8 && v.Kind() == reflect.String {
|
|
s := newValue.(string)
|
|
v = reflect.ValueOf([]byte(s))
|
|
} else {
|
|
return errors.Errorf("target value is of type %v and provided value is %v", val.Kind(), v.Kind())
|
|
}
|
|
}
|
|
val.Set(v)
|
|
return nil
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
bits := val.Type().Bits()
|
|
v, err := strconv.ParseInt(newValue.(string), 10, bits)
|
|
if err != nil {
|
|
return fmt.Errorf("target value is of type %v and provided value is not, err: %v", val.Kind(), err)
|
|
}
|
|
val.SetInt(v)
|
|
return nil
|
|
case reflect.Float32, reflect.Float64:
|
|
bits := val.Type().Bits()
|
|
v, err := strconv.ParseFloat(newValue.(string), bits)
|
|
if err != nil {
|
|
return fmt.Errorf("target value is of type %v and provided value is not, err: %v", val.Kind(), err)
|
|
}
|
|
val.SetFloat(v)
|
|
return nil
|
|
case reflect.String:
|
|
val.SetString(newValue.(string))
|
|
return nil
|
|
case reflect.Bool:
|
|
v, err := strconv.ParseBool(newValue.(string))
|
|
if err != nil {
|
|
return fmt.Errorf("target value is of type %v and provided value is not, err: %v", val.Kind(), err)
|
|
}
|
|
val.SetBool(v)
|
|
return nil
|
|
default:
|
|
return errors.Errorf("value type %v is not supported", val.Kind())
|
|
}
|
|
}
|
|
|
|
func setValue(path []string, obj reflect.Value, newValue any) error {
|
|
var val reflect.Value
|
|
switch obj.Kind() {
|
|
case reflect.Struct:
|
|
val = obj.FieldByName(path[0])
|
|
case reflect.Map:
|
|
val = obj.MapIndex(reflect.ValueOf(path[0]))
|
|
if val.IsValid() {
|
|
val = val.Elem()
|
|
}
|
|
default:
|
|
val = obj
|
|
}
|
|
|
|
if val.Kind() == reflect.Invalid {
|
|
return ErrConfigInvalidPath
|
|
}
|
|
|
|
if len(path) == 1 {
|
|
if val.Kind() == reflect.Ptr {
|
|
return setValue(path, val.Elem(), newValue)
|
|
} else if obj.Kind() == reflect.Map {
|
|
// since we cannot set map elements directly, we clone the value, set it, and then put it back in the map
|
|
mapKey := reflect.ValueOf(path[0])
|
|
subVal := obj.MapIndex(mapKey)
|
|
if subVal.IsValid() {
|
|
tmpVal := reflect.New(subVal.Elem().Type())
|
|
if err := setValueWithConversion(tmpVal.Elem(), newValue); err != nil {
|
|
return err
|
|
}
|
|
obj.SetMapIndex(mapKey, tmpVal)
|
|
return nil
|
|
}
|
|
}
|
|
return setValueWithConversion(val, newValue)
|
|
}
|
|
|
|
if val.Kind() == reflect.Struct {
|
|
return setValue(path[1:], val, newValue)
|
|
} else if val.Kind() == reflect.Map {
|
|
remainingPath := strings.Join(path[1:], ".")
|
|
mapIter := val.MapRange()
|
|
for mapIter.Next() {
|
|
key := mapIter.Key().String()
|
|
if strings.HasPrefix(remainingPath, key) {
|
|
mapVal := mapIter.Value()
|
|
|
|
if mapVal.Kind() == reflect.Ptr {
|
|
mapVal = mapVal.Elem() // if value is a pointer, dereference it
|
|
}
|
|
i := len(strings.Split(key, ".")) + 1
|
|
|
|
if i > len(path)-1 { // leaf element
|
|
i = 1
|
|
mapVal = val
|
|
}
|
|
// pass subpath
|
|
return setValue(path[i:], mapVal, newValue)
|
|
}
|
|
}
|
|
}
|
|
return errors.New("path object type is not supported")
|
|
}
|
|
|
|
func setConfigValue(path []string, config *model.Config, newValue []string) error {
|
|
if len(newValue) > 1 {
|
|
return setValue(path, reflect.ValueOf(config).Elem(), newValue)
|
|
}
|
|
return setValue(path, reflect.ValueOf(config).Elem(), newValue[0])
|
|
}
|
|
|
|
func resetConfigValue(path []string, config *model.Config, newValue any) error {
|
|
nv := reflect.ValueOf(newValue)
|
|
if nv.Kind() == reflect.Ptr {
|
|
switch nv.Elem().Kind() {
|
|
case reflect.Int:
|
|
return setValue(path, reflect.ValueOf(config).Elem(), strconv.Itoa(*newValue.(*int)))
|
|
case reflect.Bool:
|
|
return setValue(path, reflect.ValueOf(config).Elem(), strconv.FormatBool(*newValue.(*bool)))
|
|
default:
|
|
return setValue(path, reflect.ValueOf(config).Elem(), *newValue.(*string))
|
|
}
|
|
} else {
|
|
return setValue(path, reflect.ValueOf(config).Elem(), newValue)
|
|
}
|
|
}
|
|
|
|
func configGetCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
|
printer.SetSingle(true)
|
|
printer.SetFormat(printer.FormatJSON)
|
|
|
|
config, _, err := c.GetConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := strings.Split(args[0], ".")
|
|
val, ok := getValue(path, *config)
|
|
if !ok {
|
|
return errors.New("invalid key")
|
|
}
|
|
|
|
if cloudRestricted(config, path) && reflect.ValueOf(val).IsNil() {
|
|
return fmt.Errorf("accessing this config path: %s is restricted in a cloud environment", args[0])
|
|
}
|
|
|
|
printer.Print(val)
|
|
return nil
|
|
}
|
|
|
|
func configSetCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
|
config, _, err := c.GetConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := parseConfigPath(args[0])
|
|
if cErr := setConfigValue(path, config, args[1:]); cErr != nil {
|
|
if errors.Is(cErr, ErrConfigInvalidPath) && cloudRestricted(config, path) {
|
|
return fmt.Errorf("changing this config path: %s is restricted in a cloud environment", args[0])
|
|
}
|
|
|
|
return cErr
|
|
}
|
|
newConfig, _, err := c.PatchConfig(context.TODO(), config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer.PrintT("Value changed successfully", newConfig)
|
|
return nil
|
|
}
|
|
|
|
func configPatchCmdF(c client.Client, _ *cobra.Command, args []string) error {
|
|
configBytes, err := os.ReadFile(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config, _, err := c.GetConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// get original plugin map
|
|
var pluginConfig map[string]map[string]any
|
|
if config.PluginSettings.Plugins != nil {
|
|
pluginConfig = (config.Clone()).PluginSettings.Plugins
|
|
}
|
|
|
|
// apply path onto the existing config
|
|
if jErr := json.Unmarshal(configBytes, config); jErr != nil {
|
|
return jErr
|
|
}
|
|
|
|
// merge config plugin map on top of the original, and assign the
|
|
// result to the config key
|
|
config.PluginSettings.Plugins = MergePluginConfigs(pluginConfig, config.PluginSettings.Plugins)
|
|
|
|
newConfig, _, err := c.PatchConfig(context.TODO(), config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer.PrintT("Config patched successfully", newConfig)
|
|
return nil
|
|
}
|
|
|
|
func configEditCmdF(c client.Client, _ *cobra.Command, _ []string) error {
|
|
config, _, err := c.GetConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configBytes, err := json.MarshalIndent(config, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := os.CreateTemp(os.TempDir(), "mmctl-*.json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
file.Close()
|
|
os.Remove(file.Name())
|
|
}()
|
|
if _, writeErr := file.Write(configBytes); writeErr != nil {
|
|
return writeErr
|
|
}
|
|
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
editor = defaultEditor
|
|
}
|
|
|
|
editorCmd := exec.Command(editor, file.Name())
|
|
editorCmd.Stdout = os.Stdout
|
|
editorCmd.Stdin = os.Stdin
|
|
editorCmd.Stderr = os.Stderr
|
|
|
|
if cmdErr := editorCmd.Run(); cmdErr != nil {
|
|
return cmdErr
|
|
}
|
|
|
|
newConfigBytes, err := os.ReadFile(file.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if jErr := json.Unmarshal(newConfigBytes, config); jErr != nil {
|
|
return jErr
|
|
}
|
|
|
|
newConfig, _, err := c.UpdateConfig(context.TODO(), config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer.PrintT("Config updated successfully", newConfig)
|
|
return nil
|
|
}
|
|
|
|
func configResetCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
confirmFlag, _ := cmd.Flags().GetBool("confirm")
|
|
|
|
if !confirmFlag && len(args) > 0 {
|
|
if err := getConfirmation(fmt.Sprintf(
|
|
"Are you sure you want to reset %s to their default value? (YES/NO): ",
|
|
args[0]), false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
defaultConfig := &model.Config{}
|
|
defaultConfig.SetDefaults()
|
|
config, _, err := c.GetConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, arg := range args {
|
|
path := parseConfigPath(arg)
|
|
defaultValue, ok := getValue(path, *defaultConfig)
|
|
if !ok {
|
|
return errors.New("invalid key")
|
|
}
|
|
nErr := resetConfigValue(path, config, defaultValue)
|
|
if nErr != nil {
|
|
return nErr
|
|
}
|
|
}
|
|
newConfig, _, err := c.UpdateConfig(context.TODO(), config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer.PrintT("Value/s reset successfully", newConfig)
|
|
return nil
|
|
}
|
|
|
|
func configShowCmdF(c client.Client, _ *cobra.Command, _ []string) error {
|
|
printer.SetSingle(true)
|
|
printer.SetFormat(printer.FormatJSON)
|
|
config, _, err := c.GetConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printer.Print(config)
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseConfigPath(configPath string) []string {
|
|
return strings.Split(configPath, ".")
|
|
}
|
|
|
|
func configReloadCmdF(c client.Client, _ *cobra.Command, _ []string) error {
|
|
_, err := c.ReloadConfig(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configMigrateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
isLocal, _ := cmd.Flags().GetBool("local")
|
|
if !isLocal {
|
|
return errors.New("this command is only available in local mode. Please set the --local flag")
|
|
}
|
|
|
|
_, err := c.MigrateConfig(context.TODO(), args[0], args[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configSubpathCmdF(cmd *cobra.Command, _ []string) error {
|
|
assetsDir, _ := cmd.Flags().GetString("assets-dir")
|
|
path, _ := cmd.Flags().GetString("path")
|
|
|
|
if err := utils.UpdateAssetsSubpathInDir(path, assetsDir); err != nil {
|
|
return errors.Wrap(err, "failed to update assets subpath")
|
|
}
|
|
|
|
printer.Print("Config subpath successfully modified")
|
|
|
|
return nil
|
|
}
|
|
|
|
func cloudRestricted(cfg any, path []string) bool {
|
|
return cloudRestrictedR(reflect.TypeOf(cfg), path)
|
|
}
|
|
|
|
// cloudRestricted checks if the config path is restricted to the cloud
|
|
func cloudRestrictedR(t reflect.Type, path []string) bool {
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
return false
|
|
}
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
|
|
if len(path) == 0 || field.Name != path[0] {
|
|
continue
|
|
}
|
|
|
|
accessTag := field.Tag.Get(model.ConfigAccessTagType)
|
|
if strings.Contains(accessTag, model.ConfigAccessTagCloudRestrictable) {
|
|
return true
|
|
}
|
|
|
|
return cloudRestrictedR(field.Type, path[1:])
|
|
}
|
|
|
|
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
|
|
}
|