Skip to content

Commit

Permalink
Add support "%SHARD_NUM%" placeholder for vmagent sts/deployment (#508)…
Browse files Browse the repository at this point in the history
… (#598)

* Add support "%SHARD_NUM%" placeholder for fields vmagent sts (#508)

* Fix code review comments

* Fix tests
  • Loading branch information
Amper authored Mar 2, 2023
1 parent 13f2585 commit d2c5ddb
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 3 deletions.
37 changes: 37 additions & 0 deletions controllers/factory/k8stools/placeholders.go
Original file line number Diff line number Diff line change
@@ -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
}
191 changes: 191 additions & 0 deletions controllers/factory/k8stools/placeholders_test.go
Original file line number Diff line number Diff line change
@@ -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
}
})
}
}
27 changes: 24 additions & 3 deletions controllers/factory/vmagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down

0 comments on commit d2c5ddb

Please sign in to comment.