diff --git a/go.mod b/go.mod index d1862a7d..288f3416 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/onsi/ginkgo/v2 v2.4.0 github.com/onsi/gomega v1.23.0 github.com/spf13/cobra v1.6.1 + github.com/stretchr/testify v1.8.2 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.11.1 k8s.io/api v0.26.0 @@ -102,6 +103,7 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc2 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect diff --git a/pkg/cmd/gtctl/cluster/create/config.go b/pkg/cmd/gtctl/cluster/create/config.go new file mode 100644 index 00000000..f94c0f8b --- /dev/null +++ b/pkg/cmd/gtctl/cluster/create/config.go @@ -0,0 +1,90 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package create + +import ( + "fmt" + "strings" +) + +const ( + // Various of support config type + configOperator = "operator" + configCluster = "cluster" + configEtcd = "etcd" +) + +type configValues struct { + rawConfig []string + + operatorConfig string + clusterConfig string + etcdConfig string +} + +// parseConfig parse raw config values and classify it to different +// categories of config type by its prefix. +func (c *configValues) parseConfig() error { + var ( + operatorConfig []string + clusterConfig []string + etcdConfig []string + ) + + for _, raw := range c.rawConfig { + if len(raw) == 0 { + return fmt.Errorf("cannot parse empty config values") + } + + var configPrefix, configValue string + values := strings.Split(raw, ",") + + for _, value := range values { + value = strings.Trim(value, " ") + config := strings.SplitN(value, ".", 2) + configPrefix = config[0] + if len(config) == 2 { + configValue = config[1] + } else { + configValue = configPrefix + } + + switch configPrefix { + case configOperator: + operatorConfig = append(operatorConfig, configValue) + case configCluster: + clusterConfig = append(clusterConfig, configValue) + case configEtcd: + etcdConfig = append(etcdConfig, configValue) + default: + clusterConfig = append(clusterConfig, value) + } + } + } + + if len(operatorConfig) > 0 { + c.operatorConfig = strings.Join(operatorConfig, ",") + } + + if len(clusterConfig) > 0 { + c.clusterConfig = strings.Join(clusterConfig, ",") + } + + if len(etcdConfig) > 0 { + c.etcdConfig = strings.Join(etcdConfig, ",") + } + + return nil +} diff --git a/pkg/cmd/gtctl/cluster/create/config_test.go b/pkg/cmd/gtctl/cluster/create/config_test.go new file mode 100644 index 00000000..6549c3a0 --- /dev/null +++ b/pkg/cmd/gtctl/cluster/create/config_test.go @@ -0,0 +1,80 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package create + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseConfig(t *testing.T) { + testCases := []struct { + name string + config []string + expect configValues + err bool + }{ + { + name: "all-with-prefix", + config: []string{"cluster.foo=bar", "etcd.foo=bar", "operator.foo=bar"}, + expect: configValues{ + clusterConfig: "foo=bar", + etcdConfig: "foo=bar", + operatorConfig: "foo=bar", + }, + }, + { + name: "all-without-prefix", + config: []string{"foo=bar", "foo.boo=bar", "foo.boo.coo=bar"}, + expect: configValues{ + clusterConfig: "foo=bar,foo.boo=bar,foo.boo.coo=bar", + }, + }, + { + name: "mix-with-prefix", + config: []string{"etcd.foo=bar", "foo.boo=bar", "foo.boo.coo=bar"}, + expect: configValues{ + clusterConfig: "foo.boo=bar,foo.boo.coo=bar", + etcdConfig: "foo=bar", + }, + }, + { + name: "empty-values", + config: []string{""}, + err: true, + }, + { + name: "empty-config", + config: []string{}, + expect: configValues{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := configValues{rawConfig: tc.config} + err := actual.parseConfig() + + if tc.err { + assert.Error(t, err) + return + } + + tc.expect.rawConfig = tc.config + assert.Equal(t, tc.expect, actual) + }) + } +} diff --git a/pkg/cmd/gtctl/cluster/create/create.go b/pkg/cmd/gtctl/cluster/create/create.go index d98b228d..33e415ce 100644 --- a/pkg/cmd/gtctl/cluster/create/create.go +++ b/pkg/cmd/gtctl/cluster/create/create.go @@ -17,7 +17,6 @@ package create import ( "context" "fmt" - "io/ioutil" "os" "time" @@ -59,6 +58,7 @@ type createClusterCliOptions struct { // Common options. Timeout int DryRun bool + Set configValues } func NewCreateClusterCommand(l logger.Logger) *cobra.Command { @@ -96,18 +96,23 @@ func NewCreateClusterCommand(l logger.Logger) *cobra.Command { l.V(0).Infof("Creating GreptimeDB cluster '%s' on bare-metal environment...", logger.Bold(clusterName)) } + // Parse config values that set in command line + if err = options.Set.parseConfig(); err != nil { + return err + } + if !options.BareMetal { - if err := deployGreptimeDBOperator(ctx, l, &options, spinner, clusterDeployer); err != nil { + if err = deployGreptimeDBOperator(ctx, l, &options, spinner, clusterDeployer); err != nil { return err } } - if err := deployEtcdCluster(ctx, l, &options, spinner, clusterDeployer, clusterName); err != nil { + if err = deployEtcdCluster(ctx, l, &options, spinner, clusterDeployer, clusterName); err != nil { spinner.Stop(false, "Installing etcd cluster failed") return err } - if err := deployGreptimeDBCluster(ctx, l, &options, spinner, clusterDeployer, clusterName); err != nil { + if err = deployGreptimeDBCluster(ctx, l, &options, spinner, clusterDeployer, clusterName); err != nil { return err } @@ -144,10 +149,11 @@ func NewCreateClusterCommand(l logger.Logger) *cobra.Command { cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") cmd.Flags().BoolVar(&options.DryRun, "dry-run", false, "Output the manifests without applying them.") cmd.Flags().IntVar(&options.Timeout, "timeout", -1, "Timeout in seconds for the command to complete, default is no timeout.") + cmd.Flags().StringArrayVar(&options.Set.rawConfig, "set", []string{}, "set values on the command line for greptimedb cluster, etcd and operator (can specify multiple or separate values with commas: eg. cluster.key1=val1,etcd.key2=val2).") cmd.Flags().StringVar(&options.GreptimeDBChartVersion, "greptimedb-chart-version", "", "The greptimedb helm chart version, use latest version if not specified.") cmd.Flags().StringVar(&options.GreptimeDBOperatorChartVersion, "greptimedb-operator-chart-version", "", "The greptimedb-operator helm chart version, use latest version if not specified.") cmd.Flags().StringVar(&options.EtcdChartVersion, "etcd-chart-version", "", "The greptimedb-etcd helm chart version, use latest version if not specified.") - cmd.Flags().StringVar(&options.ImageRegistry, "image-registry", "", "The image registry") + cmd.Flags().StringVar(&options.ImageRegistry, "image-registry", "", "The image registry.") cmd.Flags().StringVar(&options.EtcdNamespace, "etcd-namespace", "default", "The namespace of etcd cluster.") cmd.Flags().StringVar(&options.EtcdStorageClassName, "etcd-storage-class-name", "standard", "The etcd storage class name.") cmd.Flags().StringVar(&options.EtcdStorageSize, "etcd-storage-size", "10Gi", "the etcd persistent volume size.") @@ -178,7 +184,7 @@ func newDeployer(l logger.Logger, clusterName string, options *createClusterCliO if options.Config != "" { var config baremetal.Config - data, err := ioutil.ReadFile(options.Config) + data, err := os.ReadFile(options.Config) if err != nil { return nil, err } @@ -210,6 +216,7 @@ func deployGreptimeDBOperator(ctx context.Context, l logger.Logger, options *cre createGreptimeDBOperatorOptions := &deployer.CreateGreptimeDBOperatorOptions{ GreptimeDBOperatorChartVersion: options.GreptimeDBOperatorChartVersion, ImageRegistry: options.ImageRegistry, + ConfigValues: options.Set.operatorConfig, } name := types.NamespacedName{Namespace: options.OperatorNamespace, Name: "greptimedb-operator"}.String() @@ -238,6 +245,7 @@ func deployEtcdCluster(ctx context.Context, l logger.Logger, options *createClus EtcdStorageClassName: options.EtcdStorageClassName, EtcdStorageSize: options.EtcdStorageSize, EtcdDataDir: options.EtcdDataDir, + ConfigValues: options.Set.etcdConfig, } var name string @@ -274,6 +282,7 @@ func deployGreptimeDBCluster(ctx context.Context, l logger.Logger, options *crea DatanodeStorageSize: options.StorageSize, DatanodeStorageRetainPolicy: options.StorageRetainPolicy, EtcdEndPoint: fmt.Sprintf("%s.%s:2379", common.EtcdClusterName(clusterName), options.EtcdNamespace), + ConfigValues: options.Set.clusterConfig, } var name string diff --git a/pkg/deployer/types.go b/pkg/deployer/types.go index a4223cc6..30453734 100644 --- a/pkg/deployer/types.go +++ b/pkg/deployer/types.go @@ -76,6 +76,7 @@ type CreateGreptimeDBClusterOptions struct { DatanodeStorageSize string `helm:"datanode.storage.storageSize"` DatanodeStorageRetainPolicy string `helm:"datanode.storage.storageRetainPolicy"` EtcdEndPoint string `helm:"etcdEndpoints"` + ConfigValues string `helm:"*"` } // UpdateGreptimeDBClusterOptions is the options to update a GreptimeDB cluster. @@ -94,6 +95,7 @@ type CreateEtcdClusterOptions struct { EtcdStorageClassName string `helm:"storage.storageClassName"` EtcdStorageSize string `helm:"storage.volumeSize"` EtcdDataDir string `helm:"storage.dataDir"` + ConfigValues string `helm:"*"` } // DeleteEtcdClusterOption is the options to delete an etcd cluster. @@ -104,4 +106,5 @@ type CreateGreptimeDBOperatorOptions struct { GreptimeDBOperatorChartVersion string ImageRegistry string `helm:"image.registry"` + ConfigValues string `helm:"*"` } diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index e575a9ac..9a2f2433 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -98,8 +98,12 @@ func (r *Render) GenerateHelmValues(input interface{}) (map[string]interface{}, typeOf := reflect.TypeOf(input) for i := 0; i < valueOf.NumField(); i++ { helmValueKey := typeOf.Field(i).Tag.Get(helmFieldTag) - if helmValueKey != "" && valueOf.Field(i).Len() > 0 { - rawArgs = append(rawArgs, fmt.Sprintf("%s=%s", helmValueKey, valueOf.Field(i))) + if len(helmValueKey) > 0 && valueOf.Field(i).Len() > 0 { + if helmValueKey == "*" { + rawArgs = append(rawArgs, valueOf.Field(i).String()) + } else { + rawArgs = append(rawArgs, fmt.Sprintf("%s=%s", helmValueKey, valueOf.Field(i))) + } } } diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go index d5323202..728d42a0 100644 --- a/pkg/helm/helm_test.go +++ b/pkg/helm/helm_test.go @@ -107,6 +107,7 @@ func TestRender_GenerateGreptimeDBHelmValues(t *testing.T) { DatanodeStorageRetainPolicy: "Delete", EtcdEndPoint: "127.0.0.1:2379", InitializerImageRegistry: "registry.cn-hangzhou.aliyuncs.com", + ConfigValues: "meta.replicas=4", } r := &Render{} @@ -122,6 +123,7 @@ func TestRender_GenerateGreptimeDBHelmValues(t *testing.T) { "datanode.storage.storageRetainPolicy=Delete", "etcdEndpoints=127.0.0.1:2379", "initializer.registry=registry.cn-hangzhou.aliyuncs.com", + "meta.replicas=4", } valuesWanted, err := strvals.Parse(strings.Join(ArgsStr, ",")) @@ -139,6 +141,7 @@ func TestRender_GenerateGreptimeDBOperatorHelmValues(t *testing.T) { options := deployer.CreateGreptimeDBOperatorOptions{ GreptimeDBOperatorChartVersion: "", ImageRegistry: "registry.cn-hangzhou.aliyuncs.com", + ConfigValues: "replicas=3", } r := &Render{} @@ -149,6 +152,7 @@ func TestRender_GenerateGreptimeDBOperatorHelmValues(t *testing.T) { ArgsStr := []string{ "image.registry=registry.cn-hangzhou.aliyuncs.com", + "replicas=3", } valuesWanted, err := strvals.Parse(strings.Join(ArgsStr, ",")) @@ -169,6 +173,7 @@ func TestRender_GenerateEtcdHelmValues(t *testing.T) { EtcdStorageClassName: "ebs-sc", EtcdStorageSize: "11Gi", EtcdDataDir: "/var/etcd", + ConfigValues: "image.tag=latest", } r := &Render{} @@ -182,6 +187,7 @@ func TestRender_GenerateEtcdHelmValues(t *testing.T) { "storage.storageClassName=ebs-sc", "storage.volumeSize=11Gi", "storage.dataDir=/var/etcd", + "image.tag=latest", } valuesWanted, err := strvals.Parse(strings.Join(ArgsStr, ","))