diff --git a/controllers/factory/k8stools/placeholders.go b/controllers/factory/k8stools/placeholders.go new file mode 100644 index 00000000..7f22451f --- /dev/null +++ b/controllers/factory/k8stools/placeholders.go @@ -0,0 +1,37 @@ +package k8stools + +import ( + "encoding/json" + "fmt" + "strings" +) + +// RenderPlaceholders replaces placeholders at resource with given values +// placeholder must be in %NAME% format +// resource must be reference to json serializable struct +func RenderPlaceholders[T any](resource *T, placeholders map[string]string) (*T, error) { + if resource == nil || len(placeholders) == 0 { + return resource, nil + } + + data, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("failed to marshal resource for filling placeholders: %w", err) + } + + strData := string(data) + for p, value := range placeholders { + if !strings.HasPrefix(p, "%") || !strings.HasSuffix(p, "%") { + return nil, fmt.Errorf("incorrect placeholder name format: '%v', placeholder must be in '%%NAME%%' format", p) + } + strData = strings.ReplaceAll(strData, p, value) + } + + var result *T + err = json.Unmarshal([]byte(strData), &result) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal resource after filling placeholders: %w", err) + } + + return result, nil +} diff --git a/controllers/factory/k8stools/placeholders_test.go b/controllers/factory/k8stools/placeholders_test.go new file mode 100644 index 00000000..bb6d45ec --- /dev/null +++ b/controllers/factory/k8stools/placeholders_test.go @@ -0,0 +1,191 @@ +package k8stools_test + +import ( + "testing" + + "github.com/VictoriaMetrics/operator/controllers/factory/k8stools" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestRenderPlaceholders(t *testing.T) { + type args struct { + resource any + placeholders map[string]string + } + tests := []struct { + name string + args args + want any + wantErr bool + }{ + { + name: "render without placeholders", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "value_1", + "key_2": "value_2", + }, + }, + }, + want: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "value_1", + "key_2": "value_2", + }, + }, + }, + { + name: "render without placeholders, but with specified values", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "value_1", + "key_2": "value_2", + }, + }, + placeholders: map[string]string{ + "%PLACEHOLDER_1%": "new_value_1", + "%PLACEHOLDER_2%": "new_value_2", + }, + }, + want: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "value_1", + "key_2": "value_2", + }, + }, + }, + { + name: "render with placeholders and specified values", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "%PLACEHOLDER_1%", + "key_2": "%PLACEHOLDER_2%", + "key_3": "%PLACEHOLDER_3%", + "key_4": "value_4", + }, + }, + placeholders: map[string]string{ + "%PLACEHOLDER_1%": "new_value_1", + "%PLACEHOLDER_2%": "new_value_2", + "%PLACEHOLDER_4%": "new_value_4", + }, + }, + want: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "new_value_1", + "key_2": "new_value_2", + "key_3": "%PLACEHOLDER_3%", + "key_4": "value_4", + }, + }, + }, + { + name: "render without combined placeholders in different places of resource", + args: args{ + resource: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-%PLACEHOLDER_3%-%PLACEHOLDER_4%-configmap", + }, + Data: map[string]string{ + "%PLACEHOLDER_1%_%PLACEHOLDER_2%": "bla_%PLACEHOLDER_4%_bla", + "%PLACEHOLDER_3%": "from %PLACEHOLDER_1% to %PLACEHOLDER_4%", + "key": "value", + }, + }, + placeholders: map[string]string{ + "%PLACEHOLDER_1%": "new-value-1", + "%PLACEHOLDER_2%": "new-value-2", + "%PLACEHOLDER_3%": "new-value-3", + "%PLACEHOLDER_4%": "new-value-4", + }, + }, + want: &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-new-value-3-new-value-4-configmap", + }, + Data: map[string]string{ + "new-value-1_new-value-2": "bla_new-value-4_bla", + "new-value-3": "from new-value-1 to new-value-4", + "key": "value", + }, + }, + }, + { + name: "placeholder with % in value", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "%PLACEHOLDER_1%", + "key_2": "%PLACEHOLDER_2%", + }, + }, + placeholders: map[string]string{ + "%PLACEHOLDER_1%": "%PLACEHOLDER_1%", + "%PLACEHOLDER_2%": "%PLACEHOLDER_2%", + }, + }, + want: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "%PLACEHOLDER_1%", + "key_2": "%PLACEHOLDER_2%", + }, + }, + }, + { + name: "placeholder with incorrect name 1", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "%PLACEHOLDER_1%", + }, + }, + placeholders: map[string]string{ + "PLACEHOLDER_1": "value_1", + }, + }, + wantErr: true, + }, + { + name: "placeholder with incorrect name 2", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "%PLACEHOLDER_1%", + }, + }, + placeholders: map[string]string{ + "%PLACEHOLDER_1": "value_1", + }, + }, + wantErr: true, + }, + { + name: "placeholder with incorrect name 3", + args: args{ + resource: &v1.ConfigMap{ + Data: map[string]string{ + "key_1": "%PLACEHOLDER_1%", + }, + }, + placeholders: map[string]string{ + "PLACEHOLDER_1%": "value_1", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource := tt.args.resource.(*v1.ConfigMap) + _, err := k8stools.RenderPlaceholders(resource, tt.args.placeholders) + if (err != nil) != tt.wantErr { + t.Errorf("RenderPlaceholders() error = %v, wantErr = %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/controllers/factory/vmagent.go b/controllers/factory/vmagent.go index c02017b4..e0537ed6 100644 --- a/controllers/factory/vmagent.go +++ b/controllers/factory/vmagent.go @@ -36,8 +36,12 @@ const ( vmAgentPersistentQueueMountName = "persistent-queue-data" globalRelabelingName = "global_relabeling.yaml" urlRelabelingName = "url_relabeling-%d.yaml" + shardNumPlaceholder = "%SHARD_NUM%" ) +// To save compatibility in the single-shard version still need to fill in %SHARD_NUM% placeholder +var defaultPlaceholders = map[string]string{shardNumPlaceholder: "0"} + func CreateOrUpdateVMAgentService(ctx context.Context, cr *victoriametricsv1beta1.VMAgent, rclient client.Client, c *config.BaseOperatorConf) (*corev1.Service, error) { cr = cr.DeepCopy() if cr.Spec.Port == "" { @@ -113,21 +117,30 @@ func CreateOrUpdateVMAgent(ctx context.Context, cr *victoriametricsv1beta1.VMAge if cr.Spec.ShardCount != nil && *cr.Spec.ShardCount > 1 { shardsCount := *cr.Spec.ShardCount l.Info("using cluster version of VMAgent with", "shards", shardsCount) - for i := 0; i < shardsCount; i++ { + for shardNum := 0; shardNum < shardsCount; shardNum++ { shardedDeploy := newDeploy.DeepCopyObject() - addShardSettingsToVMAgent(i, shardsCount, shardedDeploy) + addShardSettingsToVMAgent(shardNum, shardsCount, shardedDeploy) + placeholders := map[string]string{shardNumPlaceholder: strconv.Itoa(shardNum)} switch shardedDeploy := shardedDeploy.(type) { case *appsv1.Deployment: + shardedDeploy, err = k8stools.RenderPlaceholders(shardedDeploy, placeholders) + if err != nil { + return fmt.Errorf("cannot fill placeholders for deployment sharded vmagent: %w", err) + } if err := k8stools.HandleDeployUpdate(ctx, rclient, shardedDeploy); err != nil { return err } deploymentNames[shardedDeploy.Name] = struct{}{} case *appsv1.StatefulSet: + shardedDeploy, err = k8stools.RenderPlaceholders(shardedDeploy, placeholders) + if err != nil { + return fmt.Errorf("cannot fill placeholders for sts in sharded vmagent: %w", err) + } stsOpts := k8stools.STSOptions{ HasClaim: len(shardedDeploy.Spec.VolumeClaimTemplates) > 0, SelectorLabels: func() map[string]string { selectorLabels := cr.SelectorLabels() - selectorLabels["shard-num"] = strconv.Itoa(i) + selectorLabels["shard-num"] = strconv.Itoa(shardNum) return selectorLabels }, VolumeName: func() string { @@ -144,11 +157,19 @@ func CreateOrUpdateVMAgent(ctx context.Context, cr *victoriametricsv1beta1.VMAge } else { switch newDeploy := newDeploy.(type) { case *appsv1.Deployment: + newDeploy, err = k8stools.RenderPlaceholders(newDeploy, defaultPlaceholders) + if err != nil { + return fmt.Errorf("cannot fill placeholders for deployment in vmagent: %w", err) + } if err := k8stools.HandleDeployUpdate(ctx, rclient, newDeploy); err != nil { return err } deploymentNames[newDeploy.Name] = struct{}{} case *appsv1.StatefulSet: + newDeploy, err = k8stools.RenderPlaceholders(newDeploy, defaultPlaceholders) + if err != nil { + return fmt.Errorf("cannot fill placeholders for sts in vmagent: %w", err) + } stsOpts := k8stools.STSOptions{ HasClaim: len(newDeploy.Spec.VolumeClaimTemplates) > 0, SelectorLabels: cr.SelectorLabels,