github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/tools/thanosconvert/thanosconvert_test.go (about)

     1  package thanosconvert
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/oklog/ulid"
    14  	"github.com/pkg/errors"
    15  	"github.com/prometheus/prometheus/tsdb"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/mock"
    18  	"github.com/thanos-io/thanos/pkg/block/metadata"
    19  	"github.com/weaveworks/common/logging"
    20  
    21  	"github.com/cortexproject/cortex/pkg/storage/bucket"
    22  	cortex_tsdb "github.com/cortexproject/cortex/pkg/storage/tsdb"
    23  	utillog "github.com/cortexproject/cortex/pkg/util/log"
    24  )
    25  
    26  func stringNotEmpty(v string) bool {
    27  	return v != ""
    28  }
    29  
    30  type fakeBucket map[string]map[string]metadata.Meta
    31  
    32  var (
    33  	block1                 = ulid.MustNew(1, nil).String()
    34  	block2                 = ulid.MustNew(2, nil).String()
    35  	block3                 = ulid.MustNew(3, nil).String()
    36  	blockWithGetFailure    = ulid.MustNew(1000, nil).String()
    37  	blockWithUploadFailure = ulid.MustNew(1001, nil).String()
    38  	blockWithMalformedMeta = ulid.MustNew(1002, nil).String()
    39  )
    40  
    41  func TestThanosBlockConverter(t *testing.T) {
    42  	tests := []struct {
    43  		name       string
    44  		bucketData fakeBucket
    45  		assertions func(*testing.T, *bucket.ClientMock, Results, error)
    46  	}{
    47  		{
    48  			name:       "empty bucket is a noop",
    49  			bucketData: fakeBucket{},
    50  			assertions: func(t *testing.T, bkt *bucket.ClientMock, results Results, err error) {
    51  				bkt.AssertNotCalled(t, "Get", mock.Anything, mock.Anything)
    52  				bkt.AssertNotCalled(t, "Upload", mock.Anything, mock.Anything, mock.Anything)
    53  				assert.Len(t, results, 0, "expected no users in results")
    54  			},
    55  		},
    56  		{
    57  			name:       "user with no blocks is a noop",
    58  			bucketData: fakeBucket{"user1": map[string]metadata.Meta{}},
    59  			assertions: func(t *testing.T, bkt *bucket.ClientMock, results Results, err error) {
    60  				bkt.AssertNotCalled(t, "Get", mock.Anything, mock.Anything)
    61  				bkt.AssertNotCalled(t, "Upload", mock.Anything, mock.Anything, mock.Anything)
    62  				assert.Contains(t, results, "user1")
    63  				assert.Len(t, results["user1"].FailedBlocks, 0)
    64  				assert.Len(t, results["user1"].ConvertedBlocks, 0)
    65  				assert.Len(t, results["user1"].UnchangedBlocks, 0)
    66  			},
    67  		},
    68  		{
    69  			name: "bucket fully converted is a noop",
    70  			bucketData: fakeBucket{
    71  				"user1": map[string]metadata.Meta{
    72  					block1: cortexMeta("user1"),
    73  					block2: cortexMeta("user1"),
    74  					block3: cortexMeta("user1"),
    75  				},
    76  				"user2": map[string]metadata.Meta{
    77  					block1: cortexMeta("user2"),
    78  					block2: cortexMeta("user2"),
    79  				},
    80  				"user3": map[string]metadata.Meta{
    81  					block1: cortexMeta("user3"),
    82  				},
    83  			},
    84  			assertions: func(t *testing.T, bkt *bucket.ClientMock, results Results, err error) {
    85  				bkt.AssertNotCalled(t, "Upload", mock.Anything, mock.Anything, mock.Anything)
    86  				assert.Len(t, results, 3, "expected users in results")
    87  
    88  				assert.Len(t, results["user1"].FailedBlocks, 0)
    89  				assert.Len(t, results["user1"].ConvertedBlocks, 0)
    90  				assert.ElementsMatch(t, results["user1"].UnchangedBlocks, []string{block1, block2, block3})
    91  
    92  				assert.Len(t, results["user2"].FailedBlocks, 0)
    93  				assert.Len(t, results["user2"].ConvertedBlocks, 0)
    94  				assert.ElementsMatch(t, results["user2"].UnchangedBlocks, []string{block1, block2})
    95  
    96  				assert.Len(t, results["user3"].FailedBlocks, 0)
    97  				assert.Len(t, results["user3"].ConvertedBlocks, 0)
    98  				assert.ElementsMatch(t, results["user3"].UnchangedBlocks, []string{block1})
    99  
   100  			},
   101  		},
   102  		{
   103  			name: "bucket with some blocks to convert",
   104  			bucketData: fakeBucket{
   105  				"user1": map[string]metadata.Meta{
   106  					block1: cortexMeta("user1"),
   107  					block2: thanosMeta(),
   108  					block3: cortexMeta("user1"),
   109  				},
   110  				"user2": map[string]metadata.Meta{
   111  					block1: cortexMeta("user2"),
   112  					block2: cortexMeta("user2"),
   113  				},
   114  				"user3": map[string]metadata.Meta{
   115  					block1: thanosMeta(),
   116  				},
   117  			},
   118  			assertions: func(t *testing.T, bkt *bucket.ClientMock, results Results, err error) {
   119  				assert.Len(t, results, 3, "expected users in results")
   120  
   121  				assert.Len(t, results["user1"].FailedBlocks, 0)
   122  				assert.ElementsMatch(t, results["user1"].ConvertedBlocks, []string{block2})
   123  				assert.ElementsMatch(t, results["user1"].UnchangedBlocks, []string{block1, block3})
   124  
   125  				assert.Len(t, results["user2"].FailedBlocks, 0)
   126  				assert.Len(t, results["user2"].ConvertedBlocks, 0)
   127  				assert.ElementsMatch(t, results["user2"].UnchangedBlocks, []string{block1, block2})
   128  
   129  				assert.Len(t, results["user3"].FailedBlocks, 0)
   130  				assert.ElementsMatch(t, results["user3"].ConvertedBlocks, []string{block1})
   131  				assert.Len(t, results["user3"].UnchangedBlocks, 0)
   132  
   133  				bkt.AssertNumberOfCalls(t, "Upload", 2)
   134  			},
   135  		},
   136  		{
   137  			name: "bucket with failed blocks",
   138  			bucketData: fakeBucket{
   139  				"user1": map[string]metadata.Meta{
   140  					block1:              cortexMeta("user1"),
   141  					blockWithGetFailure: cortexMeta("user1"),
   142  					block3:              cortexMeta("user1"),
   143  				},
   144  				"user2": map[string]metadata.Meta{
   145  					block1: cortexMeta("user2"),
   146  					block2: cortexMeta("user2"),
   147  				},
   148  				"user3": map[string]metadata.Meta{
   149  					blockWithUploadFailure: thanosMeta(),
   150  					blockWithMalformedMeta: thanosMeta(),
   151  				},
   152  			},
   153  			assertions: func(t *testing.T, bkt *bucket.ClientMock, results Results, err error) {
   154  				assert.Len(t, results["user1"].FailedBlocks, 1)
   155  				assert.Len(t, results["user2"].FailedBlocks, 0)
   156  				assert.Len(t, results["user3"].FailedBlocks, 2)
   157  			},
   158  		},
   159  	}
   160  
   161  	for _, test := range tests {
   162  
   163  		t.Run(test.name, func(t *testing.T) {
   164  
   165  			bkt := mockBucket(test.bucketData)
   166  
   167  			converter := &ThanosBlockConverter{
   168  				logger: getLogger(),
   169  				dryRun: false,
   170  				bkt:    bkt,
   171  			}
   172  
   173  			ctx := context.Background()
   174  			results, err := converter.Run(ctx)
   175  			test.assertions(t, bkt, results, err)
   176  		})
   177  	}
   178  }
   179  
   180  func cortexMeta(user string) metadata.Meta {
   181  	return metadata.Meta{
   182  		BlockMeta: tsdb.BlockMeta{
   183  			Version: metadata.ThanosVersion1,
   184  		},
   185  		Thanos: metadata.Thanos{
   186  			Labels: map[string]string{
   187  				cortex_tsdb.TenantIDExternalLabel: user,
   188  			},
   189  		},
   190  	}
   191  }
   192  
   193  func thanosMeta() metadata.Meta {
   194  	return metadata.Meta{
   195  		BlockMeta: tsdb.BlockMeta{
   196  			Version: metadata.ThanosVersion1,
   197  		},
   198  		Thanos: metadata.Thanos{
   199  			Labels: map[string]string{
   200  				"cluster": "foo",
   201  			},
   202  		},
   203  	}
   204  }
   205  
   206  func getLogger() log.Logger {
   207  
   208  	l := logging.Level{}
   209  	if err := l.Set("info"); err != nil {
   210  		panic(err)
   211  	}
   212  	f := logging.Format{}
   213  	if err := f.Set("logfmt"); err != nil {
   214  		panic(err)
   215  	}
   216  
   217  	logger, err := utillog.NewPrometheusLogger(l, f)
   218  	if err != nil {
   219  		panic(err)
   220  	}
   221  	return logger
   222  }
   223  
   224  func mockBucket(data fakeBucket) *bucket.ClientMock {
   225  
   226  	bkt := bucket.ClientMock{}
   227  
   228  	// UsersScanner checks for deletion marks using Exist
   229  	bkt.On("Exists", mock.Anything, mock.Anything).Return(false, nil)
   230  
   231  	bkt.On("Iter", mock.Anything, "", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
   232  		// we're iterating over the top level users
   233  		f := args.Get(2).(func(string) error)
   234  		for user := range data {
   235  			if err := f(user); err != nil {
   236  				panic(err)
   237  			}
   238  		}
   239  	})
   240  
   241  	bkt.On("Iter", mock.Anything, mock.MatchedBy(stringNotEmpty), mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) {
   242  		// we're iterating over blocks within a user
   243  		user := strings.TrimSuffix(args.String(1), "/")
   244  		f := args.Get(2).(func(string) error)
   245  		if _, ok := data[user]; !ok {
   246  			panic(fmt.Sprintf("key %s not found in fake bucket data", user))
   247  		}
   248  		for block := range data[user] {
   249  			if err := f(fmt.Sprintf("%s/%s/", user, block)); err != nil {
   250  				panic(err)
   251  			}
   252  		}
   253  	})
   254  
   255  	for user, blocks := range data {
   256  		for block, meta := range blocks {
   257  
   258  			var body bytes.Buffer
   259  			enc := json.NewEncoder(&body)
   260  			if err := enc.Encode(meta); err != nil {
   261  				panic(err)
   262  			}
   263  			key := fmt.Sprintf("%s/%s/meta.json", user, block)
   264  			switch block {
   265  			case blockWithGetFailure:
   266  				bkt.On("Get", mock.Anything, key).Return(nil, errors.Errorf("test error in Get"))
   267  			case blockWithMalformedMeta:
   268  				bkt.On("Get", mock.Anything, key).Return(ioutil.NopCloser(bytes.NewBufferString("invalid json")), nil)
   269  			default:
   270  				bkt.On("Get", mock.Anything, key).Return(ioutil.NopCloser(&body), nil)
   271  			}
   272  
   273  			switch block {
   274  			case blockWithUploadFailure:
   275  				bkt.On("Upload", mock.Anything, key, mock.Anything).Return(errors.Errorf("test error in Upload"))
   276  			default:
   277  				bkt.On("Upload", mock.Anything, key, mock.Anything).Return(nil)
   278  			}
   279  		}
   280  	}
   281  
   282  	return &bkt
   283  }
   284  
   285  func TestConvertMetadata(t *testing.T) {
   286  	tests := []struct {
   287  		name            string
   288  		expectedUser    string
   289  		in              metadata.Meta
   290  		out             metadata.Meta
   291  		changesRequired []string
   292  	}{
   293  		{
   294  			name:         "no changes required",
   295  			expectedUser: "user1",
   296  			in: metadata.Meta{
   297  				Thanos: metadata.Thanos{
   298  					Labels: map[string]string{
   299  						cortex_tsdb.TenantIDExternalLabel: "user1",
   300  					},
   301  				},
   302  			},
   303  			out: metadata.Meta{
   304  				Thanos: metadata.Thanos{
   305  					Labels: map[string]string{
   306  						cortex_tsdb.TenantIDExternalLabel: "user1",
   307  					},
   308  				},
   309  			},
   310  			changesRequired: []string{},
   311  		},
   312  		{
   313  			name:         "add __org_id__ label",
   314  			expectedUser: "user1",
   315  			in: metadata.Meta{
   316  				Thanos: metadata.Thanos{
   317  					Labels: map[string]string{},
   318  				},
   319  			},
   320  			out: metadata.Meta{
   321  				Thanos: metadata.Thanos{
   322  					Labels: map[string]string{
   323  						cortex_tsdb.TenantIDExternalLabel: "user1",
   324  					},
   325  				},
   326  			},
   327  			changesRequired: []string{"add __org_id__ label"},
   328  		},
   329  		{
   330  			name:         "nil labels map",
   331  			expectedUser: "user1",
   332  			in: metadata.Meta{
   333  				Thanos: metadata.Thanos{
   334  					Labels: nil,
   335  				},
   336  			},
   337  			out: metadata.Meta{
   338  				Thanos: metadata.Thanos{
   339  					Labels: map[string]string{
   340  						cortex_tsdb.TenantIDExternalLabel: "user1",
   341  					},
   342  				},
   343  			},
   344  			changesRequired: []string{"add __org_id__ label"},
   345  		},
   346  		{
   347  			name:         "remove extra Thanos labels",
   348  			expectedUser: "user1",
   349  			in: metadata.Meta{
   350  				Thanos: metadata.Thanos{
   351  					Labels: map[string]string{
   352  						cortex_tsdb.TenantIDExternalLabel: "user1",
   353  						"extra":                           "label",
   354  						"cluster":                         "foo",
   355  					},
   356  				},
   357  			},
   358  			out: metadata.Meta{
   359  				Thanos: metadata.Thanos{
   360  					Labels: map[string]string{
   361  						cortex_tsdb.TenantIDExternalLabel: "user1",
   362  					},
   363  				},
   364  			},
   365  			changesRequired: []string{"remove extra Thanos labels"},
   366  		},
   367  		{
   368  			name:         "fix __org_id__",
   369  			expectedUser: "user1",
   370  			in: metadata.Meta{
   371  				Thanos: metadata.Thanos{
   372  					Labels: map[string]string{
   373  						cortex_tsdb.TenantIDExternalLabel: "wrong_user",
   374  					},
   375  				},
   376  			},
   377  			out: metadata.Meta{
   378  				Thanos: metadata.Thanos{
   379  					Labels: map[string]string{
   380  						cortex_tsdb.TenantIDExternalLabel: "user1",
   381  					},
   382  				},
   383  			},
   384  			changesRequired: []string{"change __org_id__ from wrong_user to user1"},
   385  		},
   386  	}
   387  
   388  	for _, test := range tests {
   389  		t.Run(test.name, func(t *testing.T) {
   390  			out, changesRequired := convertMetadata(test.in, test.expectedUser)
   391  			assert.Equal(t, test.out, out)
   392  			assert.ElementsMatch(t, changesRequired, test.changesRequired)
   393  		})
   394  	}
   395  }