Files
e1900db9ea Remove vestigial MySQL support (#34865)
* Remove legacy quoteColumnName() utility

Since Mattermost only supports PostgreSQL, the quoteColumnName() helper
that was designed to handle database-specific column quoting is no longer
needed. The function was a no-op that simply returned the column name
unchanged.

Remove the function from utils.go and update status_store.go to use
the "Manual" column name directly.

* Remove legacy driver checks from store.go

Since Mattermost only supports PostgreSQL, remove conditional checks
for different database drivers:

- Simplify specialSearchChars() to always return PostgreSQL-compatible chars
- Remove driver check from computeBinaryParam()
- Remove driver check from computeDefaultTextSearchConfig()
- Simplify GetDbVersion() to use PostgreSQL syntax directly
- Remove switch statement from ensureMinimumDBVersion()
- Remove unused driver parameter from versionString()

* Remove MySQL alternatives for batch delete operations

Since Mattermost only supports PostgreSQL, remove the MySQL-specific
DELETE...LIMIT syntax and keep only the PostgreSQL array-based approach:

- reaction_store.go: Use PostgreSQL array syntax for PermanentDeleteBatch
- file_info_store.go: Use PostgreSQL array syntax for PermanentDeleteBatch
- preference_store.go: Use PostgreSQL tuple IN subquery for DeleteInvalidVisibleDmsGms

* Remove MySQL alternatives for UPDATE...FROM syntax

Since Mattermost only supports PostgreSQL, remove the MySQL-specific
UPDATE syntax that joins tables differently:

- thread_store.go: Use PostgreSQL UPDATE...FROM syntax in
  MarkAllAsReadByChannels and MarkAllAsReadByTeam
- post_store.go: Use PostgreSQL UPDATE...FROM syntax in deleteThreadFiles

* Remove MySQL alternatives for JSON and subquery operations

Since Mattermost only supports PostgreSQL, remove the MySQL-specific
JSON and subquery syntax:

- thread_store.go: Use PostgreSQL JSONB operators for updating participants
- access_control_policy_store.go: Use PostgreSQL JSONB @> operator for
  querying JSON imports
- session_store.go: Use PostgreSQL subquery syntax for Cleanup
- job_store.go: Use PostgreSQL subquery syntax for Cleanup

* Remove MySQL alternatives for CTE queries

Since Mattermost only supports PostgreSQL, simplify code that
uses CTEs (Common Table Expressions):

- channel_store.go: Remove MySQL CASE-based fallback in
  UpdateLastViewedAt and use PostgreSQL CTE exclusively
- draft_store.go: Remove driver checks in DeleteEmptyDraftsByCreateAtAndUserId,
  DeleteOrphanDraftsByCreateAtAndUserId, and determineMaxDraftSize

* Remove driver checks in migrate.go and schema_dump.go

Simplify migration code to use PostgreSQL driver directly since
PostgreSQL is the only supported database.

* Remove driver checks in sqlx_wrapper.go

Always apply lowercase named parameter transformation since PostgreSQL
is the only supported database.

* Remove driver checks in user_store.go

Simplify user store functions to use PostgreSQL-only code paths:
- Remove isPostgreSQL parameter from helper functions
- Use LEFT JOIN pattern instead of subqueries for bot filtering
- Always use case-insensitive LIKE with lower() for search
- Remove MySQL-specific role filtering alternatives

* Remove driver checks in post_store.go

Simplify post_store.go to use PostgreSQL-only code paths:
- Inline getParentsPostsPostgreSQL into getParentsPosts
- Use PostgreSQL TO_CHAR/TO_TIMESTAMP for date formatting in analytics
- Use PostgreSQL array syntax for batch deletes
- Simplify determineMaxPostSize to always use information_schema
- Use PostgreSQL jsonb subtraction for thread participants
- Always execute RefreshPostStats (PostgreSQL materialized views)
- Use materialized views for AnalyticsPostCountsByDay
- Simplify AnalyticsPostCountByTeam to always use countByTeam

* Remove driver checks in channel_store.go

Simplify channel_store.go to use PostgreSQL-only code paths:
- Always use sq.Dollar.ReplacePlaceholders for UNION queries
- Use PostgreSQL LEFT JOIN for retention policy exclusion
- Use PostgreSQL jsonb @> operator for access control policy imports
- Simplify buildLIKEClause to always use LOWER() for case-insensitive search
- Simplify buildFulltextClauseX to always use PostgreSQL to_tsvector/to_tsquery
- Simplify searchGroupChannelsQuery to use ARRAY_TO_STRING/ARRAY_AGG

* Remove driver checks in file_info_store.go

Simplify file_info_store.go to use PostgreSQL-only code paths:
- Always use PostgreSQL to_tsvector/to_tsquery for file search
- Use file_stats materialized view for CountAll()
- Use file_stats materialized view for GetStorageUsage() when not including deleted
- Always execute RefreshFileStats() for materialized view refresh

* Remove driver checks in attributes_store.go

Simplify attributes_store.go to use PostgreSQL-only code paths:
- Always execute RefreshAttributes() for materialized view refresh
- Remove isPostgreSQL parameter from generateSearchQueryForExpression
- Always use PostgreSQL LOWER() LIKE LOWER() syntax for case-insensitive search

* Remove driver checks in retention_policy_store.go

Simplify retention_policy_store.go to use PostgreSQL-only code paths:
- Remove isPostgres parameter from scanRetentionIdsForDeletion
- Always use pq.Array for scanning retention IDs
- Always use pq.Array for inserting retention IDs
- Remove unused json import

* Remove driver checks in property stores

Simplify property_field_store.go and property_value_store.go to use
PostgreSQL-only code paths:
- Always use PostgreSQL type casts (::text, ::jsonb, ::bigint, etc.)
- Remove isPostgres variable and conditionals

* Remove driver checks in channel_member_history_store.go

Simplify PermanentDeleteBatch to use PostgreSQL-only code path:
- Always use ctid-based subquery for DELETE with LIMIT

* Remove remaining driver checks in user_store.go

Simplify user_store.go to use PostgreSQL-only code paths:
- Use LEFT JOIN for bot exclusion in AnalyticsActiveCountForPeriod
- Use LEFT JOIN for bot exclusion in IsEmpty

* Simplify fulltext search by consolidating buildFulltextClause functions

Remove convertMySQLFullTextColumnsToPostgres and consolidate
buildFulltextClause and buildFulltextClauseX into a single function
that takes variadic column arguments and returns sq.Sqlizer.

* Simplify SQL stores leveraging PostgreSQL-only support

- Simplify UpdateMembersRole in channel_store.go and team_store.go
  to use UPDATE...RETURNING instead of SELECT + UPDATE
- Simplify GetPostReminders in post_store.go to use DELETE...RETURNING
- Simplify DeleteOrphanedRows queries by removing MySQL workarounds
  for subquery locking issues
- Simplify UpdateUserLastSyncAt to use UPDATE...FROM...RETURNING
  instead of fetching user first then updating
- Remove MySQL index hint workarounds in ORDER BY clauses
- Update outdated comments referencing MySQL
- Consolidate buildFulltextClause and remove convertMySQLFullTextColumnsToPostgres

* Remove MySQL-specific test artifacts

- Delete unused MySQLStopWords variable and stop_word.go file
- Remove redundant testSearchEmailAddressesWithQuotes test
  (already covered by testSearchEmailAddresses)
- Update comment that referenced MySQL query planning

* Remove MySQL references from server code outside sqlstore

- Update config example and DSN parsing docs to reflect PostgreSQL-only support
- Remove mysql:// scheme check from IsDatabaseDSN
- Simplify SanitizeDataSource to only handle PostgreSQL
- Remove outdated MySQL comments from model and plugin code

* Remove MySQL references from test files

- Update test DSNs to use PostgreSQL format
- Remove dead mysql-replica flag and replicaFlag variable
- Simplify tests that had MySQL/PostgreSQL branches

* Update docs and test config to use PostgreSQL

- Update mmctl config set example to use postgres driver
- Update test-config.json to use PostgreSQL DSN format

* Remove MySQL migration scripts, test data, and docker image

Delete MySQL-related files that are no longer needed:
- ESR upgrade scripts (esr.*.mysql.*.sql)
- MySQL schema dumps (mattermost-mysql-*.sql)
- MySQL replication test scripts (replica-*.sh, mysql-migration-test.sh)
- MySQL test warmup data (mysql_migration_warmup.sql)
- MySQL docker image reference from mirror-docker-images.json

* Remove MySQL references from webapp

- Simplify minimumHashtagLength description to remove MySQL-specific configuration note
- Remove unused HIDE_MYSQL_STATS_NOTIFICATION preference constant
- Update en.json i18n source file

* clean up e2e-tests

* rm server/tests/template.load

* Use teamMemberSliceColumns() in UpdateMembersRole RETURNING clause

Refactor to use the existing helper function instead of hardcoding
the column names, ensuring consistency if the columns are updated.

* u.id -> u.Id

* address code review feedback

---------

Co-authored-by: Mattermost Build <[email protected]>
2026-01-20 21:01:59 +00:00

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 postgres\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: %w", 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: %w", 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: %w", 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
}