Skip to content

Commit

Permalink
cluster: publish a new instance configuration
Browse files Browse the repository at this point in the history
@TarantoolBot document
Title: `tt cluster publish` can publish a new instance configuration

This patchs adds an ability to publish a new instance configuration with `tt cluster publish`.
Prior this patch this command returns an error if the instance is not found in the config.

Now, to create a new instance configuration, a replicaset name with `--replicaset` can be specified,
and, if it's not enough, a group name with `--group`.

For example:
```sh
$ tt cluster show http://localhost:2379/foo
/foo/config/all
credentials:
  users:
    guest:
      roles:
        - super
groups:
  group-001:
    replicasets:
      replicaset-001:
        instances:
          instance-001:
            iproto:
              listen:
                - uri: localhost:3301
          instance-002:
            iproto:
              listen: {}

$ tt cluster publish http://localhost:2379/foo?name=instance-003 inst.yml
   ⨯ failed to create an instance "instance-003" configuration: replicaset name is not specified

$ tt cluster publish --replicaset replicaset-001  http://localhost:2379/foo?name=instance-003 inst.yml

$ tt cluster show http://localhost:2379/foo
credentials:
  users:
    guest:
      roles:
        - super
groups:
  group-001:
    replicasets:
      replicaset-001:
        instances:
          instance-001:
            iproto:
              listen:
                - uri: localhost:3301
          instance-002:
            iproto:
              listen: {}
          instance-003:
            database:
              mode: rw
```

Part of #316
  • Loading branch information
askalt authored and psergee committed May 27, 2024
1 parent 92a65f1 commit efe95f1
Show file tree
Hide file tree
Showing 9 changed files with 470 additions and 85 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
* `tt coredump inspect`: allows archive path as an argument (archive should be
created with `tt coredump pack`).
* `tt coredump inspect`: added `-s` option to specify the location of tarantool sources.
- `tt cluster publish`: ability to publish a new instance config.

### Added

Expand Down
49 changes: 42 additions & 7 deletions cli/cluster/cmd/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type PublishCtx struct {
Src []byte
// Config is a parsed raw data configuration to publish.
Config *libcluster.Config
// Group is a group name for a new instance configuration publishing.
Group string
// Replicaset is a replicaset name for a new instance configuration publishing.
Replicaset string
}

// PublishUri publishes a configuration to URI.
Expand Down Expand Up @@ -57,7 +61,8 @@ func PublishUri(publishCtx PublishCtx, uri *url.URL) error {
return publisher.Publish(0, publishCtx.Src)
}

return replaceInstanceConfig(instance, publishCtx.Config, collector, publisher)
return setInstanceConfig(publishCtx.Group, publishCtx.Replicaset, instance,
publishCtx.Config, collector, publisher)
}

// PublishCluster publishes a configuration to the configuration path.
Expand All @@ -81,7 +86,8 @@ func PublishCluster(publishCtx PublishCtx, path, instance string) error {
return fmt.Errorf("failed to create a file collector: %w", err)
}

return replaceInstanceConfig(instance, publishCtx.Config, collector, publisher)
return setInstanceConfig(publishCtx.Group, publishCtx.Replicaset, instance,
publishCtx.Config, collector, publisher)
}

// publishCtxValidateConfig validates a source configuration from the publish
Expand All @@ -93,9 +99,9 @@ func publishCtxValidateConfig(publishCtx PublishCtx, instance string) error {
return nil
}

// replaceInstanceConfig replaces an instance configuration in the collected
// setInstanceConfig sets an instance configuration in the collected
// cluster configuration and republishes it.
func replaceInstanceConfig(instance string, config *libcluster.Config,
func setInstanceConfig(group, replicaset, instance string, config *libcluster.Config,
collector libcluster.Collector, publisher libcluster.DataPublisher) error {
src, err := collector.Collect()
if err != nil {
Expand All @@ -108,10 +114,39 @@ func replaceInstanceConfig(instance string, config *libcluster.Config,
return fmt.Errorf("failed to parse a target configuration: %w", err)
}

cconfig, err = libcluster.ReplaceInstanceConfig(cconfig, instance, config)
gname, rname, found := libcluster.FindInstance(cconfig, instance)
if found {
// Instance is present in the configuration.
if replicaset != "" && replicaset != rname {
return fmt.Errorf("wrong replicaset name, expected %q, have %q", rname, replicaset)
}
if group != "" && group != gname {
return fmt.Errorf("wrong group name, expected %q, have %q", gname, group)
}
cconfig, err = libcluster.ReplaceInstanceConfig(cconfig, instance, config)
if err != nil {
return fmt.Errorf("failed to replace an instance %q configuration "+
"in a cluster configuration: %w", instance, err)
}
return libcluster.NewYamlConfigPublisher(publisher).Publish(cconfig.RawConfig)
}

if replicaset == "" {
return fmt.Errorf(
"replicaset name is not specified for %q instance configuration", instance)
}
if group == "" {
// Try to determine a group.
var found bool
group, found = libcluster.FindGroupByReplicaset(cconfig, replicaset)
if !found {
return fmt.Errorf("failed to determine the group of the %q replicaset", replicaset)
}
}
cconfig, err = libcluster.SetInstanceConfig(cconfig, group, replicaset,
instance, config)
if err != nil {
return fmt.Errorf("failed to replace an instance %q configuration "+
"in a cluster configuration: %w", instance, err)
return fmt.Errorf("failed to set an instance %q configuration: %w", instance, err)
}

return libcluster.NewYamlConfigPublisher(publisher).Publish(cconfig.RawConfig)
Expand Down
71 changes: 36 additions & 35 deletions cli/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"io"
"net/url"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -21,20 +20,18 @@ import (
"github.com/tarantool/tt/lib/integrity"
)

const (
defaultConfigFileName = "config.yaml"
)

var showCtx = clustercmd.ShowCtx{
Username: "",
Password: "",
Validate: false,
}

var publishCtx = clustercmd.PublishCtx{
Username: "",
Password: "",
Force: false,
Username: "",
Password: "",
Group: "",
Replicaset: "",
Force: false,
}

var promoteCtx = clustercmd.PromoteCtx{
Expand Down Expand Up @@ -227,6 +224,9 @@ func NewClusterCmd() *cobra.Command {
"https://user:pass@localhost:2379/tt cluster.yaml\n" +
" tt cluster publish " +
"https://user:pass@localhost:2379/tt?name=instance " +
"instance.yaml\n" +
" tt cluster publish --group group --replicaset replicaset " +
"https://user:pass@localhost:2379/tt?name=instance " +
"instance.yaml",
Run: func(cmd *cobra.Command, args []string) {
cmdCtx.CommandName = cmd.Name()
Expand All @@ -249,6 +249,8 @@ func NewClusterCmd() *cobra.Command {
"username (used as etcd credentials only)")
publish.Flags().StringVarP(&publishCtx.Password, "password", "p", "",
"password (used as etcd credentials only)")
publish.Flags().StringVarP(&publishCtx.Group, "group", "", "", "group name")
publish.Flags().StringVarP(&publishCtx.Replicaset, "replicaset", "", "", "replicaset name")
publish.Flags().BoolVar(&publishCtx.Force, "force", publishCtx.Force,
"force publish and skip validation")
// Integrity flags.
Expand Down Expand Up @@ -281,16 +283,15 @@ func internalClusterShowModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
}

// It looks like an application or an application:instance.
instanceCtx, name, err := parseAppStr(cmdCtx, args[0])
configPath, _, instName, err := parseAppStr(cmdCtx, args[0])
if err != nil {
return err
}

if instanceCtx.ClusterConfigPath == "" {
if configPath == "" {
return fmt.Errorf("cluster configuration file does not exist for the application")
}

return clustercmd.ShowCluster(showCtx, instanceCtx.ClusterConfigPath, name)
return clustercmd.ShowCluster(showCtx, configPath, instName)
}

// internalClusterPublishModule is an entrypoint for `cluster publish` command.
Expand All @@ -315,21 +316,22 @@ func internalClusterPublishModule(cmdCtx *cmdcontext.CmdCtx, args []string) erro
}

// It looks like an application or an application:instance.
instanceCtx, name, err := parseAppStr(cmdCtx, args[0])
configPath, appName, instName, err := parseAppStr(cmdCtx, args[0])
if err != nil {
return err
}

configPath := instanceCtx.ClusterConfigPath
if configPath == "" {
if name != "" {
if instName != "" {
return fmt.Errorf("can not to update an instance configuration " +
"if a cluster configuration file does not exist for the application")
}
configPath = filepath.Join(instanceCtx.AppDir, defaultConfigFileName)
configPath, err = running.GetClusterConfigPath(cliOpts,
cmdCtx.Cli.ConfigDir, appName, false)
if err != nil {
return err
}
}

return clustercmd.PublishCluster(publishCtx, configPath, name)
return clustercmd.PublishCluster(publishCtx, configPath, instName)
}

// internalClusterReplicasetPromoteModule is a "cluster replicaset promote" command.
Expand Down Expand Up @@ -421,29 +423,28 @@ func parseUrl(str string) (*url.URL, error) {
return nil, fmt.Errorf("specified string can not be recognized as URL")
}

// parseAppStr parses a string and returns an application instance context
// and an application instance name or an error.
func parseAppStr(cmdCtx *cmdcontext.CmdCtx, appStr string) (running.InstanceCtx, string, error) {
var (
runningCtx running.RunningCtx
name string
)

// parseAppStr parses a string and returns an application cluster config path,
// application name and instance name or an error.
func parseAppStr(cmdCtx *cmdcontext.CmdCtx, appStr string) (string, string, string, error) {
if !isConfigExist(cmdCtx) {
return running.InstanceCtx{},
"",
return "", "", "",
fmt.Errorf("unable to resolve the application name %q: %w", appStr, errNoConfig)
}

err := running.FillCtx(cliOpts, cmdCtx, &runningCtx, []string{appStr})
appName, instName, _ := strings.Cut(appStr, string(running.InstanceDelimiter))

// Fill context for the entire application.
// publish app:inst can work even if the `inst` instance doesn't exist right now.
var runningCtx running.RunningCtx
err := running.FillCtx(cliOpts, cmdCtx, &runningCtx, []string{appName})
if err != nil {
return running.InstanceCtx{}, "", err
return "", "", "", err
}

colonIds := strings.Index(appStr, string(running.InstanceDelimiter))
if colonIds != -1 {
name = runningCtx.Instances[0].InstName
configPath := ""
if len(runningCtx.Instances) != 0 {
configPath = runningCtx.Instances[0].ClusterConfigPath
}

return runningCtx.Instances[0], name, nil
return configPath, appName, instName, nil
}
39 changes: 35 additions & 4 deletions cli/running/running.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ import (

const defaultDirPerms = 0770

// stateBoardInstName is cartridge stateboard instance name.
const stateBoardInstName = "stateboard"
const (
// stateBoardInstName is cartridge stateboard instance name.
stateBoardInstName = "stateboard"

// clusterConfigDefaultFileName is a default filename for the cluster config.
// When using, make sure that both "yml" and "yaml" are considered.
clusterConfigDefaultFileName = "config.yml"
)

var (
instStateStopped = process_utils.ProcStateStopped
Expand Down Expand Up @@ -248,7 +254,7 @@ func collectAppDirFiles(appDir string) (appDirCtx appDirCtx, err error) {
}

if appDirCtx.clusterCfgPath, err = util.GetYamlFileName(
filepath.Join(appDir, "config.yml"), false); err != nil {
filepath.Join(appDir, clusterConfigDefaultFileName), false); err != nil {
return
}

Expand Down Expand Up @@ -581,6 +587,32 @@ func renderInstCtxMembers(instance *InstanceCtx) error {
return nil
}

// GetClusterConfigPath returns a cluster config path for the application.
// If mustExist flag is set and config is not found, ErrNotExists error is returned,
// default config filepath is returned otherwise.
func GetClusterConfigPath(cliOpts *config.CliOpts,
ttConfigDir, appName string, mustExist bool) (string, error) {
instEnabledPath := cliOpts.Env.InstancesEnabled
var appDir string
if instEnabledPath == "." {
appDir = ttConfigDir
} else {
appDir = filepath.Join(instEnabledPath, appName)
}
configPath := filepath.Join(appDir, clusterConfigDefaultFileName)
ret, err := util.GetYamlFileName(configPath, true)
if errors.Is(err, os.ErrNotExist) {
if mustExist {
return "", err
}
return configPath, nil
}
if err != nil {
return "", err
}
return ret, nil
}

// CollectInstancesForApps collects instances information per application.
func CollectInstancesForApps(appList []string, cliOpts *config.CliOpts,
ttConfigDir string, integrityCtx integrity.IntegrityCtx) (
Expand All @@ -589,7 +621,6 @@ func CollectInstancesForApps(appList []string, cliOpts *config.CliOpts,
if cliOpts.Env.InstancesEnabled == "." {
instEnabledPath = ttConfigDir
}

apps := make(map[string][]InstanceCtx)
for _, appName := range appList {
appName = strings.TrimSuffix(appName, ".lua")
Expand Down
63 changes: 63 additions & 0 deletions cli/running/running_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tarantool/tt/cli/cmdcontext"
"github.com/tarantool/tt/cli/config"
"github.com/tarantool/tt/cli/configure"
"github.com/tarantool/tt/lib/integrity"
"golang.org/x/exp/slices"
Expand Down Expand Up @@ -411,3 +412,65 @@ func TestGetAppPath(t *testing.T) {
}))

}

func TestGetClusterConfigPath(t *testing.T) {
instEnabled := filepath.Join("testdata", "instances_enabled")
defaultCliOpts := &config.CliOpts{Env: &config.TtEnvOpts{InstancesEnabled: instEnabled}}
cases := []struct {
cliOpts *config.CliOpts
ttConfigDir string
app string
mustExist bool
expected string
wantErr bool
}{
{
cliOpts: defaultCliOpts,
app: "cluster_app",
mustExist: true,
expected: filepath.Join(instEnabled, "cluster_app", "config.yml"),
},
{
cliOpts: &config.CliOpts{Env: &config.TtEnvOpts{InstancesEnabled: instEnabled}},
app: "cluster_app_yaml_config_extension",
mustExist: true,
expected: filepath.Join(instEnabled, "cluster_app_yaml_config_extension",
"config.yaml"),
},
{
cliOpts: defaultCliOpts,
app: "single_inst",
mustExist: true,
wantErr: true,
},
{
cliOpts: defaultCliOpts,
app: "single_inst",
mustExist: false,
expected: filepath.Join(instEnabled, "single_inst", "config.yml"),
},
{
cliOpts: &config.CliOpts{
Env: &config.TtEnvOpts{
InstancesEnabled: ".",
},
},
ttConfigDir: filepath.Join(instEnabled, "cluster_app"),
app: "cluster_app",
mustExist: true,
expected: filepath.Join(instEnabled, "cluster_app", "config.yml"),
},
}

for _, tc := range cases {
t.Run(tc.app, func(t *testing.T) {
actual, err := GetClusterConfigPath(tc.cliOpts, tc.ttConfigDir, tc.app, tc.mustExist)
if tc.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.expected, actual)
})
}
}
Loading

0 comments on commit efe95f1

Please sign in to comment.