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 }