mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
c855109044
Bumps [github.com/twmb/franz-go/pkg/kadm](https://github.com/twmb/franz-go) from 1.16.0 to 1.17.1. - [Changelog](https://github.com/twmb/franz-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/twmb/franz-go/compare/v1.16.0...v1.17.1) --- updated-dependencies: - dependency-name: github.com/twmb/franz-go/pkg/kadm dependency-version: 1.17.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
674 lines
18 KiB
Go
674 lines
18 KiB
Go
// Package kadm provides a helper Kafka admin client around a *kgo.Client.
|
|
//
|
|
// This package is meant to cover the common use cases for dropping into an
|
|
// "admin" like interface for Kafka. As with any admin client, this package
|
|
// must make opinionated decisions on what to provide and what to hide. The
|
|
// underlying Kafka protocol gives more detailed information in responses, or
|
|
// allows more fine tuning in requests, but most of the time, these details are
|
|
// unnecessary.
|
|
//
|
|
// By virtue of making opinionated decisions, this package cannot satisfy every
|
|
// need for requests and responses. If you need more control than this admin
|
|
// client provides, you can use the kmsg package directly.
|
|
//
|
|
// This package contains a lot of types, but the main two types type to know
|
|
// are Client and ShardErrors. Every other type is used for inputs or outputs
|
|
// to methods on the client.
|
|
//
|
|
// The Client type is a simple small wrapper around a *kgo.Client that exists
|
|
// solely to namespace methods. The ShardErrors type is a bit more complicated.
|
|
// When issuing requests, under the hood some of these requests actually need
|
|
// to be mapped to brokers and split, issuing different pieces of the input
|
|
// request to different brokers. The *kgo.Client handles this all internally,
|
|
// but (if using RequestSharded as directed), returns each response to each of
|
|
// these split requests individually. Each response can fail or be successful.
|
|
// This package goes one step further and merges these failures into one meta
|
|
// failure, ShardErrors. Any function that returns ShardErrors is documented as
|
|
// such, and if a function returns a non-nil ShardErrors, it is possible that
|
|
// the returned data is actually valid and usable. If you care to, you can log
|
|
// / react to the partial failures and continue using the partial successful
|
|
// result. This is in contrast to other clients, which either require to to
|
|
// request individual brokers directly, or they completely hide individual
|
|
// failures, or they completely fail on any individual failure.
|
|
//
|
|
// For methods that list or describe things, this package often completely
|
|
// fails responses on auth failures. If you use a method that accepts two
|
|
// topics, one that you are authorized to and one that you are not, you will
|
|
// not receive a partial successful response. Instead, you will receive an
|
|
// AuthError. Methods that do *not* fail on auth errors are explicitly
|
|
// documented as such.
|
|
//
|
|
// Users may often find it easy to work with lists of topics or partitions.
|
|
// Rather than needing to build deeply nested maps directly, this package has a
|
|
// few helper types that are worth knowing:
|
|
//
|
|
// TopicsList - a slice of topics and their partitions
|
|
// TopicsSet - a set of topics, each containing a set of partitions
|
|
// Partitions - a slice of partitions
|
|
// OffsetsList - a slice of offsets
|
|
// Offsets - a map of offsets
|
|
//
|
|
// These types are meant to be easy to build and use, and can be used as the
|
|
// starting point for other types.
|
|
//
|
|
// Many functions in this package are variadic and return either a map or a
|
|
// list of responses, and you may only use one element as input and are only
|
|
// interested in one element of output. This package provides the following
|
|
// functions to help:
|
|
//
|
|
// Any(map)
|
|
// AnyE(map, err)
|
|
// First(slice)
|
|
// FirstE(slice, err)
|
|
//
|
|
// The intended use case of these is something like `kadm.AnyE(kadm.CreateTopics(..., "my-one-topic"))`,
|
|
// such that you can immediately get the response for the one topic you are
|
|
// creating.
|
|
package kadm
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"runtime/debug"
|
|
"sort"
|
|
"sync"
|
|
|
|
"github.com/twmb/franz-go/pkg/kgo"
|
|
)
|
|
|
|
func unptrStr(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
var (
|
|
reVersion *regexp.Regexp
|
|
reVersionOnce sync.Once
|
|
)
|
|
|
|
// Copied from kgo, but we use the kadm package version.
|
|
func softwareVersion() string {
|
|
info, ok := debug.ReadBuildInfo()
|
|
if ok {
|
|
reVersionOnce.Do(func() { reVersion = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9.-]*[a-zA-Z0-9])?$`) })
|
|
for _, dep := range info.Deps {
|
|
if dep.Path == "github.com/twmb/franz-go/pkg/kadm" {
|
|
if reVersion.MatchString(dep.Version) {
|
|
return dep.Version
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// Client is an admin client.
|
|
//
|
|
// This is a simple wrapper around a *kgo.Client to provide helper admin methods.
|
|
type Client struct {
|
|
cl *kgo.Client
|
|
|
|
timeoutMillis int32
|
|
}
|
|
|
|
// NewClient returns an admin client.
|
|
func NewClient(cl *kgo.Client) *Client {
|
|
return &Client{cl, 15000} // 15s timeout default, matching kmsg
|
|
}
|
|
|
|
// NewOptClient returns a new client directly from kgo options. This is a
|
|
// wrapper around creating a new *kgo.Client and then creating an admin client.
|
|
func NewOptClient(opts ...kgo.Opt) (*Client, error) {
|
|
cl, err := kgo.NewClient(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewClient(cl), nil
|
|
}
|
|
|
|
// Close closes the underlying *kgo.Client.
|
|
func (cl *Client) Close() {
|
|
cl.cl.Close()
|
|
}
|
|
|
|
// SetTimeoutMillis sets the timeout to use for requests that have a timeout,
|
|
// overriding the default of 15,000 (15s).
|
|
//
|
|
// Not all requests have timeouts. Most requests are expected to return
|
|
// immediately or are expected to deliberately hang. The following requests
|
|
// have timeout fields:
|
|
//
|
|
// Produce
|
|
// ListOffsets (Kafka 4+, relevant for tiered storage listing)
|
|
// CreateTopics
|
|
// DeleteTopics
|
|
// DeleteRecords
|
|
// CreatePartitions
|
|
// ElectLeaders
|
|
// AlterPartitionAssignments
|
|
// ListPartitionReassignments
|
|
// UpdateFeatures
|
|
//
|
|
// Not all requests above are supported in the admin API.
|
|
func (cl *Client) SetTimeoutMillis(millis int32) {
|
|
cl.timeoutMillis = millis
|
|
}
|
|
|
|
// StringPtr is a shortcut function to aid building configs for creating or
|
|
// altering topics.
|
|
func StringPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
// BrokerDetail is a type alias for kgo.BrokerMetadata.
|
|
type BrokerDetail = kgo.BrokerMetadata
|
|
|
|
// BrokerDetails contains the details for many brokers.
|
|
type BrokerDetails []BrokerDetail
|
|
|
|
// NodeIDs returns the IDs of all nodes.
|
|
func (ds BrokerDetails) NodeIDs() []int32 {
|
|
var all []int32
|
|
for _, d := range ds {
|
|
all = append(all, d.NodeID)
|
|
}
|
|
return int32s(all)
|
|
}
|
|
|
|
// Partition is a partition for a topic.
|
|
type Partition struct {
|
|
Topic string // Topic is the topic for this partition.
|
|
Partition int32 // Partition is this partition's number.
|
|
}
|
|
|
|
// Offset is an offset for a topic.
|
|
type Offset struct {
|
|
Topic string
|
|
Partition int32
|
|
At int64 // Offset is the partition to set.
|
|
LeaderEpoch int32 // LeaderEpoch is the broker leader epoch of the record at this offset.
|
|
Metadata string // Metadata, if non-empty, is used for offset commits.
|
|
}
|
|
|
|
// NewOffsetFromRecord is a helper to create an Offset for a given Record
|
|
func NewOffsetFromRecord(record *kgo.Record) Offset {
|
|
return Offset{
|
|
Topic: record.Topic,
|
|
Partition: record.Partition,
|
|
At: record.Offset + 1,
|
|
LeaderEpoch: record.LeaderEpoch,
|
|
}
|
|
}
|
|
|
|
// Partitions wraps many partitions.
|
|
type Partitions []Partition
|
|
|
|
// TopicsSet returns these partitions as TopicsSet.
|
|
func (ps Partitions) TopicsSet() TopicsSet {
|
|
s := make(TopicsSet)
|
|
for _, p := range ps {
|
|
s.Add(p.Topic, p.Partition)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// TopicsList returns these partitions as sorted TopicsList.
|
|
func (ps Partitions) TopicsList() TopicsList {
|
|
return ps.TopicsSet().Sorted()
|
|
}
|
|
|
|
// OffsetsList wraps many offsets and is a helper for building Offsets.
|
|
type OffsetsList []Offset
|
|
|
|
// Offsets returns this list as the non-list Offsets. All fields in each
|
|
// Offset must be set properly.
|
|
func (l OffsetsList) Offsets() Offsets {
|
|
os := make(Offsets)
|
|
for _, o := range l {
|
|
os.Add(o)
|
|
}
|
|
return os
|
|
}
|
|
|
|
// KOffsets returns this list as a kgo offset map.
|
|
func (l OffsetsList) KOffsets() map[string]map[int32]kgo.Offset {
|
|
return l.Offsets().KOffsets()
|
|
}
|
|
|
|
// Offsets wraps many offsets and is the type used for offset functions.
|
|
type Offsets map[string]map[int32]Offset
|
|
|
|
// Lookup returns the offset at t and p and whether it exists.
|
|
func (os Offsets) Lookup(t string, p int32) (Offset, bool) {
|
|
if len(os) == 0 {
|
|
return Offset{}, false
|
|
}
|
|
ps := os[t]
|
|
if len(ps) == 0 {
|
|
return Offset{}, false
|
|
}
|
|
o, exists := ps[p]
|
|
return o, exists
|
|
}
|
|
|
|
// Add adds an offset for a given topic/partition to this Offsets map.
|
|
//
|
|
// If the partition already exists, the offset is only added if:
|
|
//
|
|
// - the new leader epoch is higher than the old, or
|
|
// - the leader epochs equal, and the new offset is higher than the old
|
|
//
|
|
// If you would like to add offsets forcefully no matter what, use the Delete
|
|
// method before this.
|
|
func (os *Offsets) Add(o Offset) {
|
|
if *os == nil {
|
|
*os = make(map[string]map[int32]Offset)
|
|
}
|
|
ot := (*os)[o.Topic]
|
|
if ot == nil {
|
|
ot = make(map[int32]Offset)
|
|
(*os)[o.Topic] = ot
|
|
}
|
|
|
|
prior, exists := ot[o.Partition]
|
|
if !exists || prior.LeaderEpoch < o.LeaderEpoch ||
|
|
prior.LeaderEpoch == o.LeaderEpoch && prior.At < o.At {
|
|
ot[o.Partition] = o
|
|
}
|
|
}
|
|
|
|
// Delete removes any offset at topic t and partition p.
|
|
func (os Offsets) Delete(t string, p int32) {
|
|
if os == nil {
|
|
return
|
|
}
|
|
ot := os[t]
|
|
if ot == nil {
|
|
return
|
|
}
|
|
delete(ot, p)
|
|
if len(ot) == 0 {
|
|
delete(os, t)
|
|
}
|
|
}
|
|
|
|
// AddOffset is a helper to add an offset for a given topic and partition. The
|
|
// leader epoch field must be -1 if you do not know the leader epoch or if
|
|
// you do not have an offset yet.
|
|
func (os *Offsets) AddOffset(t string, p int32, o int64, leaderEpoch int32) {
|
|
os.Add(Offset{
|
|
Topic: t,
|
|
Partition: p,
|
|
At: o,
|
|
LeaderEpoch: leaderEpoch,
|
|
})
|
|
}
|
|
|
|
// KeepFunc calls fn for every offset, keeping the offset if fn returns true.
|
|
func (os Offsets) KeepFunc(fn func(o Offset) bool) {
|
|
for t, ps := range os {
|
|
for p, o := range ps {
|
|
if !fn(o) {
|
|
delete(ps, p)
|
|
}
|
|
}
|
|
if len(ps) == 0 {
|
|
delete(os, t)
|
|
}
|
|
}
|
|
}
|
|
|
|
// DeleteFunc calls fn for every offset, deleting the offset if fn returns
|
|
// true.
|
|
func (os Offsets) DeleteFunc(fn func(o Offset) bool) {
|
|
os.KeepFunc(func(o Offset) bool { return !fn(o) })
|
|
}
|
|
|
|
// TopicsSet returns the set of topics and partitions currently used in these
|
|
// offsets.
|
|
func (os Offsets) TopicsSet() TopicsSet {
|
|
s := make(TopicsSet)
|
|
os.Each(func(o Offset) { s.Add(o.Topic, o.Partition) })
|
|
return s
|
|
}
|
|
|
|
// Each calls fn for each offset in these offsets.
|
|
func (os Offsets) Each(fn func(Offset)) {
|
|
for _, ps := range os {
|
|
for _, o := range ps {
|
|
fn(o)
|
|
}
|
|
}
|
|
}
|
|
|
|
// KOffsets returns these offsets as a kgo offset map.
|
|
func (os Offsets) KOffsets() map[string]map[int32]kgo.Offset {
|
|
tskgo := make(map[string]map[int32]kgo.Offset)
|
|
for t, ps := range os {
|
|
pskgo := make(map[int32]kgo.Offset)
|
|
for p, o := range ps {
|
|
pskgo[p] = kgo.NewOffset().
|
|
At(o.At).
|
|
WithEpoch(o.LeaderEpoch)
|
|
}
|
|
tskgo[t] = pskgo
|
|
}
|
|
return tskgo
|
|
}
|
|
|
|
// Sorted returns the offsets sorted by topic and partition.
|
|
func (os Offsets) Sorted() []Offset {
|
|
var s []Offset
|
|
os.Each(func(o Offset) { s = append(s, o) })
|
|
sort.Slice(s, func(i, j int) bool {
|
|
return s[i].Topic < s[j].Topic ||
|
|
s[i].Topic == s[j].Topic && s[i].Partition < s[j].Partition
|
|
})
|
|
return s
|
|
}
|
|
|
|
// OffsetsFromFetches returns Offsets for the final record in any partition in
|
|
// the fetches. This is a helper to enable committing an entire returned batch.
|
|
//
|
|
// This function looks at only the last record per partition, assuming that the
|
|
// last record is the highest offset (which is the behavior returned by kgo's
|
|
// Poll functions). The returned offsets are one past the offset contained in
|
|
// the records.
|
|
func OffsetsFromFetches(fs kgo.Fetches) Offsets {
|
|
os := make(Offsets)
|
|
fs.EachPartition(func(p kgo.FetchTopicPartition) {
|
|
if len(p.Records) == 0 {
|
|
return
|
|
}
|
|
r := p.Records[len(p.Records)-1]
|
|
os.Add(NewOffsetFromRecord(r))
|
|
})
|
|
return os
|
|
}
|
|
|
|
// OffsetsFromRecords returns offsets for all given records, using the highest
|
|
// offset per partition. The returned offsets are one past the offset contained
|
|
// in the records.
|
|
func OffsetsFromRecords(rs ...kgo.Record) Offsets {
|
|
os := make(Offsets)
|
|
for _, r := range rs {
|
|
os.Add(NewOffsetFromRecord(&r))
|
|
}
|
|
return os
|
|
}
|
|
|
|
// TopicsSet is a set of topics and, per topic, a set of partitions.
|
|
//
|
|
// All methods provided for TopicsSet are safe to use on a nil (default) set.
|
|
type TopicsSet map[string]map[int32]struct{}
|
|
|
|
// Lookup returns whether the topic and partition exists.
|
|
func (s TopicsSet) Lookup(t string, p int32) bool {
|
|
if len(s) == 0 {
|
|
return false
|
|
}
|
|
ps := s[t]
|
|
if len(ps) == 0 {
|
|
return false
|
|
}
|
|
_, exists := ps[p]
|
|
return exists
|
|
}
|
|
|
|
// Each calls fn for each topic / partition in the topics set.
|
|
func (s TopicsSet) Each(fn func(t string, p int32)) {
|
|
for t, ps := range s {
|
|
for p := range ps {
|
|
fn(t, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// EachPartitions calls fn for each topic and its partitions in the topics set.
|
|
func (s TopicsSet) EachPartitions(fn func(t string, ps []int32)) {
|
|
for t, ps := range s {
|
|
sliced := make([]int32, 0, len(ps))
|
|
for p := range ps {
|
|
sliced = append(sliced, p)
|
|
}
|
|
fn(t, sliced)
|
|
}
|
|
}
|
|
|
|
// EmptyTopics returns all topics with no partitions.
|
|
func (s TopicsSet) EmptyTopics() []string {
|
|
var e []string
|
|
for t, ps := range s {
|
|
if len(ps) == 0 {
|
|
e = append(e, t)
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// Add adds partitions for a topic to the topics set. If no partitions are
|
|
// added, this still creates the topic.
|
|
func (s *TopicsSet) Add(t string, ps ...int32) {
|
|
if *s == nil {
|
|
*s = make(map[string]map[int32]struct{})
|
|
}
|
|
existing := (*s)[t]
|
|
if existing == nil {
|
|
existing = make(map[int32]struct{}, len(ps))
|
|
(*s)[t] = existing
|
|
}
|
|
for _, p := range ps {
|
|
existing[p] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Delete removes partitions from a topic from the topics set. If the topic
|
|
// ends up with no partitions, the topic is removed from the set.
|
|
func (s TopicsSet) Delete(t string, ps ...int32) {
|
|
if s == nil || len(ps) == 0 {
|
|
return
|
|
}
|
|
existing := s[t]
|
|
if existing == nil {
|
|
return
|
|
}
|
|
for _, p := range ps {
|
|
delete(existing, p)
|
|
}
|
|
if len(existing) == 0 {
|
|
delete(s, t)
|
|
}
|
|
}
|
|
|
|
// Topics returns all topics in this set in sorted order.
|
|
func (s TopicsSet) Topics() []string {
|
|
ts := make([]string, 0, len(s))
|
|
for t := range s {
|
|
ts = append(ts, t)
|
|
}
|
|
sort.Strings(ts)
|
|
return ts
|
|
}
|
|
|
|
// Merge merges another topic set into this one.
|
|
func (s TopicsSet) Merge(other TopicsSet) {
|
|
for t, ps := range other {
|
|
for p := range ps {
|
|
s.Add(t, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MergeTopics topics merges topic names into this set.
|
|
func (s TopicsSet) MergeTopics(ts []string) {
|
|
for _, t := range ts {
|
|
s.Add(t)
|
|
}
|
|
}
|
|
|
|
// IntoList returns this set as a list.
|
|
func (s TopicsSet) IntoList() TopicsList {
|
|
l := make(TopicsList, 0, len(s))
|
|
for t, ps := range s {
|
|
lps := make([]int32, 0, len(ps))
|
|
for p := range ps {
|
|
lps = append(lps, p)
|
|
}
|
|
l = append(l, TopicPartitions{
|
|
Topic: t,
|
|
Partitions: lps,
|
|
})
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Sorted returns this set as a list in topic-sorted order, with each topic
|
|
// having sorted partitions.
|
|
func (s TopicsSet) Sorted() TopicsList {
|
|
l := make(TopicsList, 0, len(s))
|
|
for t, ps := range s {
|
|
tps := TopicPartitions{
|
|
Topic: t,
|
|
Partitions: make([]int32, 0, len(ps)),
|
|
}
|
|
for p := range ps {
|
|
tps.Partitions = append(tps.Partitions, p)
|
|
}
|
|
tps.Partitions = int32s(tps.Partitions)
|
|
l = append(l, tps)
|
|
}
|
|
sort.Slice(l, func(i, j int) bool { return l[i].Topic < l[j].Topic })
|
|
return l
|
|
}
|
|
|
|
// TopicPartitions is a topic and partitions.
|
|
type TopicPartitions struct {
|
|
Topic string
|
|
Partitions []int32
|
|
}
|
|
|
|
// TopicsList is a list of topics and partitions.
|
|
type TopicsList []TopicPartitions
|
|
|
|
// Each calls fn for each topic / partition in the topics list.
|
|
func (l TopicsList) Each(fn func(t string, p int32)) {
|
|
for _, t := range l {
|
|
for _, p := range t.Partitions {
|
|
fn(t.Topic, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// EachPartitions calls fn for each topic and its partitions in the topics
|
|
// list.
|
|
func (l TopicsList) EachPartitions(fn func(t string, ps []int32)) {
|
|
for _, t := range l {
|
|
fn(t.Topic, t.Partitions)
|
|
}
|
|
}
|
|
|
|
// EmptyTopics returns all topics with no partitions.
|
|
func (l TopicsList) EmptyTopics() []string {
|
|
var e []string
|
|
for _, t := range l {
|
|
if len(t.Partitions) == 0 {
|
|
e = append(e, t.Topic)
|
|
}
|
|
}
|
|
return e
|
|
}
|
|
|
|
// Topics returns all topics in this set in sorted order.
|
|
func (l TopicsList) Topics() []string {
|
|
ts := make([]string, 0, len(l))
|
|
for _, t := range l {
|
|
ts = append(ts, t.Topic)
|
|
}
|
|
sort.Strings(ts)
|
|
return ts
|
|
}
|
|
|
|
// IntoSet returns this list as a set.
|
|
func (l TopicsList) IntoSet() TopicsSet {
|
|
s := make(TopicsSet)
|
|
for _, t := range l {
|
|
s.Add(t.Topic, t.Partitions...)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// First returns the first element of the input slice and whether it exists.
|
|
// This is the non-error-accepting equivalent of FirstE.
|
|
//
|
|
// Many client methods in kadm accept a variadic amount of input arguments and
|
|
// return either a slice or a map of responses, but you often use the method
|
|
// with only one argument. This function can help extract the one response you
|
|
// are interested in.
|
|
func First[S ~[]T, T any](s S) (T, bool) {
|
|
if len(s) == 0 {
|
|
var t T
|
|
return t, false
|
|
}
|
|
return s[0], true
|
|
}
|
|
|
|
// Any returns the first range element of the input map and whether it exists.
|
|
// This is the non-error-accepting equivalent of AnyE.
|
|
//
|
|
// Many client methods in kadm accept a variadic amount of input arguments and
|
|
// return either a slice or a map of responses, but you often use the method
|
|
// with only one argument. This function can help extract the one response you
|
|
// are interested in.
|
|
func Any[M ~map[K]V, K comparable, V any](m M) (V, bool) {
|
|
for _, v := range m {
|
|
return v, true
|
|
}
|
|
var v V
|
|
return v, false
|
|
}
|
|
|
|
// ErrEmpty is returned from FirstE or AnyE if the input is empty.
|
|
var ErrEmpty = errors.New("empty")
|
|
|
|
// FirstE returns the first element of the input slice, or the input error
|
|
// if it is non-nil. If the error is nil but the slice is empty, this returns
|
|
// ErrEmpty. This is the error-accepting equivalent of First.
|
|
//
|
|
// Many client methods in kadm accept a variadic amount of input arguments and
|
|
// return either a slice or a map of responses, but you often use the method
|
|
// with only one argument. This function can help extract the one response you
|
|
// are interested in.
|
|
func FirstE[S ~[]T, T any](s S, err error) (T, error) {
|
|
if err != nil {
|
|
var t T
|
|
return t, err
|
|
}
|
|
if len(s) == 0 {
|
|
var t T
|
|
return t, ErrEmpty
|
|
}
|
|
return s[0], err
|
|
}
|
|
|
|
// AnyE returns the first range element of the input map, or the input error if
|
|
// it is non-nil. If the error is nil but the map is empty, this returns
|
|
// ErrEmpty. This is the error-accepting equivalent of Any.
|
|
//
|
|
// Many client methods in kadm accept a variadic amount of input arguments and
|
|
// return either a slice or a map of responses, but you often use the method
|
|
// with only one argument. This function can help extract the one response you
|
|
// are interested in.
|
|
func AnyE[M ~map[K]V, K comparable, V any](m M, err error) (V, error) {
|
|
if err != nil {
|
|
var v V
|
|
return v, err
|
|
}
|
|
for _, v := range m {
|
|
return v, nil
|
|
}
|
|
var v V
|
|
return v, ErrEmpty
|
|
}
|