github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/sink/kafka/v2/admin_test.go (about)

     1  // Copyright 2023 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package v2
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"net"
    20  	"testing"
    21  
    22  	"github.com/golang/mock/gomock"
    23  	"github.com/pingcap/tiflow/cdc/model"
    24  	"github.com/pingcap/tiflow/pkg/errors"
    25  	pkafka "github.com/pingcap/tiflow/pkg/sink/kafka"
    26  	mock "github.com/pingcap/tiflow/pkg/sink/kafka/v2/mock"
    27  	"github.com/segmentio/kafka-go"
    28  	"github.com/segmentio/kafka-go/sasl/plain"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  func newClusterAdminClientWithMock(t *testing.T) (pkafka.ClusterAdminClient, *mock.MockClient) {
    33  	transport, err := newTransport(pkafka.NewOptions())
    34  	require.NoError(t, err)
    35  
    36  	changefeedID := model.DefaultChangeFeedID("changefeed-test")
    37  	adminClient := newClusterAdminClient([]string{"127.0.0.1:9092"}, transport, changefeedID)
    38  
    39  	ctrl := gomock.NewController(t)
    40  	client := mock.NewMockClient(ctrl)
    41  	adminClient.(*admin).client = client
    42  	return adminClient, client
    43  }
    44  
    45  func TestNewClusterAdminClient(t *testing.T) {
    46  	t.Parallel()
    47  
    48  	transport, err := newTransport(pkafka.NewOptions())
    49  	require.NoError(t, err)
    50  
    51  	changefeedID := model.DefaultChangeFeedID("changefeed-test")
    52  	adminClient := newClusterAdminClient([]string{"127.0.0.1:9092"}, transport, changefeedID)
    53  	require.NotNil(t, adminClient)
    54  	require.NotNil(t, adminClient.(*admin).client)
    55  }
    56  
    57  func TestGetAllBrokers(t *testing.T) {
    58  	t.Parallel()
    59  
    60  	admin, client := newClusterAdminClientWithMock(t)
    61  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
    62  		Return(nil, fmt.Errorf("kafka.(*Client).Metadata"))
    63  
    64  	ctx := context.Background()
    65  	brokers, err := admin.GetAllBrokers(ctx)
    66  	require.Error(t, err)
    67  	require.Nil(t, brokers)
    68  
    69  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
    70  		Return(&kafka.MetadataResponse{}, nil)
    71  	brokers, err = admin.GetAllBrokers(ctx)
    72  	require.NoError(t, err)
    73  	require.NotNil(t, brokers)
    74  	require.Len(t, brokers, 0)
    75  
    76  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
    77  		Return(&kafka.MetadataResponse{
    78  			Brokers: []kafka.Broker{
    79  				{ID: 1},
    80  			},
    81  		}, nil)
    82  	brokers, err = admin.GetAllBrokers(ctx)
    83  	require.NoError(t, err)
    84  	require.Len(t, brokers, 1)
    85  	require.Equal(t, int32(1), brokers[0].ID)
    86  }
    87  
    88  func TestGetBrokerConfig(t *testing.T) {
    89  	t.Parallel()
    90  
    91  	ctx := context.Background()
    92  	admin, client := newClusterAdminClientWithMock(t)
    93  
    94  	// cannot find the coordinator
    95  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
    96  		Return(nil, fmt.Errorf("kafka.(*Client).Metadata"))
    97  	result, err := admin.GetBrokerConfig(ctx, "test-config-name")
    98  	require.Error(t, err)
    99  	require.Equal(t, "", result)
   100  
   101  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   102  		Return(&kafka.MetadataResponse{
   103  			Controller: kafka.Broker{ID: 1},
   104  		}, nil).AnyTimes()
   105  
   106  	// cannot get kafka broker's config
   107  	client.EXPECT().DescribeConfigs(gomock.Any(), gomock.Any()).
   108  		Return(nil, fmt.Errorf("kafka.(*Client).DescribeConfigs"))
   109  	result, err = admin.GetBrokerConfig(ctx, "test-config-name")
   110  	require.Error(t, err)
   111  	require.Equal(t, "", result)
   112  
   113  	// config is not found
   114  	client.EXPECT().DescribeConfigs(gomock.Any(), gomock.Any()).
   115  		Return(&kafka.DescribeConfigsResponse{
   116  			Resources: []kafka.DescribeConfigResponseResource{},
   117  		}, nil)
   118  	result, err = admin.GetBrokerConfig(ctx, "test-config-name")
   119  	require.Error(t, err, errors.ErrKafkaConfigNotFound)
   120  	require.Equal(t, "", result)
   121  
   122  	// config is found
   123  	client.EXPECT().DescribeConfigs(gomock.Any(), gomock.Any()).
   124  		Return(&kafka.DescribeConfigsResponse{
   125  			Resources: []kafka.DescribeConfigResponseResource{
   126  				{
   127  					ConfigEntries: []kafka.DescribeConfigResponseConfigEntry{
   128  						{
   129  							ConfigName:  "test-config-name",
   130  							ConfigValue: "test-config-value",
   131  						},
   132  					},
   133  				},
   134  			},
   135  		}, nil)
   136  
   137  	result, err = admin.GetBrokerConfig(ctx, "test-config-name")
   138  	require.NoError(t, err)
   139  	require.Equal(t, "test-config-value", result)
   140  
   141  	// config is found, but the key does not match,
   142  	// this should not happen, but we still prevent such case.
   143  	client.EXPECT().DescribeConfigs(gomock.Any(), gomock.Any()).
   144  		Return(&kafka.DescribeConfigsResponse{
   145  			Resources: []kafka.DescribeConfigResponseResource{
   146  				{
   147  					ConfigEntries: []kafka.DescribeConfigResponseConfigEntry{
   148  						{
   149  							ConfigName:  "undesired-config-name",
   150  							ConfigValue: "undesired-config-value",
   151  						},
   152  					},
   153  				},
   154  			},
   155  		}, nil)
   156  	result, err = admin.GetBrokerConfig(ctx, "test-config-name")
   157  	require.Error(t, err, errors.ErrKafkaConfigNotFound)
   158  	require.Len(t, result, 0)
   159  }
   160  
   161  func TestGetTopicMeta(t *testing.T) {
   162  	t.Parallel()
   163  
   164  	ctx := context.Background()
   165  	admin, client := newClusterAdminClientWithMock(t)
   166  
   167  	// cannot get topics meta from kafka
   168  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   169  		Return(nil, fmt.Errorf("kafka.(*Client).Metadata"))
   170  	result, err := admin.GetTopicsMeta(ctx, []string{}, true)
   171  	require.Error(t, err)
   172  	require.Nil(t, result)
   173  
   174  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   175  		Return(&kafka.MetadataResponse{}, nil)
   176  	result, err = admin.GetTopicsMeta(ctx, []string{}, true)
   177  	require.NoError(t, err)
   178  	require.Len(t, result, 0)
   179  
   180  	targetTopic := "topic-1"
   181  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   182  		Return(&kafka.MetadataResponse{
   183  			Topics: []kafka.Topic{
   184  				{
   185  					Name: targetTopic,
   186  					Partitions: []kafka.Partition{
   187  						{}, {}, // 2 partitions
   188  					},
   189  				},
   190  				{
   191  					Name: "topic-2",
   192  					Partitions: []kafka.Partition{
   193  						{}, {}, // 2 partitions
   194  					},
   195  					Error: errors.New("topic error found"),
   196  				},
   197  			},
   198  		}, nil).Times(2)
   199  	// ignore topic error
   200  	result, err = admin.GetTopicsMeta(ctx, []string{targetTopic}, true)
   201  	require.NoError(t, err)
   202  	require.Len(t, result, 1)
   203  	require.Equal(t, "topic-1", result[targetTopic].Name)
   204  	require.Equal(t, int32(2), result[targetTopic].NumPartitions)
   205  
   206  	// not ignore topic error
   207  	result, err = admin.GetTopicsMeta(ctx, []string{targetTopic}, false)
   208  	require.Error(t, err)
   209  	require.Nil(t, result)
   210  }
   211  
   212  func TestGetTopicsPartitions(t *testing.T) {
   213  	t.Parallel()
   214  
   215  	ctx := context.Background()
   216  	admin, client := newClusterAdminClientWithMock(t)
   217  
   218  	// cannot get topics meta from kafka
   219  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   220  		Return(nil, fmt.Errorf("kafka.(*Client).Metadata"))
   221  	result, err := admin.GetTopicsMeta(ctx, []string{"test"}, false)
   222  	require.Error(t, err)
   223  	require.Nil(t, result)
   224  
   225  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   226  		Return(&kafka.MetadataResponse{}, nil)
   227  	result, err = admin.GetTopicsMeta(ctx, []string{"test"}, false)
   228  	require.NoError(t, err)
   229  	require.Len(t, result, 0)
   230  
   231  	client.EXPECT().Metadata(gomock.Any(), gomock.Any()).
   232  		Return(&kafka.MetadataResponse{
   233  			Topics: []kafka.Topic{
   234  				{
   235  					Name: "topic-1",
   236  					Partitions: []kafka.Partition{
   237  						{}, {}, // 2 partitions
   238  					},
   239  					Error: errors.New("topic error found"),
   240  				},
   241  				{
   242  					Name: "topic-2",
   243  					Partitions: []kafka.Partition{
   244  						{}, {}, {}, // 3 partitions
   245  					},
   246  					Error: nil,
   247  				},
   248  				{
   249  					Name:  "topic-3",
   250  					Error: nil,
   251  				},
   252  			},
   253  		}, nil)
   254  	// ignore topic error
   255  	result, err = admin.GetTopicsMeta(ctx, []string{"topic-1", "topic-2", "topic-3"}, true)
   256  	require.NoError(t, err)
   257  	require.Len(t, result, 2)
   258  	require.Equal(t, int32(3), result["topic-2"].NumPartitions)
   259  	require.Equal(t, int32(0), result["topic-3"].NumPartitions)
   260  }
   261  
   262  func TestCreateTopic(t *testing.T) {
   263  	t.Parallel()
   264  
   265  	ctx := context.Background()
   266  	admin, client := newClusterAdminClientWithMock(t)
   267  
   268  	client.EXPECT().CreateTopics(gomock.Any(), gomock.Any()).
   269  		Return(nil, fmt.Errorf("kafka.(*Client).CreateTopics"))
   270  	err := admin.CreateTopic(ctx, &pkafka.TopicDetail{
   271  		Name:              "topic-1",
   272  		NumPartitions:     1,
   273  		ReplicationFactor: 1,
   274  	}, false)
   275  	require.Error(t, err)
   276  
   277  	client.EXPECT().CreateTopics(gomock.Any(), gomock.Any()).
   278  		Return(&kafka.CreateTopicsResponse{
   279  			Errors: map[string]error{
   280  				"topic-1": errors.New("topic-1 error"),
   281  			},
   282  		}, nil)
   283  
   284  	err = admin.CreateTopic(ctx, &pkafka.TopicDetail{
   285  		Name:              "topic-1",
   286  		NumPartitions:     1,
   287  		ReplicationFactor: 1,
   288  	}, false)
   289  	require.Error(t, err, "topic-1 error")
   290  
   291  	client.EXPECT().CreateTopics(gomock.Any(), gomock.Any()).
   292  		Return(&kafka.CreateTopicsResponse{}, nil)
   293  	err = admin.CreateTopic(ctx, &pkafka.TopicDetail{
   294  		Name:              "topic-1",
   295  		NumPartitions:     1,
   296  		ReplicationFactor: 1,
   297  	}, false)
   298  	require.NoError(t, err)
   299  }
   300  
   301  type fr struct{}
   302  
   303  func (f fr) RoundTrip(context.Context, net.Addr, kafka.Request) (kafka.Response, error) {
   304  	return nil, nil
   305  }
   306  
   307  func TestAdminClose(t *testing.T) {
   308  	client := mock.NewMockGokrb5v8Client(gomock.NewController(t))
   309  	ad := &admin{}
   310  	ad.Close()
   311  	ad = &admin{
   312  		client: &kafka.Client{},
   313  	}
   314  	ad.Close()
   315  	ad = &admin{
   316  		client: &kafka.Client{
   317  			Transport: fr{},
   318  		},
   319  	}
   320  	ad.Close()
   321  	ad = &admin{
   322  		client: &kafka.Client{
   323  			Transport: &kafka.Transport{
   324  				SASL: plain.Mechanism{},
   325  			},
   326  		},
   327  	}
   328  	ad.Close()
   329  	ad = &admin{
   330  		client: &kafka.Client{
   331  			Transport: &kafka.Transport{
   332  				SASL: mechanism{client: client},
   333  			},
   334  		},
   335  	}
   336  	client.EXPECT().Destroy()
   337  	ad.Close()
   338  }