diff --git a/cli/cmd/replicaset.go b/cli/cmd/replicaset.go index d0bc5ec78..c422a3e40 100644 --- a/cli/cmd/replicaset.go +++ b/cli/cmd/replicaset.go @@ -28,15 +28,18 @@ var ( replicaset.OrchestratorCustom: &orchestratorCustom, } - replicasetUser string - replicasetPassword string - replicasetSslKeyFile string - replicasetSslCertFile string - replicasetSslCaFile string - replicasetSslCiphers string - replicasetForce bool - replicasetTimeout int - replicasetIntegrityPrivateKey string + replicasetUser string + replicasetPassword string + replicasetSslKeyFile string + replicasetSslCertFile string + replicasetSslCaFile string + replicasetSslCiphers string + replicasetForce bool + replicasetTimeout int + replicasetIntegrityPrivateKey string + replicasetBootstrapVshard bool + replicasetCartridgeReplicasetsFile string + replicasetReplicasetName string replicasetUriHelp = " The URI can be specified in the following formats:\n" + " * [tcp://][username:password@][host:port]\n" + @@ -149,6 +152,34 @@ func newExpelCmd() *cobra.Command { return cmd } +// newBootstrapCmd creates a "replicaset bootstrap" command. +func newBootstrapCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "bootstrap [--timeout secs] [flags] ", + Short: "Bootstrap an application or instance", + Long: "Bootstrap an application or instance.", + Run: func(cmd *cobra.Command, args []string) { + cmdCtx.CommandName = cmd.Name() + err := modules.RunCmd(&cmdCtx, cmd.CommandPath(), &modulesInfo, + internalReplicasetBootstrapModule, args) + util.HandleCmdErr(cmd, err) + }, + Args: cobra.ExactArgs(1), + } + + addOrchestratorFlags(cmd) + cmd.Flags().BoolVarP(&replicasetBootstrapVshard, "bootstrap-vshard", "", false, + "bootstrap vshard") + cmd.Flags().StringVarP(&replicasetCartridgeReplicasetsFile, "file", "", "", + `file where replicasets configuration is described (default "/instances.yml")`) + cmd.Flags().StringVarP(&replicasetReplicasetName, "replicaset", "", + "", "replicaset name for an instance bootstrapping") + cmd.Flags().IntVarP(&replicasetTimeout, "timeout", "", replicasetcmd. + VShardBootstrapDefaultTimeout, "timeout") + + return cmd +} + // newBootstrapVShardCmd creates a "vshard bootstrap" command. func newBootstrapVShardCmd() *cobra.Command { cmd := &cobra.Command{ @@ -202,6 +233,7 @@ func NewReplicasetCmd() *cobra.Command { cmd.AddCommand(newDemoteCmd()) cmd.AddCommand(newExpelCmd()) cmd.AddCommand(newVShardCmd()) + cmd.AddCommand(newBootstrapCmd()) return cmd } @@ -452,6 +484,32 @@ func internalReplicasetBootstrapVShardModule(cmdCtx *cmdcontext.CmdCtx, args []s }) } +// internalReplicasetBootstrapModule is a "bootstrap" command for the "replicaset" module. +func internalReplicasetBootstrapModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { + _, instName, found := strings.Cut(args[0], string(running.InstanceDelimiter)) + + var ctx replicasetCtx + if err := replicasetFillCtx(cmdCtx, &ctx, args, true); err != nil { + return err + } + if ctx.IsInstanceConnect { + defer ctx.Conn.Close() + } + bootstrapCtx := replicasetcmd.BootstapCtx{ + ReplicasetsFile: replicasetCartridgeReplicasetsFile, + Orchestrator: ctx.Orchestrator, + RunningCtx: ctx.RunningCtx, + Timeout: replicasetTimeout, + BootstrapVShard: replicasetBootstrapVshard, + Replicaset: replicasetReplicasetName, + } + if found { + bootstrapCtx.Instance = instName + } + + return replicasetcmd.Bootstrap(bootstrapCtx) +} + // getOrchestartor returns a chosen orchestrator or an unknown one. func getOrchestrator() (replicaset.Orchestrator, error) { orchestrator := replicaset.OrchestratorUnknown diff --git a/cli/replicaset/cmd/bootstrap.go b/cli/replicaset/cmd/bootstrap.go new file mode 100644 index 000000000..045ab59f1 --- /dev/null +++ b/cli/replicaset/cmd/bootstrap.go @@ -0,0 +1,73 @@ +package replicasetcmd + +import ( + "fmt" + + "github.com/apex/log" + "github.com/tarantool/tt/cli/replicaset" + "github.com/tarantool/tt/cli/running" +) + +// BootstrapCtx describes context to bootstrap an instance or application. +type BootstapCtx struct { + // ReplicasetsFile is a Cartridge replicasets file. + ReplicasetsFile string + // Orchestrator is a forced orchestator choice. + Orchestrator replicaset.Orchestrator + // RunningCtx is an application running context. + RunningCtx running.RunningCtx + // Timeout describes a timeout in seconds. + // We keep int as it can be passed to the target instance. + Timeout int + // BootstrapVShard is true when the vshard must be bootstrapped. + BootstrapVShard bool + // Instance is an instance name to bootstrap. + Instance string + // Replicaset is a replicaset name for an instance bootstrapping. + Replicaset string +} + +// Bootstrap bootstraps an instance or application. +func Bootstrap(ctx BootstapCtx) error { + if ctx.Instance != "" { + if ctx.Replicaset == "" { + return fmt.Errorf("the replicaset must be specified to bootstrap an instance") + } + } else { + if ctx.Replicaset != "" { + return fmt.Errorf( + "the replicaset can not be specified in the case of application bootstrapping") + } + } + + orchestratorType, err := getApplicationOrchestrator(ctx.Orchestrator, + ctx.RunningCtx) + if err != nil { + return err + } + + orchestrator, err := makeApplicationOrchestrator(orchestratorType, + ctx.RunningCtx, nil, nil) + if err != nil { + return err + } + + err = orchestrator.Bootstrap(replicaset.BootstrapCtx{ + ReplicasetsFile: ctx.ReplicasetsFile, + Timeout: ctx.Timeout, + Instance: ctx.Instance, + Replicaset: ctx.Replicaset, + BootstrapVShard: ctx.BootstrapVShard, + }) + if err == nil { + // Re-discovery and log topology. + replicasets, err := orchestrator.Discovery(replicaset.SkipCache) + if err != nil { + return err + } + statusReplicasets(replicasets) + fmt.Println() + log.Info("Done.") + } + return err +} diff --git a/cli/replicaset/cmd/common.go b/cli/replicaset/cmd/common.go index 0197e8064..546d04e69 100644 --- a/cli/replicaset/cmd/common.go +++ b/cli/replicaset/cmd/common.go @@ -21,6 +21,7 @@ type replicasetOrchestrator interface { replicaset.Demoter replicaset.Expeller replicaset.VShardBootstrapper + replicaset.Bootstrapper } // makeApplicationOrchestrator creates an orchestrator for the application. diff --git a/test/integration/replicaset/test_replicaset_bootstrap.py b/test/integration/replicaset/test_replicaset_bootstrap.py new file mode 100644 index 000000000..d4e943e01 --- /dev/null +++ b/test/integration/replicaset/test_replicaset_bootstrap.py @@ -0,0 +1,124 @@ +import os +import re +import shutil + +import pytest + +from utils import get_tarantool_version, run_command_and_get_output, wait_file + +tarantool_major_version, tarantool_minor_version = get_tarantool_version() + + +@pytest.mark.skipif(tarantool_major_version > 2, + reason="skip custom test for Tarantool > 2") +@pytest.mark.parametrize("case", [["--config", "--custom"], + ["--custom", "--cartridge"], + ["--config", "--cartridge"], + ["--config", "--custom", "--cartridge"]]) +def test_bootstrap(tt_cmd, tmpdir_with_cfg, case): + cmd = [tt_cmd, "rs", "bootstrap"] + case + ["app:instance"] + rc, out = run_command_and_get_output(cmd, cwd=tmpdir_with_cfg) + assert rc == 1 + assert re.search(r" ⨯ only one type of orchestrator can be forced", out) + + +@pytest.mark.skipif(tarantool_major_version > 2, + reason="skip custom test for Tarantool > 2") +def test_bootstrap_no_instance(tt_cmd, tmpdir_with_cfg): + tmpdir = tmpdir_with_cfg + app_name = "test_custom_app" + app_path = os.path.join(tmpdir, app_name) + shutil.copytree(os.path.join(os.path.dirname(__file__), app_name), app_path) + + status_cmd = [tt_cmd, "rs", "bootstrap", "test_custom_app:unexist"] + rc, out = run_command_and_get_output(status_cmd, cwd=tmpdir_with_cfg) + assert rc == 1 + assert re.search(r" ⨯ instance \"unexist\" not found", out) + + +@pytest.mark.skipif(tarantool_major_version > 2, + reason="skip custom test for Tarantool > 2") +@pytest.mark.parametrize("flag", [None, "--custom"]) +def test_bootstrap_custom_app(tt_cmd, tmpdir_with_cfg, flag): + tmpdir = tmpdir_with_cfg + app_name = "test_custom_app" + app_path = os.path.join(tmpdir, app_name) + shutil.copytree(os.path.join(os.path.dirname(__file__), app_name), app_path) + try: + # Start a cluster. + start_cmd = [tt_cmd, "start", app_name] + rc, out = run_command_and_get_output(start_cmd, cwd=tmpdir) + assert rc == 0 + + # Check for start. + file = wait_file(os.path.join(tmpdir, app_name), 'ready', []) + assert file != "" + + cmd = [tt_cmd, "rs", "bootstrap"] + if flag: + cmd.append(flag) + cmd.append("test_custom_app") + + rc, out = run_command_and_get_output(cmd, cwd=tmpdir) + assert rc == 1 + expected = '⨯ bootstrap is not supported for an application by "custom" orchestrator' + assert expected in out + finally: + stop_cmd = [tt_cmd, "stop", app_name] + rc, _ = run_command_and_get_output(stop_cmd, cwd=tmpdir) + assert rc == 0 + + +@pytest.mark.skipif(tarantool_major_version > 2, + reason="skip custom test for Tarantool > 2") +def test_bootstrap_instance_no_replicaset_specified(tt_cmd, tmpdir_with_cfg): + tmpdir = tmpdir_with_cfg + app_name = "test_custom_app" + app_path = os.path.join(tmpdir, app_name) + shutil.copytree(os.path.join(os.path.dirname(__file__), app_name), app_path) + try: + # Start a cluster. + start_cmd = [tt_cmd, "start", app_name] + rc, out = run_command_and_get_output(start_cmd, cwd=tmpdir) + assert rc == 0 + + # Check for start. + file = wait_file(os.path.join(tmpdir, app_name), 'ready', []) + assert file != "" + + cmd = [tt_cmd, "rs", "bootstrap", "test_custom_app:test_custom_app"] + rc, out = run_command_and_get_output(cmd, cwd=tmpdir) + assert rc != 0 + assert "⨯ the replicaset must be specified to bootstrap an instance" in out + finally: + stop_cmd = [tt_cmd, "stop", app_name] + rc, _ = run_command_and_get_output(stop_cmd, cwd=tmpdir) + assert rc == 0 + + +@pytest.mark.skipif(tarantool_major_version > 2, + reason="skip custom test for Tarantool > 2") +def test_bootstrap_app_replicaset_specified(tt_cmd, tmpdir_with_cfg): + tmpdir = tmpdir_with_cfg + app_name = "test_custom_app" + app_path = os.path.join(tmpdir, app_name) + shutil.copytree(os.path.join(os.path.dirname(__file__), app_name), app_path) + try: + # Start a cluster. + start_cmd = [tt_cmd, "start", app_name] + rc, out = run_command_and_get_output(start_cmd, cwd=tmpdir) + assert rc == 0 + + # Check for start. + file = wait_file(os.path.join(tmpdir, app_name), 'ready', []) + assert file != "" + + cmd = [tt_cmd, "rs", "bootstrap", "--replicaset", "r1", "test_custom_app"] + rc, out = run_command_and_get_output(cmd, cwd=tmpdir) + assert rc != 0 + expected = "⨯ the replicaset can not be specified in the case of application bootstrapping" + assert expected in out + finally: + stop_cmd = [tt_cmd, "stop", app_name] + rc, _ = run_command_and_get_output(stop_cmd, cwd=tmpdir) + assert rc == 0