From f704f5f25024ffc3591e419e4f7fb540d1644613 Mon Sep 17 00:00:00 2001 From: encalada Date: Sat, 24 Feb 2024 14:06:26 +0100 Subject: [PATCH 01/11] Add code for consumer and observer Introduce kafkadata file Add run.sh Add related Dockerfile's Improve README Signed-off-by: encalada --- kafka-observer/Dockerfile.consumer | 21 ++ kafka-observer/Dockerfile.observer | 21 ++ kafka-observer/README.md | 78 ++++++ kafka-observer/cmd/consumer/main.go | 157 ++++++++++++ kafka-observer/cmd/observer/main.go | 341 ++++++++++++++++++++++++++ kafka-observer/go.mod | 48 ++++ kafka-observer/go.sum | 172 +++++++++++++ kafka-observer/images/kafkapoc.jpg | Bin 0 -> 53517 bytes kafka-observer/internal/cmd/config.go | 108 ++++++++ kafka-observer/resources/kafkadata | 8 + kafka-observer/run.sh | 60 +++++ 11 files changed, 1014 insertions(+) create mode 100644 kafka-observer/Dockerfile.consumer create mode 100644 kafka-observer/Dockerfile.observer create mode 100644 kafka-observer/README.md create mode 100644 kafka-observer/cmd/consumer/main.go create mode 100644 kafka-observer/cmd/observer/main.go create mode 100644 kafka-observer/go.mod create mode 100644 kafka-observer/go.sum create mode 100644 kafka-observer/images/kafkapoc.jpg create mode 100644 kafka-observer/internal/cmd/config.go create mode 100644 kafka-observer/resources/kafkadata create mode 100755 kafka-observer/run.sh diff --git a/kafka-observer/Dockerfile.consumer b/kafka-observer/Dockerfile.consumer new file mode 100644 index 00000000..8d9f0baf --- /dev/null +++ b/kafka-observer/Dockerfile.consumer @@ -0,0 +1,21 @@ +FROM icr.io/codeengine/golang:latest AS stage + +WORKDIR /app/src + +COPY cmd/consumer/ ./consumer/ + +COPY internal/ ./internal/ + +COPY go.mod . + +COPY go.sum . + +RUN CGO_ENABLED=0 GOOS=linux go build -o consumer ./consumer + +FROM icr.io/codeengine/golang:latest + +WORKDIR /app/src + +COPY --from=stage /app/src/consumer/consumer . + +CMD [ "./consumer" ] \ No newline at end of file diff --git a/kafka-observer/Dockerfile.observer b/kafka-observer/Dockerfile.observer new file mode 100644 index 00000000..375c89c9 --- /dev/null +++ b/kafka-observer/Dockerfile.observer @@ -0,0 +1,21 @@ +FROM icr.io/codeengine/golang:latest AS stage + +WORKDIR /app/src + +COPY cmd/observer/ ./observer/ + +COPY internal/ ./internal/ + +COPY go.mod . + +COPY go.sum . + +RUN CGO_ENABLED=0 GOOS=linux go build -o observer ./observer + +FROM icr.io/codeengine/golang:latest + +WORKDIR /app/src + +COPY --from=stage /app/src/observer/observer . + +CMD [ "./observer" ] \ No newline at end of file diff --git a/kafka-observer/README.md b/kafka-observer/README.md new file mode 100644 index 00000000..1478317e --- /dev/null +++ b/kafka-observer/README.md @@ -0,0 +1,78 @@ +# Kafka Observer + +## Introduction + +This sample demonstrates how you can use IBM Cloud Code Engine and IBM Cloud Event Streams to efficiently consume events. This sample has two components, an observer and a consumer. + +Here is the architecture diagram for this sample. + +![Architecture Diagram](images/kafkapoc.jpg) + +Here the observer is a code engine job which will run all the time, and it will look for events from multiple topics from the IBM Cloud Event streams instance. Once it gets a message/event from it, the observer triggers the consumer jobruns which will consume the events. + +So here the observer works as a wake up mechanism for triggering the consumer jobruns, eliminating the need for your consumers to constantly check for the events. + +The number of consumer jobruns triggered by the observer can be configured in this [config file](resources/kafkadata) + +Here in this sample, the consumer is configured in such a way that once it begins consuming the events, it will automatically be terminated if it doesn't get any messages within one-minute timeframe. + +// TODO: we need to decide where we configure the consumer groups for the consumers, it would be better, if can configure it in the kafkadata file. + +Also, you can configure multiple consumers with different consumer groups for a topic. + + +## Prerequisites to run this sample : + +- You should have your [IBM Cloud Events streams](https://cloud.ibm.com/eventstreams-provisioning/6a7f4e38-f218-48ef-9dd2-df408747568e/create) instance ready. Also, you have to create the topics from which the messages will be consumed. + +- Create a [IBM Cloud Codeengine project](https://cloud.ibm.com/docs/codeengine?topic=codeengine-manage-project#create-a-project). + +- Add the topics and jobDefinitions in the [kafkadata](resources/kafkadata) file. Template for Kafkadata file is: + +``` + + partitions: + jobs: + - + - + + partitions: + jobs: + - +``` + +- Set the required fields in the [run.sh](run.sh) file + +- To test this sample, you need a producer which can send messages to the kafka topics, if you don't have you can create it in your code engine project by following the steps in this [doc](https://cloud.ibm.com/docs/codeengine?topic=codeengine-subscribe-kafka-tutorial). + +## Running this sample + +1. Login to ibm cloud + +``` +ibmcloud login --apikey -r -g +``` + +2. Select the code engine project. +``` +ibmcloud ce project select --name +``` + +3. Execute `run.sh` file +``` +./run.sh +``` + +Once you execute `run.sh`, it will create the necessary resources in your code engine project like secrets, configmaps, jobs etc and will start the observer in your codeengine project. Now you can send messages using your producer to the kafka topics, then the observer will watch for messages and runs the corresponding consumer jobruns based on the configuration in the [kafkadata](resources/kafkadata) file. + +**_NOTE:_** +If you created the producer app using the steps mentioned in the prerequisites, then you can run this command to send the events: + +``` +curl "?topic=&num=" +``` + +You can clean the resources in the codeengine project by running this command +``` +./run.sh clean +``` diff --git a/kafka-observer/cmd/consumer/main.go b/kafka-observer/cmd/consumer/main.go new file mode 100644 index 00000000..0da53639 --- /dev/null +++ b/kafka-observer/cmd/consumer/main.go @@ -0,0 +1,157 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/IBM/CodeEngine/kafka-observer/internal/cmd" + "github.com/IBM/sarama" +) + +var ( + version = "" + oldest = true + verbose = false +) + +var idleTimer *time.Timer + +// Consumer implements the ConsumerGroupHandler +// interface +type Consumer struct { + ready chan bool + // ce *codeenginev2.CodeEngineV2 + topicToJD map[string]cmd.TopicsToJobs +} + +func main() { + fmt.Println("retrieving config") + + config := cmd.GetConfig() + + keepRunning := true + log.Println("Starting a new Sarama consumer") + + if verbose { + sarama.Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags) + } + + version = sarama.DefaultVersion.String() + version, err := sarama.ParseKafkaVersion(version) + if err != nil { + log.Panicf("Error parsing Kafka version: %v", err) + } + + saramaConfig := sarama.NewConfig() + saramaConfig.Version = version + saramaConfig.Consumer.Offsets.AutoCommit.Enable = true + saramaConfig.ClientID, _ = os.Hostname() + saramaConfig.Net.SASL.Enable = true + saramaConfig.Net.SASL.User = config.KafkaAuth.User + saramaConfig.Net.SASL.Password = config.KafkaAuth.Token + saramaConfig.Net.TLS.Enable = true + + if oldest { + saramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest + } + + saramaConfig.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.NewBalanceStrategyRoundRobin()} + + consumer := Consumer{ + ready: make(chan bool), + // ce: ceService, + topicToJD: config.KafkaCE, + } + + brokers := config.Kafka.Brokers + + ctx, cancel := context.WithCancel(context.Background()) + client, err := sarama.NewConsumerGroup(brokers, "consuming-group", saramaConfig) + if err != nil { + log.Panicf("Error creating consumer group client: %v", err) + } + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for { + // TODO: fix topics, cannot use all + if err := client.Consume(ctx, config.Kafka.Topics, &consumer); err != nil { + if errors.Is(err, sarama.ErrClosedConsumerGroup) { + return + } + log.Panicf("Error from consumer: %v", err) + } + if ctx.Err() != nil { + return + } + consumer.ready = make(chan bool) + } + }() + + <-consumer.ready + log.Println("Sarama consumer up and running!...") + + sigusr1 := make(chan os.Signal, 1) + signal.Notify(sigusr1, syscall.SIGUSR1) + + sigterm := make(chan os.Signal, 1) + signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM) + + idleTimer = time.NewTimer(time.Second * 100) + defer idleTimer.Stop() + + for keepRunning { + select { + case <-ctx.Done(): + log.Println("terminating: context cancelled") + keepRunning = false + case <-idleTimer.C: + log.Println("we are done, timeout expired") + keepRunning = false + case <-sigterm: + log.Println("terminating: via signal") + keepRunning = false + } + } + cancel() + wg.Wait() + if err = client.Close(); err != nil { + log.Panicf("Error closing client: %v", err) + } +} + +func (consumer *Consumer) Setup(sarama.ConsumerGroupSession) error { + close(consumer.ready) + return nil +} + +func (consumer *Consumer) Cleanup(sarama.ConsumerGroupSession) error { + return nil +} + +func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { + for { + idleTimer.Reset(time.Second * 60) + select { + case message, ok := <-claim.Messages(): + if !ok { + log.Printf("message channel was closed") + return nil + } + log.Printf("Message claimed: value = %s, timestamp = %v, topic = %s, partition = %v, offset = %v", string(message.Value), message.Timestamp, message.Topic, message.Partition, message.Offset) + session.MarkMessage(message, "") + case <-session.Context().Done(): + log.Printf("completed") + return nil + } + } +} diff --git a/kafka-observer/cmd/observer/main.go b/kafka-observer/cmd/observer/main.go new file mode 100644 index 00000000..284b045e --- /dev/null +++ b/kafka-observer/cmd/observer/main.go @@ -0,0 +1,341 @@ +package main + +import ( + "context" + "errors" + "log" + "os" + "os/signal" + "strconv" + "sync" + "syscall" + "time" + + "github.com/IBM/CodeEngine/kafka-observer/internal/cmd" + "github.com/IBM/code-engine-go-sdk/codeenginev2" + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/sarama" +) + +var ( + version = "" + oldest = true + verbose = false + projectID string +) + +// Consumer implements the ConsumerGroupHandler +// interface +type Consumer struct { + ready chan bool + ce *codeenginev2.CodeEngineV2 + topicToJD map[string]cmd.TopicsToJobs + sync.Mutex +} + +func main() { + + config := cmd.GetConfig() + + keepRunning := true + log.Println("Starting a new Kafka observer") + + if verbose { + sarama.Logger = log.New(os.Stdout, "[sarama] ", log.LstdFlags) + } + + ceService, err := GetCodeengineService(config.CEClient) + if err != nil { + log.Panicf("could not get the codeengine service") + } + + projectID = os.Getenv("CE_PROJECT_ID") + if projectID == "" { + log.Panicf("CE_PROJECT_ID is not set") + } + + observerConsumerGroup := os.Getenv("OBSERVER_CONSUMER_GROUP") + if observerConsumerGroup == "" { + log.Printf("consumer group for the observer is not set, so it defaults to observer-group") + observerConsumerGroup = "observer-group" + } + + version = sarama.DefaultVersion.String() + version, err := sarama.ParseKafkaVersion(version) + if err != nil { + log.Panicf("Error parsing Kafka version: %v", err) + } + + saramaConfig := sarama.NewConfig() + saramaConfig.Version = version + saramaConfig.Consumer.Offsets.AutoCommit.Enable = true + saramaConfig.ClientID, _ = os.Hostname() + saramaConfig.Net.SASL.Enable = true + saramaConfig.Net.SASL.User = config.KafkaAuth.User + saramaConfig.Net.SASL.Password = config.KafkaAuth.Token + saramaConfig.Net.TLS.Enable = true + + if oldest { + saramaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest + } + + saramaConfig.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.NewBalanceStrategyRoundRobin()} + + consumer := Consumer{ + ready: make(chan bool), + ce: ceService, + topicToJD: config.KafkaCE, + } + + brokers := config.Kafka.Brokers + + ctx, cancel := context.WithCancel(context.Background()) + client, err := sarama.NewConsumerGroup(brokers, observerConsumerGroup, saramaConfig) + if err != nil { + log.Panicf("Error creating consumer group client: %v", err) + } + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for { + if err := client.Consume(ctx, config.Kafka.Topics, &consumer); err != nil { + if errors.Is(err, sarama.ErrClosedConsumerGroup) { + return + } + log.Panicf("Error from consumer: %v", err) + } + if ctx.Err() != nil { + return + } + consumer.ready = make(chan bool) + } + }() + + <-consumer.ready + log.Println("Sarama consumer up and running, listening to topics : ", config.Kafka.Topics) + + sigusr1 := make(chan os.Signal, 1) + signal.Notify(sigusr1, syscall.SIGUSR1) + + sigterm := make(chan os.Signal, 1) + signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM) + + for keepRunning { + select { + case <-ctx.Done(): + log.Println("terminating: context cancelled") + keepRunning = false + case <-sigterm: + log.Println("terminating: via signal") + keepRunning = false + } + } + cancel() + + wg.Wait() + if err = client.Close(); err != nil { + log.Panicf("Error closing client: %v", err) + } +} + +func (consumer *Consumer) Setup(sarama.ConsumerGroupSession) error { + close(consumer.ready) + return nil +} + +func (consumer *Consumer) Cleanup(sarama.ConsumerGroupSession) error { + return nil +} + +func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { + for { + select { + case message, ok := <-claim.Messages(): + if !ok { + log.Printf("message channel was closed") + return nil + } + log.Printf("Message claimed: value = %s, timestamp = %v, topic = %s, partition = %v, offset = %v", string(message.Value), message.Timestamp, message.Topic, message.Partition, message.Offset) + + exists := false + var topicConfig cmd.TopicsToJobs + if topicConfig, exists = consumer.topicToJD[message.Topic]; !exists { + // if topic is not known, we will ignore the msg. + session.MarkMessage(message, "") + continue + } + + if len(topicConfig.Jobs) == 0 { + // if no job is specified for the topic. + session.MarkMessage(message, "") + continue + } + + failuresCounter := 0 + + wg := &sync.WaitGroup{} + + // jobrun creation happens inside a goroutine with a wait group + for _, jd := range topicConfig.Jobs { + wg.Add(1) + go func(name string) { + defer wg.Done() + // check Jobrun if it is already running + desiredPartitions, err := strconv.ParseInt(consumer.topicToJD[message.Topic].Partitions, 10, 64) + if err != nil { + log.Panicf("No of partitions not defined for the topic %s", consumer.topicToJD[message.Topic]) + } + // Adding lock to avoid creation of jobruns by multiple threads at the same time + consumer.Lock() + indices := getIndicesTobeCreated(consumer.ce, name, desiredPartitions) + if indices != "" { + err := CreateJobrun(consumer.ce, name, indices) + if err != nil { + failuresCounter++ + log.Printf("creating Jobrun %s failed", name) + log.Println(err.Error()) + return + } + } + consumer.Unlock() + }(jd) + } + wg.Wait() + + if failuresCounter > 0 { + // dont commit the msg, retry + return nil + } + + session.MarkMessage(message, "") + + case <-session.Context().Done(): + log.Printf("completed") + return nil + } + } +} + +// GetCodeengineService returns a codeengine client +func GetCodeengineService(ceConfig cmd.CEClient) (*codeenginev2.CodeEngineV2, error) { + authenticator := &core.IamAuthenticator{ + ApiKey: ceConfig.IamApiKey, + ClientId: "bx", + ClientSecret: "bx", + URL: "https://iam.test.cloud.ibm.com", + } + + codeEngineService, err := codeenginev2.NewCodeEngineV2(&codeenginev2.CodeEngineV2Options{ + Authenticator: authenticator, + URL: "https://api." + "dev-serving.codeengine.dev.cloud.ibm.com/v2", + }) + if err != nil { + log.Printf("NewCodeEngineV2 error: %s\n", err.Error()) + return nil, err + } + return codeEngineService, nil +} + +// CreateJobrun creates the jobrun for a job +func CreateJobrun(codeEngineService *codeenginev2.CodeEngineV2, job string, arraySpec string) error { + createJobRunOptions := codeEngineService.NewCreateJobRunOptions(projectID) + createJobRunOptions.SetJobName(job) + createJobRunOptions.SetScaleArraySpec(arraySpec) + + log.Printf("Creating jobrun for job %s with arrayspec %s", job, arraySpec) + result, _, err := codeEngineService.CreateJobRun(createJobRunOptions) + if err != nil { + return err + } + + log.Printf("Jobrun %s created for job %s ", *result.Name, *result.JobName) + + retryTimes := 0 + + for { + getJobRunOptions := codeEngineService.NewGetJobRunOptions( + projectID, + *result.Name, + ) + + // For now ignoring error + jr, _, _ := codeEngineService.GetJobRun(getJobRunOptions) + var podsActive int64 + if jr != nil { + if jr.StatusDetails != nil { + if jr.StatusDetails.Requested != nil { + podsActive += *jr.StatusDetails.Requested + } + if jr.StatusDetails.Running != nil { + podsActive += *jr.StatusDetails.Running + } + if jr.StatusDetails.Pending != nil { + podsActive += *jr.StatusDetails.Pending + } + } + } + if podsActive > 0 { + log.Printf("Jobrun %s became active", *result.Name) + break + } + + if retryTimes > 4 { + log.Printf("couldn't get the jobrun %s in 5 retries", *result.Name) + return errors.New("couldn't get the jobrun " + *result.Name + "in 5 retries ") + } + + time.Sleep(2 * time.Second) + retryTimes++ + } + return nil +} + +// getIndicesTobeCreated returns the arrayspec for a jobrun based on existing pods +func getIndicesTobeCreated(codeEngineService *codeenginev2.CodeEngineV2, jobName string, partitions int64) string { + var alreadyCreated int64 + listJobRunsOptions := &codeenginev2.ListJobRunsOptions{ + ProjectID: core.StringPtr(projectID), + JobName: core.StringPtr(jobName), + Limit: core.Int64Ptr(int64(100)), + } + + pager, err := codeEngineService.NewJobRunsPager(listJobRunsOptions) + if err != nil { + panic(err) + } + + var allResults []codeenginev2.JobRun + for pager.HasNext() { + nextPage, err := pager.GetNext() + if err != nil { + panic(err) + } + allResults = append(allResults, nextPage...) + } + + if len(allResults) == 0 { + return "0-" + strconv.FormatInt((partitions-1), 10) + } + + for _, jr := range allResults { + if jr.StatusDetails != nil { + if jr.StatusDetails.Requested != nil { + alreadyCreated += *jr.StatusDetails.Requested + } + if jr.StatusDetails.Running != nil { + alreadyCreated += *jr.StatusDetails.Running + } + if jr.StatusDetails.Pending != nil { + alreadyCreated += *jr.StatusDetails.Pending + } + } + } + newpods := partitions - alreadyCreated + if newpods <= 0 { + return "" + } + reqArraySpec := "0-" + strconv.FormatInt(newpods-1, 10) + return reqArraySpec +} diff --git a/kafka-observer/go.mod b/kafka-observer/go.mod new file mode 100644 index 00000000..03eeecb4 --- /dev/null +++ b/kafka-observer/go.mod @@ -0,0 +1,48 @@ +module github.com/IBM/CodeEngine/kafka-observer + +go 1.21.0 + +require ( + github.com/IBM/code-engine-go-sdk v0.0.0-20240126185534-a6e054aa01ed + github.com/IBM/go-sdk-core/v5 v5.15.0 + github.com/IBM/sarama v1.43.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/eapache/go-resiliency v1.6.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/go-openapi/errors v0.21.0 // indirect + github.com/go-openapi/strfmt v0.22.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.15.5 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.4 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + go.mongodb.org/mongo-driver v1.13.1 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/kafka-observer/go.sum b/kafka-observer/go.sum new file mode 100644 index 00000000..d227b329 --- /dev/null +++ b/kafka-observer/go.sum @@ -0,0 +1,172 @@ +github.com/IBM/code-engine-go-sdk v0.0.0-20240126185534-a6e054aa01ed h1:X0VrZW5ulbqxbOmy5JoZcH0A+tw80k0/ZmRZz1NqogM= +github.com/IBM/code-engine-go-sdk v0.0.0-20240126185534-a6e054aa01ed/go.mod h1:m4pD/58c6NVzlAFkN3XCYXpmDFmUyTG31ivLy/loyHQ= +github.com/IBM/go-sdk-core/v5 v5.15.0 h1:AhFoWVk3i58f9vnDoEoZumI/zbtRoP5moWIz5YQOmZg= +github.com/IBM/go-sdk-core/v5 v5.15.0/go.mod h1:5Obavm/s1Tc2PxivEIfgCvj/HJ5h3QIOjLHS5y8QJf0= +github.com/IBM/sarama v1.43.0 h1:YFFDn8mMI2QL0wOrG0J2sFoVIAFl7hS9JQi2YZsXtJc= +github.com/IBM/sarama v1.43.0/go.mod h1:zlE6HEbC/SMQ9mhEYaF7nNLYOUyrs0obySKCckWP9BM= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30= +github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY= +github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho= +github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI= +github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= +github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= +go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kafka-observer/images/kafkapoc.jpg b/kafka-observer/images/kafkapoc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6d82fb7c06d8890de25cc60ef19fc7c417a03893 GIT binary patch literal 53517 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