Files
renovate-sh-app[bot]zeekayrenovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
9d24f1a68e fix(deps): update module github.com/twmb/franz-go/pkg/kadm to v1.17.2 (#6332)
| datasource | package                           | from    | to      |
| ---------- | --------------------------------- | ------- | ------- |
| go         | github.com/twmb/franz-go/pkg/kadm | v1.17.1 | v1.17.2 |

Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2026-02-22 00:55:20 -08:00

659 lines
22 KiB
Go

package kadm
import (
"context"
"errors"
"sort"
"github.com/twmb/franz-go/pkg/kerr"
"github.com/twmb/franz-go/pkg/kmsg"
)
var includeAuthOps = "include_auth_ops"
// ListTopics issues a metadata request and returns TopicDetails. Specific
// topics to describe can be passed as additional arguments. If no topics are
// specified, all topics are requested. Internal topics are not returned unless
// specifically requested. To see all topics including internal topics, use
// ListTopicsWithInternal.
//
// This returns an error if the request fails to be issued, or an *AuthError.
func (cl *Client) ListTopics(
ctx context.Context,
topics ...string,
) (TopicDetails, error) {
t, err := cl.ListTopicsWithInternal(ctx, topics...)
if err != nil {
return nil, err
}
// Only filter internal topics when listing all topics (no specific topics requested).
// If specific topics are named, include them even if they are internal.
if len(topics) == 0 {
t.FilterInternal()
}
return t, nil
}
// ListTopicsWithInternal is the same as ListTopics, but does not filter
// internal topics before returning.
func (cl *Client) ListTopicsWithInternal(
ctx context.Context,
topics ...string,
) (TopicDetails, error) {
m, err := cl.Metadata(ctx, topics...)
if err != nil {
return nil, err
}
return m.Topics, nil
}
// CreateTopicResponse contains the response for an individual created topic.
type CreateTopicResponse struct {
Topic string // Topic is the topic that was created.
ID TopicID // ID is the topic ID for this topic, if talking to Kafka v2.8+.
Err error // Err is any error preventing this topic from being created.
ErrMessage string // ErrMessage a potential extra message describing any error.
NumPartitions int32 // NumPartitions is the number of partitions in the response, if talking to Kafka v2.4+.
ReplicationFactor int16 // ReplicationFactor is how many replicas every partition has for this topic, if talking to Kafka 2.4+.
Configs map[string]Config // Configs contains the topic configuration (minus config synonyms), if talking to Kafka 2.4+.
}
// CreateTopicResponses contains per-topic responses for created topics.
type CreateTopicResponses map[string]CreateTopicResponse
// Sorted returns all create topic responses sorted first by topic ID, then by
// topic name.
func (rs CreateTopicResponses) Sorted() []CreateTopicResponse {
s := make([]CreateTopicResponse, 0, len(rs))
for _, d := range rs {
s = append(s, d)
}
sort.Slice(s, func(i, j int) bool {
l, r := s[i], s[j]
if l.ID.Less(r.ID) {
return true
}
return l.Topic < r.Topic
})
return s
}
// On calls fn for the response topic if it exists, returning the response and
// the error returned from fn. If fn is nil, this simply returns the response.
//
// The fn is given a copy of the response. This function returns the copy as
// well; any modifications within fn are modifications on the returned copy.
//
// If the topic does not exist, this returns kerr.UnknownTopicOrPartition.
func (rs CreateTopicResponses) On(topic string, fn func(*CreateTopicResponse) error) (CreateTopicResponse, error) {
if len(rs) > 0 {
r, ok := rs[topic]
if ok {
if fn == nil {
return r, nil
}
return r, fn(&r)
}
}
return CreateTopicResponse{}, kerr.UnknownTopicOrPartition
}
// Error iterates over all responses and returns the first error
// encountered, if any.
func (rs CreateTopicResponses) Error() error {
for _, r := range rs {
if r.Err != nil {
return r.Err
}
}
return nil
}
// CreateTopic issues a create topics request with the given partitions,
// replication factor, and (optional) configs for the given topic name.
// This is similar to CreateTopics, but returns the kerr.ErrorForCode(response.ErrorCode)
// if the request/response is successful.
func (cl *Client) CreateTopic(
ctx context.Context,
partitions int32,
replicationFactor int16,
configs map[string]*string,
topic string,
) (CreateTopicResponse, error) {
createTopicResponse, err := cl.CreateTopics(
ctx,
partitions,
replicationFactor,
configs,
topic,
)
if err != nil {
return CreateTopicResponse{}, err
}
response, exists := createTopicResponse[topic]
if !exists {
return CreateTopicResponse{}, errors.New("requested topic was not part of create topic response")
}
return response, response.Err
}
// CreateTopics issues a create topics request with the given partitions,
// replication factor, and (optional) configs for every topic. Under the hood,
// this uses the default 15s request timeout and lets Kafka choose where to
// place partitions.
//
// Version 4 of the underlying create topic request was introduced in Kafka 2.4
// and brought client support for creation defaults. If talking to a 2.4+
// cluster, you can use -1 for partitions and replicationFactor to use broker
// defaults.
//
// This package includes a StringPtr function to aid in building config values.
//
// This does not return an error on authorization failures, instead,
// authorization failures are included in the responses. This only returns an
// error if the request fails to be issued. You may consider checking
// ValidateCreateTopics before using this method.
func (cl *Client) CreateTopics(
ctx context.Context,
partitions int32,
replicationFactor int16,
configs map[string]*string,
topics ...string,
) (CreateTopicResponses, error) {
return cl.createTopics(ctx, false, partitions, replicationFactor, configs, topics)
}
// ValidateCreateTopics validates a create topics request with the given
// partitions, replication factor, and (optional) configs for every topic.
//
// This package includes a StringPtr function to aid in building config values.
//
// This uses the same logic as CreateTopics, but with the request's
// ValidateOnly field set to true. The response is the same response you would
// receive from CreateTopics, but no topics are actually created.
func (cl *Client) ValidateCreateTopics(
ctx context.Context,
partitions int32,
replicationFactor int16,
configs map[string]*string,
topics ...string,
) (CreateTopicResponses, error) {
return cl.createTopics(ctx, true, partitions, replicationFactor, configs, topics)
}
func (cl *Client) createTopics(ctx context.Context, dry bool, p int32, rf int16, configs map[string]*string, topics []string) (CreateTopicResponses, error) {
if len(topics) == 0 {
return make(CreateTopicResponses), nil
}
req := kmsg.NewCreateTopicsRequest()
req.TimeoutMillis = cl.timeoutMillis
req.ValidateOnly = dry
for _, t := range topics {
rt := kmsg.NewCreateTopicsRequestTopic()
rt.Topic = t
rt.NumPartitions = p
rt.ReplicationFactor = rf
for k, v := range configs {
rc := kmsg.NewCreateTopicsRequestTopicConfig()
rc.Name = k
rc.Value = v
rt.Configs = append(rt.Configs, rc)
}
req.Topics = append(req.Topics, rt)
}
resp, err := req.RequestWith(ctx, cl.cl)
if err != nil {
return nil, err
}
rs := make(CreateTopicResponses)
for _, t := range resp.Topics {
rt := CreateTopicResponse{
Topic: t.Topic,
ID: t.TopicID,
Err: kerr.ErrorForCode(t.ErrorCode),
ErrMessage: unptrStr(t.ErrorMessage),
NumPartitions: t.NumPartitions,
ReplicationFactor: t.ReplicationFactor,
Configs: make(map[string]Config),
}
for _, c := range t.Configs {
rt.Configs[c.Name] = Config{
Key: c.Name,
Value: c.Value,
Source: kmsg.ConfigSource(c.Source),
Sensitive: c.IsSensitive,
}
}
rs[t.Topic] = rt
}
return rs, nil
}
// DeleteTopicResponse contains the response for an individual deleted topic.
type DeleteTopicResponse struct {
Topic string // Topic is the topic that was deleted, if not using topic IDs.
ID TopicID // ID is the topic ID for this topic, if talking to Kafka v2.8+ and using topic IDs.
Err error // Err is any error preventing this topic from being deleted.
ErrMessage string // ErrMessage a potential extra message describing any error.
}
// DeleteTopicResponses contains per-topic responses for deleted topics.
type DeleteTopicResponses map[string]DeleteTopicResponse
// Sorted returns all delete topic responses sorted first by topic ID, then by
// topic name.
func (rs DeleteTopicResponses) Sorted() []DeleteTopicResponse {
s := make([]DeleteTopicResponse, 0, len(rs))
for _, d := range rs {
s = append(s, d)
}
sort.Slice(s, func(i, j int) bool {
l, r := s[i], s[j]
if l.ID.Less(r.ID) {
return true
}
return l.Topic < r.Topic
})
return s
}
// On calls fn for the response topic if it exists, returning the response and
// the error returned from fn. If fn is nil, this simply returns the response.
//
// The fn is given a copy of the response. This function returns the copy as
// well; any modifications within fn are modifications on the returned copy.
//
// If the topic does not exist, this returns kerr.UnknownTopicOrPartition.
func (rs DeleteTopicResponses) On(topic string, fn func(*DeleteTopicResponse) error) (DeleteTopicResponse, error) {
if len(rs) > 0 {
r, ok := rs[topic]
if ok {
if fn == nil {
return r, nil
}
return r, fn(&r)
}
}
return DeleteTopicResponse{}, kerr.UnknownTopicOrPartition
}
// Error iterates over all responses and returns the first error
// encountered, if any.
func (rs DeleteTopicResponses) Error() error {
for _, r := range rs {
if r.Err != nil {
return r.Err
}
}
return nil
}
// DeleteTopic issues a delete topic request for the given topic name with a
// (by default) 15s timeout. This is similar to DeleteTopics, but returns the
// kerr.ErrorForCode(response.ErrorCode) if the request/response is successful.
func (cl *Client) DeleteTopic(ctx context.Context, topic string) (DeleteTopicResponse, error) {
rs, err := cl.DeleteTopics(ctx, topic)
if err != nil {
return DeleteTopicResponse{}, err
}
r, exists := rs[topic]
if !exists {
return DeleteTopicResponse{}, errors.New("requested topic was not part of delete topic response")
}
return r, r.Err
}
// DeleteTopics issues a delete topics request for the given topic names with a
// (by default) 15s timeout.
//
// This does not return an error on authorization failures, instead,
// authorization failures are included in the responses. This only returns an
// error if the request fails to be issued.
func (cl *Client) DeleteTopics(ctx context.Context, topics ...string) (DeleteTopicResponses, error) {
if len(topics) == 0 {
return make(DeleteTopicResponses), nil
}
req := kmsg.NewDeleteTopicsRequest()
req.TimeoutMillis = cl.timeoutMillis
req.TopicNames = topics
for _, t := range topics {
rt := kmsg.NewDeleteTopicsRequestTopic()
rt.Topic = kmsg.StringPtr(t)
req.Topics = append(req.Topics, rt)
}
resp, err := req.RequestWith(ctx, cl.cl)
if err != nil {
return nil, err
}
rs := make(DeleteTopicResponses)
for _, t := range resp.Topics {
// A valid Kafka will return non-nil topics here, because we
// are deleting by topic name, not ID. We still check to be
// sure, but multiple invalid (nil) topics will collide.
var topic string
if t.Topic != nil {
topic = *t.Topic
}
rs[topic] = DeleteTopicResponse{
Topic: topic,
ID: t.TopicID,
Err: kerr.ErrorForCode(t.ErrorCode),
ErrMessage: unptrStr(t.ErrorMessage),
}
}
return rs, nil
}
// DeleteRecordsResponse contains the response for an individual partition from
// a delete records request.
type DeleteRecordsResponse struct {
Topic string // Topic is the topic this response is for.
Partition int32 // Partition is the partition this response is for.
LowWatermark int64 // LowWatermark is the new earliest / start offset for this partition if the request was successful.
Err error // Err is any error preventing the delete records request from being successful for this partition.
}
// DeleteRecordsResponses contains per-partition responses to a delete records request.
type DeleteRecordsResponses map[string]map[int32]DeleteRecordsResponse
// Lookup returns the response at t and p and whether it exists.
func (ds DeleteRecordsResponses) Lookup(t string, p int32) (DeleteRecordsResponse, bool) {
if len(ds) == 0 {
return DeleteRecordsResponse{}, false
}
ps := ds[t]
if len(ps) == 0 {
return DeleteRecordsResponse{}, false
}
r, exists := ps[p]
return r, exists
}
// Each calls fn for every delete records response.
func (ds DeleteRecordsResponses) Each(fn func(DeleteRecordsResponse)) {
for _, ps := range ds {
for _, d := range ps {
fn(d)
}
}
}
// Sorted returns all delete records responses sorted first by topic, then by
// partition.
func (ds DeleteRecordsResponses) Sorted() []DeleteRecordsResponse {
var s []DeleteRecordsResponse
for _, ps := range ds {
for _, d := range ps {
s = append(s, d)
}
}
sort.Slice(s, func(i, j int) bool {
l, r := s[i], s[j]
if l.Topic < r.Topic {
return true
}
if l.Topic > r.Topic {
return false
}
return l.Partition < r.Partition
})
return s
}
// On calls fn for the response topic/partition if it exists, returning the
// response and the error returned from fn. If fn is nil, this simply returns
// the response.
//
// The fn is given a copy of the response. This function returns the copy as
// well; any modifications within fn are modifications on the returned copy.
//
// If the topic or partition does not exist, this returns
// kerr.UnknownTopicOrPartition.
func (ds DeleteRecordsResponses) On(topic string, partition int32, fn func(*DeleteRecordsResponse) error) (DeleteRecordsResponse, error) {
if len(ds) > 0 {
t, ok := ds[topic]
if ok {
p, ok := t[partition]
if ok {
if fn == nil {
return p, nil
}
return p, fn(&p)
}
}
}
return DeleteRecordsResponse{}, kerr.UnknownTopicOrPartition
}
// Error iterates over all responses and returns the first error
// encountered, if any.
func (ds DeleteRecordsResponses) Error() error {
for _, ps := range ds {
for _, r := range ps {
if r.Err != nil {
return r.Err
}
}
}
return nil
}
// DeleteRecords issues a delete records request for the given offsets. Per
// offset, only the Offset field needs to be set.
//
// To delete records, Kafka sets the LogStartOffset for partitions to the
// requested offset. All segments whose max partition is before the requested
// offset are deleted, and any records within the segment before the requested
// offset can no longer be read.
//
// This does not return an error on authorization failures, instead,
// authorization failures are included in the responses.
//
// This may return *ShardErrors.
func (cl *Client) DeleteRecords(ctx context.Context, os Offsets) (DeleteRecordsResponses, error) {
if len(os) == 0 {
return make(DeleteRecordsResponses), nil
}
req := kmsg.NewPtrDeleteRecordsRequest()
req.TimeoutMillis = cl.timeoutMillis
for t, ps := range os {
rt := kmsg.NewDeleteRecordsRequestTopic()
rt.Topic = t
for p, o := range ps {
rp := kmsg.NewDeleteRecordsRequestTopicPartition()
rp.Partition = p
rp.Offset = o.At
rt.Partitions = append(rt.Partitions, rp)
}
req.Topics = append(req.Topics, rt)
}
shards := cl.cl.RequestSharded(ctx, req)
rs := make(DeleteRecordsResponses)
return rs, shardErrEach(req, shards, func(kr kmsg.Response) error {
resp := kr.(*kmsg.DeleteRecordsResponse)
for _, t := range resp.Topics {
rt, exists := rs[t.Topic]
if !exists { // topic could be spread around brokers, we need to check existence
rt = make(map[int32]DeleteRecordsResponse)
rs[t.Topic] = rt
}
for _, p := range t.Partitions {
rt[p.Partition] = DeleteRecordsResponse{
Topic: t.Topic,
Partition: p.Partition,
LowWatermark: p.LowWatermark,
Err: kerr.ErrorForCode(p.ErrorCode),
}
}
}
return nil
})
}
// CreatePartitionsResponse contains the response for an individual topic from
// a create partitions request.
type CreatePartitionsResponse struct {
Topic string // Topic is the topic this response is for.
Err error // Err is non-nil if partitions were unable to be added to this topic.
ErrMessage string // ErrMessage a potential extra message describing any error.
}
// CreatePartitionsResponses contains per-topic responses for a create
// partitions request.
type CreatePartitionsResponses map[string]CreatePartitionsResponse
// Sorted returns all create partitions responses sorted by topic.
func (rs CreatePartitionsResponses) Sorted() []CreatePartitionsResponse {
var s []CreatePartitionsResponse
for _, r := range rs {
s = append(s, r)
}
sort.Slice(s, func(i, j int) bool { return s[i].Topic < s[j].Topic })
return s
}
// On calls fn for the response topic if it exists, returning the response and
// the error returned from fn. If fn is nil, this simply returns the response.
//
// The fn is given a copy of the response. This function returns the copy as
// well; any modifications within fn are modifications on the returned copy.
//
// If the topic does not exist, this returns kerr.UnknownTopicOrPartition.
func (rs CreatePartitionsResponses) On(topic string, fn func(*CreatePartitionsResponse) error) (CreatePartitionsResponse, error) {
if len(rs) > 0 {
r, ok := rs[topic]
if ok {
if fn == nil {
return r, nil
}
return r, fn(&r)
}
}
return CreatePartitionsResponse{}, kerr.UnknownTopicOrPartition
}
// Error iterates over all responses and returns the first error
// encountered, if any.
func (rs CreatePartitionsResponses) Error() error {
for _, r := range rs {
if r.Err != nil {
return r.Err
}
}
return nil
}
// CreatePartitions issues a create partitions request for the given topics,
// adding "add" partitions to each topic. This request lets Kafka choose where
// the new partitions should be.
//
// This does not return an error on authorization failures for the create
// partitions request itself, instead, authorization failures are included in
// the responses. Before adding partitions, this request must issue a metadata
// request to learn the current count of partitions. If that fails, this
// returns the metadata request error. If you already know the final amount of
// partitions you want, you can use UpdatePartitions to set the count directly
// (rather than adding to the current count). You may consider checking
// ValidateCreatePartitions before using this method.
func (cl *Client) CreatePartitions(ctx context.Context, add int, topics ...string) (CreatePartitionsResponses, error) {
return cl.createPartitions(ctx, false, add, -1, topics)
}
// UpdatePartitions issues a create partitions request for the given topics,
// setting the final partition count to "set" for each topic. This request lets
// Kafka choose where the new partitions should be.
//
// This does not return an error on authorization failures for the create
// partitions request itself, instead, authorization failures are included in
// the responses. Unlike CreatePartitions, this request uses your "set" value
// to set the new final count of partitions. "set" must be equal to or larger
// than the current count of partitions in the topic. All topics will have the
// same final count of partitions (unlike CreatePartitions, which allows you to
// add a specific count of partitions to topics that have a different amount of
// current partitions). You may consider checking ValidateUpdatePartitions
// before using this method.
func (cl *Client) UpdatePartitions(ctx context.Context, set int, topics ...string) (CreatePartitionsResponses, error) {
return cl.createPartitions(ctx, false, -1, set, topics)
}
// ValidateCreatePartitions validates a create partitions request for adding
// "add" partitions to the given topics.
//
// This uses the same logic as CreatePartitions, but with the request's
// ValidateOnly field set to true. The response is the same response you would
// receive from CreatePartitions, but no partitions are actually added.
func (cl *Client) ValidateCreatePartitions(ctx context.Context, add int, topics ...string) (CreatePartitionsResponses, error) {
return cl.createPartitions(ctx, true, add, -1, topics)
}
// ValidateUpdatePartitions validates a create partitions request for setting
// the partition count on the given topics to "set".
//
// This uses the same logic as UpdatePartitions, but with the request's
// ValidateOnly field set to true. The response is the same response you would
// receive from UpdatePartitions, but no partitions are actually added.
func (cl *Client) ValidateUpdatePartitions(ctx context.Context, set int, topics ...string) (CreatePartitionsResponses, error) {
return cl.createPartitions(ctx, true, -1, set, topics)
}
func (cl *Client) createPartitions(ctx context.Context, dry bool, add, set int, topics []string) (CreatePartitionsResponses, error) {
if len(topics) == 0 {
return make(CreatePartitionsResponses), nil
}
var td TopicDetails
var err error
if add != -1 {
td, err = cl.ListTopics(ctx, topics...)
if err != nil {
return nil, err
}
}
req := kmsg.NewCreatePartitionsRequest()
req.TimeoutMillis = cl.timeoutMillis
req.ValidateOnly = dry
for _, t := range topics {
rt := kmsg.NewCreatePartitionsRequestTopic()
rt.Topic = t
if add == -1 {
rt.Count = int32(set)
} else {
rt.Count = int32(len(td[t].Partitions) + add)
}
req.Topics = append(req.Topics, rt)
}
resp, err := req.RequestWith(ctx, cl.cl)
if err != nil {
return nil, err
}
rs := make(CreatePartitionsResponses)
for _, t := range resp.Topics {
rs[t.Topic] = CreatePartitionsResponse{
Topic: t.Topic,
Err: kerr.ErrorForCode(t.ErrorCode),
ErrMessage: unptrStr(t.ErrorMessage),
}
}
return rs, nil
}
// WithAuthorizedOps attaches an internal key/value to the context through WithValue.
// Using this context will cause all Metadata requests (assuming Kafka version 2.3.0
// or higher) to fetch the list of authorized operations. See KIP-430 for details.
func WithAuthorizedOps(ctx context.Context) context.Context {
return context.WithValue(ctx, &includeAuthOps, struct{}{})
}