snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s
Some checks failed
master / ci (push) Failing after 0s
This commit is contained in:
40
tools/mill/.default-config.yml
Normal file
40
tools/mill/.default-config.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# These are the current default settings for the Watermill CLI tool.
|
||||
# Use them as a template for your own config
|
||||
log: false
|
||||
trace: false
|
||||
debug: false
|
||||
|
||||
amqp:
|
||||
uri: ""
|
||||
durable: true
|
||||
consume:
|
||||
exchange: ""
|
||||
queue: ""
|
||||
produce:
|
||||
exchange: ""
|
||||
exchangetype: fanout
|
||||
routingkey: ""
|
||||
|
||||
googlecloud:
|
||||
projectid: ""
|
||||
topic: ""
|
||||
consume:
|
||||
subscription: ""
|
||||
produce:
|
||||
subscription:
|
||||
add:
|
||||
ackdeadline: 10s
|
||||
labels: ""
|
||||
retainacked: false
|
||||
retentionduration: 168h0m0s
|
||||
rm:
|
||||
# no flags for rm yet
|
||||
|
||||
kafka:
|
||||
brokers: []
|
||||
topic: ""
|
||||
consume:
|
||||
consumergroup: ""
|
||||
frombeginning: false
|
||||
produce:
|
||||
# no flags for produce yet
|
||||
2
tools/mill/Makefile
Normal file
2
tools/mill/Makefile
Normal file
@@ -0,0 +1,2 @@
|
||||
mill:
|
||||
go build -o mill main.go
|
||||
100
tools/mill/README.md
Normal file
100
tools/mill/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# mill - a simple CLI tool for Watermill
|
||||
|
||||
`mill` is a CLI tool for the [Watermill](https://watermill.io) library.
|
||||
|
||||
It has two basic functionalities, namely producing and consuming messages on the following
|
||||
Pub/Subs:
|
||||
|
||||
1. Kafka
|
||||
2. Google Cloud Pub/Sub
|
||||
3. RabbitMQ
|
||||
|
||||
See Watermill's [Supported Pub/Subs](https://watermill.io/pubsubs) for more details on how this works.
|
||||
|
||||
## Installation
|
||||
|
||||
To install this tool, just execute:
|
||||
|
||||
```bash
|
||||
go install github.com/ThreeDotsLabs/watermill/tools/mill@latest
|
||||
```
|
||||
|
||||
This will install a `mill` binary in your system.
|
||||
|
||||
## Consume mode
|
||||
|
||||
In consume mode, the tool subscribes to a topic/queue/subscription (nomenclature depending on the particular Pub/Sub provider)
|
||||
and prints the messages' payload to the standard output.
|
||||
|
||||
Other outputs, for example ones that add a timestamp or preserve UUIDs or metadata, are easily attainable by modification
|
||||
of the marshaling function of the `io.Publisher` of the `consumeCmd`.
|
||||
|
||||
## Produce mode
|
||||
|
||||
In produce mode, subsequent lines of data from the standard input are transformed into messages outgoing to the requested
|
||||
provider's topic/exchange.
|
||||
|
||||
The message's payload is set to the line from stdin, the UUID is auto-generated and the metadata is empty.
|
||||
|
||||
Similarly, the contents of the message could be parsed differently from stdin, by modification
|
||||
of the unmarshaling function of the `io.Subscriber` of the `produceCmd`.
|
||||
|
||||
## Usage
|
||||
|
||||
The basic syntax of the tool is:
|
||||
|
||||
```bash
|
||||
mill <provider> <command>
|
||||
```
|
||||
|
||||
with the appropriate flags regulating the specific behaviour of each command.
|
||||
|
||||
`command` is usually one of `produce` or `consume`, but some providers may handle additional commands
|
||||
that are specific for them.
|
||||
|
||||
The flags are context-specific, so the best way to find out about them is to use the `-h` flag and study
|
||||
which flags are available/required for the specific context and act accordingly.
|
||||
|
||||
### Advanced usage
|
||||
|
||||
A neat feature of producers and consumers is that you can use the power of stdin/stdout piping for stuff like:
|
||||
|
||||
```bash
|
||||
myservice | tee myservice.log | mill kafka produce -t myservice-logs --brokers kafka-host:8082
|
||||
```
|
||||
|
||||
And on another host:
|
||||
|
||||
```bash
|
||||
mill kafka consume -t myservice-logs --brokers kafka-host:8082 >> myservice.log
|
||||
```
|
||||
|
||||
In the above example, the host on which `myservice` runs has its own copy of `myservice.log` and any host that consumes
|
||||
from the kafka topic will replicate the log entries in their local copy.
|
||||
|
||||
|
||||
## Additional functionalities
|
||||
|
||||
### Google Cloud Pub/Sub
|
||||
|
||||
#### Adding/Removing subscriptions
|
||||
|
||||
You can use `mill` to create/remove subscriptions for Google Cloud Pub/Sub:
|
||||
|
||||
```bash
|
||||
mill googlecloud subscription add -t <topic> <subscription_id>
|
||||
|
||||
mill googlecloud subscription rm <subscription_id>
|
||||
```
|
||||
|
||||
Additional flags are available for `subscription add` to regulate the newly created subscription's settings.
|
||||
|
||||
#### Listing subscriptions
|
||||
|
||||
You can use `mill` to list existing subscriptions:
|
||||
|
||||
```bash
|
||||
mill googlecloud subscription ls [-t topic]
|
||||
```
|
||||
|
||||
The topic is optional. If omitted, all topics will be listed with their subscriptions.
|
||||
174
tools/mill/cmd/amqp.go
Normal file
174
tools/mill/cmd/amqp.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill-amqp/v3/pkg/amqp"
|
||||
)
|
||||
|
||||
var amqpCmd = &cobra.Command{
|
||||
Use: "amqp",
|
||||
Short: "Commands for the AMQP Pub/Sub provider",
|
||||
Long: `Consume or produce messages from the AMQP Pub/Sub provider.
|
||||
|
||||
For the configuration of consuming/producing of the messages, check the help of the relevant command.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := rootCmd.PersistentPreRunE(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("Using AMQP Pub/Sub", nil)
|
||||
|
||||
if cmd.Use == "consume" {
|
||||
consumer, err = amqp.NewSubscriber(amqpConsumerConfig(), logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Use == "produce" {
|
||||
producer, err = amqp.NewPublisher(amqpProducerConfig(), logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func amqpConsumerConfig() amqp.Config {
|
||||
uri := viper.GetString("amqp.uri")
|
||||
queue := viper.GetString("amqp.consume.queue")
|
||||
exchangeName := viper.GetString("amqp.consume.exchange")
|
||||
exchangeType := viper.GetString("amqp.produce.exchangeType")
|
||||
durable := viper.GetBool("amqp.durable")
|
||||
|
||||
return amqp.Config{
|
||||
Connection: amqp.ConnectionConfig{
|
||||
AmqpURI: uri,
|
||||
},
|
||||
Marshaler: amqp.DefaultMarshaler{},
|
||||
Queue: amqp.QueueConfig{
|
||||
GenerateName: func(topic string) string {
|
||||
return queue
|
||||
},
|
||||
Durable: durable,
|
||||
},
|
||||
Consume: amqp.ConsumeConfig{
|
||||
Qos: amqp.QosConfig{
|
||||
PrefetchCount: 1,
|
||||
},
|
||||
},
|
||||
|
||||
Exchange: amqp.ExchangeConfig{
|
||||
GenerateName: func(topic string) string {
|
||||
return exchangeName
|
||||
},
|
||||
Type: exchangeType,
|
||||
Durable: durable,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func amqpProducerConfig() amqp.Config {
|
||||
uri := viper.GetString("amqp.uri")
|
||||
exchangeName := viper.GetString("amqp.produce.exchange")
|
||||
exchangeType := viper.GetString("amqp.produce.exchangeType")
|
||||
routingKey := viper.GetString("amqp.produce.routingKey")
|
||||
durable := viper.GetBool("amqp.durable")
|
||||
|
||||
return amqp.Config{
|
||||
Connection: amqp.ConnectionConfig{
|
||||
AmqpURI: uri,
|
||||
},
|
||||
Marshaler: amqp.DefaultMarshaler{},
|
||||
Exchange: amqp.ExchangeConfig{
|
||||
GenerateName: func(topic string) string {
|
||||
return exchangeName
|
||||
},
|
||||
Type: exchangeType,
|
||||
Durable: durable,
|
||||
},
|
||||
Publish: amqp.PublishConfig{
|
||||
GenerateRoutingKey: func(topic string) string {
|
||||
return routingKey
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(amqpCmd)
|
||||
configureAmqpCmd()
|
||||
consumeCmd := addConsumeCmd(amqpCmd, "amqp.consume.queue")
|
||||
configureConsumeCmd(consumeCmd)
|
||||
produceCmd := addProduceCmd(amqpCmd, "amqp.produce.exchange")
|
||||
configureProduceCmd(produceCmd)
|
||||
}
|
||||
|
||||
func configureAmqpCmd() {
|
||||
amqpCmd.PersistentFlags().StringP(
|
||||
"uri",
|
||||
"u",
|
||||
"",
|
||||
"The URI to the AMQP instance (required)",
|
||||
)
|
||||
ensure(amqpCmd.MarkPersistentFlagRequired("uri"))
|
||||
ensure(viper.BindPFlag("amqp.uri", amqpCmd.PersistentFlags().Lookup("uri")))
|
||||
|
||||
amqpCmd.PersistentFlags().Bool(
|
||||
"durable",
|
||||
true,
|
||||
"If true, the queues and exchanges created automatically (if any) will be durable",
|
||||
)
|
||||
ensure(viper.BindPFlag("amqp.durable", amqpCmd.PersistentFlags().Lookup("durable")))
|
||||
|
||||
amqpCmd.PersistentFlags().String(
|
||||
"exchange-type",
|
||||
"fanout",
|
||||
"If exchange needs to be created, it will be created with this type. The common types are 'direct', 'fanout', 'topic' and 'headers'.",
|
||||
)
|
||||
ensure(viper.BindPFlag("amqp.produce.exchangeType", amqpCmd.PersistentFlags().Lookup("exchange-type")))
|
||||
}
|
||||
|
||||
func configureConsumeCmd(consumeCmd *cobra.Command) {
|
||||
consumeCmd.PersistentFlags().StringP(
|
||||
"queue",
|
||||
"q",
|
||||
"",
|
||||
"The name of the AMQP queue to consume messages from (required)",
|
||||
)
|
||||
ensure(consumeCmd.MarkPersistentFlagRequired("queue"))
|
||||
ensure(viper.BindPFlag("amqp.consume.queue", consumeCmd.PersistentFlags().Lookup("queue")))
|
||||
|
||||
consumeCmd.PersistentFlags().StringP(
|
||||
"exchange",
|
||||
"x",
|
||||
"",
|
||||
"If non-empty, an exchange with this name is created if it didn't exist. Then, the queue is bound to this exchange.",
|
||||
)
|
||||
ensure(viper.BindPFlag("amqp.consume.exchange", consumeCmd.PersistentFlags().Lookup("exchange")))
|
||||
}
|
||||
|
||||
func configureProduceCmd(produceCmd *cobra.Command) {
|
||||
produceCmd.PersistentFlags().StringP(
|
||||
"exchange",
|
||||
"x",
|
||||
"",
|
||||
"The name of the AMQP exchange to produce messages to (required)",
|
||||
)
|
||||
ensure(produceCmd.MarkPersistentFlagRequired("exchange"))
|
||||
ensure(viper.BindPFlag("amqp.produce.exchange", produceCmd.PersistentFlags().Lookup("exchange")))
|
||||
|
||||
produceCmd.PersistentFlags().StringP(
|
||||
"routing-key",
|
||||
"r",
|
||||
"",
|
||||
"The routing key to use when publishing the message.",
|
||||
)
|
||||
ensure(produceCmd.MarkPersistentFlagRequired("routing-key"))
|
||||
ensure(viper.BindPFlag("amqp.produce.routingKey", produceCmd.PersistentFlags().Lookup("routing-key")))
|
||||
}
|
||||
70
tools/mill/cmd/consume.go
Normal file
70
tools/mill/cmd/consume.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill-io/pkg/io"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/plugin"
|
||||
)
|
||||
|
||||
// consumer is initialized by the parent command to the pub/sub provider of choice.
|
||||
var consumer message.Subscriber
|
||||
|
||||
func addConsumeCmd(parent *cobra.Command, topicKey string) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "consume",
|
||||
Short: "Consume messages from a pub/sub and print them to stdout",
|
||||
Long: `Consume messages from the pub/sub of your choice and print them on the standard output.
|
||||
|
||||
For the configuration of particular pub/sub providers, see the help for the provider commands.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
topic := viper.GetString(topicKey)
|
||||
router, err := message.NewRouter(
|
||||
message.RouterConfig{
|
||||
CloseTimeout: 5 * time.Second,
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create router")
|
||||
}
|
||||
|
||||
router.AddPlugin(plugin.SignalsHandler)
|
||||
|
||||
out, err := io.NewPublisher(
|
||||
os.Stdout,
|
||||
io.PublisherConfig{
|
||||
MarshalFunc: io.PayloadMarshalFunc,
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create console producer")
|
||||
}
|
||||
|
||||
router.AddHandler(
|
||||
"dump_to_stdout",
|
||||
topic,
|
||||
consumer,
|
||||
"",
|
||||
out,
|
||||
func(msg *message.Message) ([]*message.Message, error) {
|
||||
// just forward the message to stdout
|
||||
return message.Messages{msg}, nil
|
||||
},
|
||||
)
|
||||
|
||||
return router.Run(context.Background())
|
||||
},
|
||||
}
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
return cmd
|
||||
}
|
||||
487
tools/mill/cmd/googlecloud.go
Normal file
487
tools/mill/cmd/googlecloud.go
Normal file
@@ -0,0 +1,487 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/pubsub"
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill-googlecloud/pkg/googlecloud"
|
||||
"github.com/ThreeDotsLabs/watermill/tools/mill/cmd/internal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/api/iterator"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var googleCloudTempSubscriptionID string
|
||||
|
||||
var googleCloudCmd = &cobra.Command{
|
||||
Use: "googlecloud",
|
||||
Short: "Commands for the Google Cloud Pub/Sub provider",
|
||||
Long: `Consume or produce messages from the Google Cloud Pub/Sub provider. Manage subscriptions.
|
||||
|
||||
For the configuration of consuming/producing of the messages, check the help of the relevant command.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := rootCmd.PersistentPreRunE(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("Using Google Cloud Pub/Sub", nil)
|
||||
if cmd.Use == "consume" {
|
||||
subName := viper.GetString("googlecloud.consume.subscription")
|
||||
if subName == "" {
|
||||
subName, err = generateTempSubscription()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
consumer, err = googlecloud.NewSubscriber(
|
||||
googlecloud.SubscriberConfig{
|
||||
GenerateSubscriptionName: func(topic string) string {
|
||||
return subName
|
||||
},
|
||||
ProjectID: projectID(),
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Use == "produce" {
|
||||
producer, err = googlecloud.NewPublisher(
|
||||
googlecloud.PublisherConfig{
|
||||
ProjectID: projectID(),
|
||||
},
|
||||
logger,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger.Debug("Google Cloud Pub/Sub cleanup", nil)
|
||||
if googleCloudTempSubscriptionID != "" {
|
||||
if err := removeTempSubscription(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var googleCloudSubscriptionCmd = &cobra.Command{
|
||||
Use: "subscription",
|
||||
Short: "Manage Google Cloud Pub/Sub subscriptions",
|
||||
Long: `Add or remove subscriptions for the Google Cloud Pub/Sub provider.`,
|
||||
}
|
||||
|
||||
var googleCloudSubscriptionAddCmd = &cobra.Command{
|
||||
Use: "add <subscription_id>",
|
||||
Short: "Add a new subscription in Google Cloud Pub/Sub",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgs: []string{"subscriptionID"},
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
subID := args[0]
|
||||
|
||||
topic := viper.GetString("googlecloud.subscription.add.topic")
|
||||
ackDeadline := viper.GetDuration("googlecloud.subscription.add.ackDeadline")
|
||||
retainAcked := viper.GetBool("googlecloud.subscription.add.retainAcked")
|
||||
retentionDuration := viper.GetDuration("googlecloud.subscription.add.retentionDuration")
|
||||
|
||||
// StringToString doesn't work with viper, so let's parse this manually
|
||||
labels := strings.Split(viper.GetString("googlecloud.subscription.add.labels"), ",")
|
||||
labelsMap := make(map[string]string, len(labels))
|
||||
for _, l := range labels {
|
||||
fields := strings.Split(l, "=")
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
labelsMap[fields[0]] = fields[1]
|
||||
}
|
||||
|
||||
logger := logger.With(watermill.LogFields{
|
||||
"subscription_id": subID,
|
||||
"topic": topic,
|
||||
"ackDeadline": ackDeadline,
|
||||
"retainAcked": retainAcked,
|
||||
"retentionDuration": retentionDuration,
|
||||
"labels": labelsMap,
|
||||
})
|
||||
logger.Info("Creating new subscription", nil)
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
logger.Info("Subscription created", nil)
|
||||
}
|
||||
}()
|
||||
|
||||
return addSubscription(
|
||||
subID,
|
||||
topic,
|
||||
ackDeadline,
|
||||
retainAcked,
|
||||
retentionDuration,
|
||||
labelsMap,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
var googleCloudSubscriptionRmCmd = &cobra.Command{
|
||||
Use: "rm <subscription_id>",
|
||||
Short: "Remove a subscription in Google Cloud Pub/Sub",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgs: []string{"subscriptionID"},
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
subID := args[0]
|
||||
|
||||
logger := logger.With(watermill.LogFields{
|
||||
"subscription_id": subID,
|
||||
})
|
||||
logger.Info("Removing a subscription", nil)
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
logger.Info("Subscription removed", nil)
|
||||
}
|
||||
}()
|
||||
|
||||
return removeSubscription(subID)
|
||||
},
|
||||
}
|
||||
|
||||
var googleCloudSubscriptionLsCmd = &cobra.Command{
|
||||
Use: "ls",
|
||||
Short: "List subscriptions in Google Cloud Pub/Sub. Topic may be provided optionally to filter subscriptions by topic.",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
topic := viper.GetString("googlecloud.subscription.ls.topic")
|
||||
verbose := viper.GetBool("googlecloud.subscription.ls.verbose")
|
||||
|
||||
logger := logger
|
||||
if topic != "" {
|
||||
logger = logger.With(watermill.LogFields{
|
||||
"topic": topic,
|
||||
})
|
||||
}
|
||||
logger.Info("Listing all subscriptions", nil)
|
||||
|
||||
return listSubscriptions(topic, logger, verbose)
|
||||
},
|
||||
}
|
||||
|
||||
func generateTempSubscription() (id string, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
logger.Debug("Temp subscription created", watermill.LogFields{
|
||||
"subscription_name": id,
|
||||
})
|
||||
googleCloudTempSubscriptionID = id
|
||||
}
|
||||
}()
|
||||
|
||||
randomID := "watermill_console_consumer_" + watermill.NewShortUUID()
|
||||
return randomID, addSubscription(
|
||||
randomID,
|
||||
viper.GetString("googlecloud.topic"),
|
||||
10*time.Second,
|
||||
false,
|
||||
10*time.Minute,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func addSubscription(
|
||||
id string,
|
||||
topic string,
|
||||
ackDeadline time.Duration,
|
||||
retainAckedMessages bool,
|
||||
retentionDuration time.Duration,
|
||||
labels map[string]string,
|
||||
) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
client, err := pubsub.NewClient(ctx, projectID())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create pubsub client")
|
||||
}
|
||||
|
||||
t := client.Topic(topic)
|
||||
exists, err := t.Exists(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not check if topic exists")
|
||||
}
|
||||
if !exists {
|
||||
t, err = client.CreateTopic(ctx, t.ID())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create topic")
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.CreateSubscription(ctx, id, pubsub.SubscriptionConfig{
|
||||
Topic: t,
|
||||
AckDeadline: ackDeadline,
|
||||
RetainAckedMessages: retainAckedMessages,
|
||||
RetentionDuration: retentionDuration,
|
||||
Labels: labels,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create subscription")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeTempSubscription() (err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
logger.Debug("Temporary subscription removed", watermill.LogFields{
|
||||
"subscription_name": googleCloudTempSubscriptionID,
|
||||
})
|
||||
}
|
||||
}()
|
||||
return removeSubscription(googleCloudTempSubscriptionID)
|
||||
}
|
||||
|
||||
func removeSubscription(id string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
client, err := pubsub.NewClient(ctx, projectID())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create pubsub client")
|
||||
}
|
||||
|
||||
sub := client.Subscription(id)
|
||||
exists, err := sub.Exists(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not check if sub exists")
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sub.Delete(ctx)
|
||||
}
|
||||
|
||||
func listSubscriptions(topic string, adapter watermill.LoggerAdapter, verbose bool) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
client, err := pubsub.NewClient(ctx, projectID())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create pubsub client")
|
||||
}
|
||||
|
||||
if topic != "" {
|
||||
topic := client.Topic(topic)
|
||||
return listSubscriptionsForTopic(ctx, client, topic, logger, verbose)
|
||||
}
|
||||
|
||||
it := client.Topics(ctx)
|
||||
noTopics := true
|
||||
for {
|
||||
topic, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
if noTopics {
|
||||
logger.Info("No topics in project", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve next subscription")
|
||||
}
|
||||
|
||||
noTopics = false
|
||||
err = listSubscriptionsForTopic(ctx, client, topic, logger, verbose)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error listing subscriptions for topic")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listSubscriptionsForTopic(ctx context.Context, client *pubsub.Client, topic *pubsub.Topic, logger watermill.LoggerAdapter, verbose bool) error {
|
||||
noSubs := true
|
||||
exists, err := topic.Exists(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not check if topic exists")
|
||||
}
|
||||
if !exists {
|
||||
logger.Info("Topic does not exist", watermill.LogFields{"topic": topic.String()})
|
||||
return nil
|
||||
}
|
||||
|
||||
it := topic.Subscriptions(ctx)
|
||||
for {
|
||||
sub, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
if noSubs {
|
||||
logger.Info("No subscriptions for the topic", watermill.LogFields{"topic": topic.String()})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not retrieve next subscription")
|
||||
}
|
||||
|
||||
if noSubs {
|
||||
noSubs = false
|
||||
fmt.Printf("Topic %s:\n", topic.String())
|
||||
}
|
||||
name := sub.String()
|
||||
config, err := sub.Config(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not retrieve subscription config for subscription '%s'", name)
|
||||
}
|
||||
|
||||
err = printSubscriptionInfo(name, config)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error printing subscription '%s'", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printSubscriptionInfo(name string, config pubsub.SubscriptionConfig) error {
|
||||
b, err := yaml.Marshal(subscriptionConfig{
|
||||
Name: name,
|
||||
PushConfig: subscriptionConfigPushConfig{
|
||||
Endpoint: config.PushConfig.Endpoint,
|
||||
Attributes: config.PushConfig.Attributes,
|
||||
},
|
||||
AckDeadline: config.AckDeadline,
|
||||
RetainAckedMessages: config.RetainAckedMessages,
|
||||
RetentionDuration: config.RetentionDuration,
|
||||
Labels: config.Labels,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(internal.Indent(string(b), " "))
|
||||
return nil
|
||||
}
|
||||
|
||||
// subscriptionConfig provides a marshallable form to pubsub.SubscriptionConfig
|
||||
type subscriptionConfig struct {
|
||||
Name string
|
||||
PushConfig subscriptionConfigPushConfig `yaml:"push_config"`
|
||||
AckDeadline time.Duration `yaml:"ack_deadline"`
|
||||
RetainAckedMessages bool `yaml:"retain_acked_messages"`
|
||||
RetentionDuration time.Duration `yaml:"retention_duration"`
|
||||
Labels map[string]string `yaml:"labels"`
|
||||
}
|
||||
|
||||
type subscriptionConfigPushConfig struct {
|
||||
Endpoint string
|
||||
Attributes map[string]string
|
||||
}
|
||||
|
||||
func projectID() string {
|
||||
projectID := viper.GetString("googlecloud.projectID")
|
||||
if projectID == "" {
|
||||
projectID = os.Getenv("GOOGLE_CLOUD_PROJECT")
|
||||
}
|
||||
|
||||
return projectID
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(googleCloudCmd)
|
||||
|
||||
googleCloudCmd.PersistentFlags().StringP(
|
||||
"topic",
|
||||
"t",
|
||||
"",
|
||||
"The topic to produce messages to (produce), consume message from (consume), list from (ls) or the topic for the newly created subscription (subscription.add)",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.topic", googleCloudCmd.PersistentFlags().Lookup("topic")))
|
||||
ensure(googleCloudCmd.MarkPersistentFlagRequired("topic"))
|
||||
|
||||
consumeCmd := addConsumeCmd(googleCloudCmd, "googlecloud.topic")
|
||||
addProduceCmd(googleCloudCmd, "googlecloud.topic")
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
googleCloudCmd.PersistentFlags().String("project", "", "The projectID for Google Cloud Pub/Sub. Defaults to the GOOGLE_CLOUD_PROJECT environment variable.")
|
||||
ensure(viper.BindPFlag("googlecloud.projectID", googleCloudCmd.PersistentFlags().Lookup("project")))
|
||||
|
||||
consumeCmd.PersistentFlags().StringP(
|
||||
"subscription",
|
||||
"s",
|
||||
"",
|
||||
"The subscription for Google Cloud Pub/Sub. If left empty, a temporary subscription is created and removed when the consumer is closed",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.consume.subscription", consumeCmd.PersistentFlags().Lookup("subscription")))
|
||||
|
||||
googleCloudCmd.AddCommand(googleCloudSubscriptionCmd)
|
||||
googleCloudSubscriptionCmd.AddCommand(googleCloudSubscriptionAddCmd)
|
||||
|
||||
googleCloudSubscriptionAddCmd.Flags().StringP("topic", "t", "", "The topic for the new subscription (required)")
|
||||
ensure(googleCloudSubscriptionAddCmd.MarkFlagRequired("topic"))
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.add.topic", googleCloudSubscriptionAddCmd.Flags().Lookup("topic")))
|
||||
|
||||
googleCloudSubscriptionAddCmd.Flags().DurationP(
|
||||
"ack-deadline",
|
||||
"a",
|
||||
10*time.Second,
|
||||
"How long Pub/Sub waits for the subscriber to acknowledge receipt before resending the message. Deadline time is from 10 seconds to 600 seconds",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.add.ackDeadline", googleCloudSubscriptionAddCmd.Flags().Lookup("ack-deadline")))
|
||||
|
||||
googleCloudSubscriptionAddCmd.Flags().Bool(
|
||||
"retain-acked",
|
||||
false,
|
||||
"Acknowledged messages will be kept 7 days from publication unless set otherwise in \"message retention duration\".",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.add.retainAcked", googleCloudSubscriptionAddCmd.Flags().Lookup("retain-acked")))
|
||||
|
||||
googleCloudSubscriptionAddCmd.Flags().Duration(
|
||||
"retention-duration",
|
||||
7*24*time.Hour,
|
||||
"How long the retained messages will be kept. The allowed duration is from 10 minutes to 7 days, which is the default.",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.add.retentionDuration", googleCloudSubscriptionAddCmd.Flags().Lookup("retention-duration")))
|
||||
|
||||
// StringToString doesn't work correctly with viper
|
||||
googleCloudSubscriptionAddCmd.Flags().String(
|
||||
"labels",
|
||||
"",
|
||||
"The set of labels for the subscription. Format: '--labels key1=value1,key2=value2,...'",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.add.labels", googleCloudSubscriptionAddCmd.Flags().Lookup("labels")))
|
||||
|
||||
googleCloudSubscriptionCmd.AddCommand(googleCloudSubscriptionRmCmd)
|
||||
|
||||
googleCloudSubscriptionCmd.AddCommand(googleCloudSubscriptionLsCmd)
|
||||
googleCloudSubscriptionLsCmd.Flags().StringP(
|
||||
"topic",
|
||||
"t",
|
||||
"",
|
||||
"The topic for the new subscription (optional, will list subscriptions for all topics if omitted)",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.ls.topic", googleCloudSubscriptionLsCmd.Flags().Lookup("topic")))
|
||||
|
||||
googleCloudSubscriptionLsCmd.Flags().BoolP(
|
||||
"verbose",
|
||||
"v",
|
||||
false,
|
||||
"will print more information, including the subscription config",
|
||||
)
|
||||
ensure(viper.BindPFlag("googlecloud.subscription.ls.verbose", googleCloudSubscriptionLsCmd.Flags().Lookup("verbose")))
|
||||
}
|
||||
19
tools/mill/cmd/internal/indent.go
Normal file
19
tools/mill/cmd/internal/indent.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package internal
|
||||
|
||||
import "strings"
|
||||
|
||||
// Indent indents all lines in the given string with a given prefix.
|
||||
func Indent(s, prefix string) string {
|
||||
endsWithNewline := strings.HasSuffix(s, "\n")
|
||||
split := strings.Split(s, "\n")
|
||||
|
||||
for i, ss := range split {
|
||||
split[i] = prefix + ss
|
||||
}
|
||||
joined := strings.Join(split, "\n")
|
||||
if endsWithNewline {
|
||||
joined += "\n"
|
||||
}
|
||||
|
||||
return joined
|
||||
}
|
||||
92
tools/mill/cmd/kafka.go
Normal file
92
tools/mill/cmd/kafka.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/IBM/sarama"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill-kafka/v3/pkg/kafka"
|
||||
)
|
||||
|
||||
var kafkaCmd = &cobra.Command{
|
||||
Use: "kafka",
|
||||
Short: "Commands for the kafka Pub/Sub provider",
|
||||
Long: `Consume or produce messages from the kafka Pub/Sub provider.
|
||||
|
||||
For the configuration of consuming/producing of the messages, check the help of the relevant command.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := rootCmd.PersistentPreRunE(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("Using kafka pub/sub", nil)
|
||||
|
||||
brokers := viper.GetStringSlice("kafka.brokers")
|
||||
|
||||
if cmd.Use == "consume" {
|
||||
saramaSubscriberConfig := kafka.DefaultSaramaSubscriberConfig()
|
||||
|
||||
if viper.GetBool("kafka.consume.fromBeginning") {
|
||||
logger.Trace("Configured sarama to consume messages from beginning", nil)
|
||||
// equivalent of auto.offset.reset: earliest
|
||||
saramaSubscriberConfig.Consumer.Offsets.Initial = sarama.OffsetOldest
|
||||
}
|
||||
|
||||
consumer, err = kafka.NewSubscriber(
|
||||
kafka.SubscriberConfig{
|
||||
Brokers: brokers,
|
||||
Unmarshaler: kafka.DefaultMarshaler{},
|
||||
OverwriteSaramaConfig: saramaSubscriberConfig,
|
||||
ConsumerGroup: viper.GetString("kafka.consume.consumerGroup"),
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Use == "produce" {
|
||||
producer, err = kafka.NewPublisher(
|
||||
kafka.PublisherConfig{
|
||||
Brokers: brokers,
|
||||
Marshaler: kafka.DefaultMarshaler{},
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(kafkaCmd)
|
||||
|
||||
kafkaCmd.PersistentFlags().StringP(
|
||||
"topic",
|
||||
"t",
|
||||
"",
|
||||
"The topic to produce messages to (produce) or consume message from (consume)",
|
||||
)
|
||||
ensure(kafkaCmd.MarkPersistentFlagRequired("topic"))
|
||||
ensure(viper.BindPFlag("kafka.topic", kafkaCmd.PersistentFlags().Lookup("topic")))
|
||||
|
||||
consumeCmd := addConsumeCmd(kafkaCmd, "kafka.topic")
|
||||
_ = addProduceCmd(kafkaCmd, "kafka.topic")
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
kafkaCmd.PersistentFlags().StringSliceP("brokers", "b", nil, "A list of kafka brokers")
|
||||
ensure(kafkaCmd.MarkPersistentFlagRequired("brokers"))
|
||||
ensure(viper.BindPFlag("kafka.brokers", kafkaCmd.PersistentFlags().Lookup("brokers")))
|
||||
|
||||
consumeCmd.PersistentFlags().Bool("from-beginning", false, "Equivalent to auto.offset.reset: earliest")
|
||||
ensure(viper.BindPFlag("kafka.consume.fromBeginning", consumeCmd.PersistentFlags().Lookup("from-beginning")))
|
||||
|
||||
consumeCmd.PersistentFlags().StringP("consumer-group", "c", "", "The kafka consumer group. Defaults to empty.")
|
||||
ensure(viper.BindPFlag("kafka.consume.consumerGroup", consumeCmd.PersistentFlags().Lookup("consumer-group")))
|
||||
}
|
||||
78
tools/mill/cmd/produce.go
Normal file
78
tools/mill/cmd/produce.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill-io/pkg/io"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/plugin"
|
||||
)
|
||||
|
||||
// producer is initialized by parent command to the pub/sub provider of choice.
|
||||
var producer message.Publisher
|
||||
|
||||
func addProduceCmd(parent *cobra.Command, topicKey string) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "produce",
|
||||
Short: "Produce messages to a pub/sub from the stdin",
|
||||
Long: `Produce messages to the pub/sub of your choice from the standard input.
|
||||
|
||||
For the configuration of particular pub/sub providers, see the help for the provider commands.`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
topic := viper.GetString(topicKey)
|
||||
router, err := message.NewRouter(
|
||||
message.RouterConfig{
|
||||
CloseTimeout: 10 * time.Second,
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create router")
|
||||
}
|
||||
|
||||
router.AddPlugin(plugin.SignalsHandler)
|
||||
|
||||
in, err := io.NewSubscriber(
|
||||
os.Stdin,
|
||||
io.SubscriberConfig{
|
||||
PollInterval: time.Second,
|
||||
UnmarshalFunc: io.PayloadUnmarshalFunc,
|
||||
},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create console subscriber")
|
||||
}
|
||||
|
||||
router.AddHandler(
|
||||
"produce_from_stdin",
|
||||
"stdin",
|
||||
in,
|
||||
topic,
|
||||
producer,
|
||||
func(msg *message.Message) ([]*message.Message, error) {
|
||||
if string(msg.Payload) == "\n" {
|
||||
logger.Trace("Message is empty, don't publish", nil)
|
||||
return nil, nil
|
||||
}
|
||||
// just pass the message along
|
||||
return message.Messages{msg}, nil
|
||||
},
|
||||
)
|
||||
|
||||
return router.Run(context.Background())
|
||||
},
|
||||
}
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
return cmd
|
||||
}
|
||||
149
tools/mill/cmd/root.go
Normal file
149
tools/mill/cmd/root.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
var logger watermill.LoggerAdapter
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "mill",
|
||||
Short: "A CLI for Watermill.",
|
||||
Long: `A CLI for Watermill.
|
||||
|
||||
Use console-based producer or consumer for various pub/sub providers.`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
log := viper.GetBool("log")
|
||||
debug := viper.GetBool("debug")
|
||||
trace := viper.GetBool("trace")
|
||||
if log || debug || trace {
|
||||
logger = watermill.NewStdLogger(debug, trace)
|
||||
} else {
|
||||
logger = watermill.NopLogger{}
|
||||
}
|
||||
|
||||
if err := checkRequiredFlags(cmd.Flags()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writeConfig := viper.GetString("writeConfig")
|
||||
if writeConfig != "" {
|
||||
settings := viper.AllSettings()
|
||||
delete(settings, "writeconfig")
|
||||
b, err := yaml.Marshal(settings)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not marshal config to yaml")
|
||||
}
|
||||
|
||||
f, err := os.Create(writeConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not create file for write")
|
||||
}
|
||||
_, err = fmt.Fprintf(f, "%s", b)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not write to file")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.PersistentFlags().SortFlags = false
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mill.yaml)")
|
||||
|
||||
outputFlags := pflag.NewFlagSet("output", pflag.ExitOnError)
|
||||
outputFlags.BoolP("log", "l", false, "If true, the logger output is sent to stderr. No logger output otherwise.")
|
||||
ensure(viper.BindPFlag("log", outputFlags.Lookup("log")))
|
||||
|
||||
outputFlags.BoolP("debug", "d", false, "If true, debug output is enabled from the logger")
|
||||
ensure(viper.BindPFlag("debug", outputFlags.Lookup("debug")))
|
||||
|
||||
outputFlags.Bool("trace", false, "If true, trace output is enabled from the logger")
|
||||
ensure(viper.BindPFlag("trace", outputFlags.Lookup("trace")))
|
||||
|
||||
outputFlags.String("write-config", "", "Write the config of the current command as yaml to the specified path")
|
||||
ensure(viper.BindPFlag("writeConfig", outputFlags.Lookup("write-config")))
|
||||
|
||||
rootCmd.PersistentFlags().AddFlagSet(outputFlags)
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Find home directory.
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
viper.AddConfigPath(home)
|
||||
viper.SetConfigName(".mill")
|
||||
}
|
||||
|
||||
// read in environment variables that match
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
|
||||
func ensure(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkRequiredFlags(flags *pflag.FlagSet) error {
|
||||
requiredError := false
|
||||
flagName := ""
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
requiredAnnotation := flag.Annotations[cobra.BashCompOneRequiredFlag]
|
||||
if len(requiredAnnotation) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
flagRequired := requiredAnnotation[0] == "true"
|
||||
|
||||
if flagRequired && !flag.Changed {
|
||||
requiredError = true
|
||||
flagName = flag.Name
|
||||
}
|
||||
})
|
||||
|
||||
if requiredError {
|
||||
return errors.New("Required flag `" + flagName + "` has not been set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
85
tools/mill/go.mod
Normal file
85
tools/mill/go.mod
Normal file
@@ -0,0 +1,85 @@
|
||||
module github.com/ThreeDotsLabs/watermill/tools/mill
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/pubsub v1.50.2
|
||||
github.com/IBM/sarama v1.48.1
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
github.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2
|
||||
github.com/ThreeDotsLabs/watermill-googlecloud v1.2.6
|
||||
github.com/ThreeDotsLabs/watermill-io v1.1.2
|
||||
github.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.2
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
google.golang.org/api v0.279.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.123.0 // indirect
|
||||
cloud.google.com/go/auth v0.20.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.11.0 // indirect
|
||||
cloud.google.com/go/pubsub/v2 v2.6.0 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect
|
||||
github.com/eapache/go-resiliency v1.7.0 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // 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.18.6 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.11.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
|
||||
google.golang.org/grpc v1.81.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
320
tools/mill/go.sum
Normal file
320
tools/mill/go.sum
Normal file
@@ -0,0 +1,320 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
|
||||
cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.11.0 h1:KieQ9Pb+LLPak1O3Rv3GgCxhnmkYf7Xyh0P5HfF1jFM=
|
||||
cloud.google.com/go/iam v1.11.0/go.mod h1:KP+nKGugNJW4LcLx1uEZcq1ok5sQHFaQehQNl4QDgV4=
|
||||
cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
|
||||
cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
|
||||
cloud.google.com/go/longrunning v0.13.0 h1:dUfqF8y0bHOeZzF5+tKPZ6RBCeEEDOejvwGwENv/eEc=
|
||||
cloud.google.com/go/longrunning v0.13.0/go.mod h1:8nqFBPOO1U/XkhWl0I19AMZEphrHi73VNABIpKYaTwM=
|
||||
cloud.google.com/go/pubsub v1.50.2 h1:54Up97HnThdP4H8jjWJSSQ/mnYG2EKon7ZSNETRq0tM=
|
||||
cloud.google.com/go/pubsub v1.50.2/go.mod h1:jyCWeZdGFqd4mitSsBERnJcpqaHBsxQoPkNvjj4sp0w=
|
||||
cloud.google.com/go/pubsub/v2 v2.6.0 h1:8pjR0id+GTB+krKx5G6AGJoYrHog58w2Q89PCOrfM64=
|
||||
cloud.google.com/go/pubsub/v2 v2.6.0/go.mod h1:4anqvV/w8Pcgu2tO0qr2XgsF3GXHowzryfQ5gOnVmWY=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/IBM/sarama v1.48.1 h1:x1dSWebprjjE7Wr7n8RVAxwa4mt4O9JejRxnZrGIXk0=
|
||||
github.com/IBM/sarama v1.48.1/go.mod h1:m/Q1aFezH82/AglfTpJbw/fO0ZybYXhPgTmvajiZX50=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
|
||||
github.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2 h1:aeyFSR4SUsbszmocuFiYY13nsHorc6CXIS2Hy7+xgFU=
|
||||
github.com/ThreeDotsLabs/watermill-amqp/v3 v3.0.2/go.mod h1:+8tCh6VCuBcQWhfETCwzRINKQ1uyeg9moH3h7jMKxQk=
|
||||
github.com/ThreeDotsLabs/watermill-googlecloud v1.2.6 h1:Ll1NzWoiEXr3mtQ6APuVfMBoRNeGeLLvPcclYrpPTCA=
|
||||
github.com/ThreeDotsLabs/watermill-googlecloud v1.2.6/go.mod h1:74wkEkvh9NawpHArWQ7OhHnuldkFj6+J//ZMi0Fgw58=
|
||||
github.com/ThreeDotsLabs/watermill-io v1.1.2 h1:t3wismmE++6HbB+fDnSdvLtSKs0yYaSXRtLmyUcRyTk=
|
||||
github.com/ThreeDotsLabs/watermill-io v1.1.2/go.mod h1:DF6rhoPWBOeWRW/1wWjNfLkke8rZsB5BUzBox/L6fRI=
|
||||
github.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.2 h1:lLmrzZnl8o8U5uLVhMLSFHGSuWLcsqhW1MOtltx2CbQ=
|
||||
github.com/ThreeDotsLabs/watermill-kafka/v3 v3.1.2/go.mod h1:o1GcoF/1CSJ9JSmQzUkULvpZeO635pZe+WWrYNFlJNk=
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
|
||||
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84=
|
||||
github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc=
|
||||
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
|
||||
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
|
||||
github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
|
||||
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/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
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-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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.einride.tech/aip v0.83.0 h1:TI21IdeOnLTwZEJ3BxtImIZk6bsN2Q+sd0x99SLiQ+M=
|
||||
go.einride.tech/aip v0.83.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
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=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74=
|
||||
google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60 h1:rhBdfmsOlOZIvz3Y5/BdUzPg2CkO8L7QQPKj96B8554=
|
||||
google.golang.org/genproto v0.0.0-20260511170946-3700d4141b60/go.mod h1:8xo2Pj1b20ZOCpzlU3B9qieMwVIAXx1QVZWLMlPL6sM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 h1:3WsB1FAbiRIf2tOxscWKs3pQBD9he1NsrnbhMuWfekc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60/go.mod h1:7yoXV7RIh5gblj/xVYoogxAWvA9wUeVbpsK/M694l00=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
|
||||
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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/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=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
9
tools/mill/main.go
Normal file
9
tools/mill/main.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import "github.com/ThreeDotsLabs/watermill/tools/mill/cmd"
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
|
||||
// TODO: alternative input/output modes: json, gob, protobuf... (?)
|
||||
38
tools/pq/README.md
Normal file
38
tools/pq/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# pq
|
||||
|
||||
pq is a CLI tool for working with delayed messages on poison queues.
|
||||
|
||||
For now, it supports the PostgreSQL Pub/Sub implementation.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
go install github.com/ThreeDotsLabs/watermill/tools/pq@latest
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Set the `DATABASE_URL` environment variable to your PostgreSQL connection string.
|
||||
|
||||
For example, to connect to the database used for the [delayed requeue example](../../_examples/real-world-examples/delayed-requeue):
|
||||
|
||||
```bash
|
||||
export DATABASE_URL="postgres://watermill:password@postgres:5432/watermill?sslmode=disable"
|
||||
```
|
||||
|
||||
```bash
|
||||
pq -backend postgres -topic requeue
|
||||
```
|
||||
|
||||
This will use the default `watermill_` prefix, so will use the `watermill_requeue` table.
|
||||
|
||||
If you use a custom prefix, use the `-raw-topic` flag instead:
|
||||
|
||||
```bash
|
||||
pq -backend postgres -raw-topic my_prefix_requeue
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
- Requeue — Updates the `_watermill_delayed_until` metadata to the current time, so the message will be instantly requeued.
|
||||
- Ack — Removes the message from the queue (be careful — you will lose the message forever).
|
||||
99
tools/pq/backend/postgres.go
Normal file
99
tools/pq/backend/postgres.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/components/delay"
|
||||
"github.com/ThreeDotsLabs/watermill/tools/pq/cli"
|
||||
)
|
||||
|
||||
type PostgresMessage struct {
|
||||
Offset int `db:"offset"`
|
||||
UUID string `db:"uuid"`
|
||||
Payload string `db:"payload"`
|
||||
Metadata string `db:"metadata"`
|
||||
}
|
||||
|
||||
type PostgresBackend struct {
|
||||
db *sqlx.DB
|
||||
config cli.BackendConfig
|
||||
}
|
||||
|
||||
func NewPostgresBackend(ctx context.Context, config cli.BackendConfig) (*PostgresBackend, error) {
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
if dbURL == "" {
|
||||
return nil, fmt.Errorf("missing DATABASE_URL")
|
||||
}
|
||||
|
||||
db, err := sqlx.Connect("postgres", dbURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PostgresBackend{
|
||||
db: db,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *PostgresBackend) AllMessages(ctx context.Context) ([]cli.Message, error) {
|
||||
var dbMessages []PostgresMessage
|
||||
err := r.db.SelectContext(ctx, &dbMessages, fmt.Sprintf(`SELECT "offset", uuid, payload, metadata FROM %v WHERE acked = false ORDER BY "offset"`, r.topic()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var messages []cli.Message
|
||||
|
||||
for _, dbMsg := range dbMessages {
|
||||
var metadata map[string]string
|
||||
err := json.Unmarshal([]byte(dbMsg.Metadata), &metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg, err := cli.NewMessage(fmt.Sprint(dbMsg.Offset), dbMsg.UUID, dbMsg.Payload, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messages = append(messages, msg)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (r *PostgresBackend) Requeue(ctx context.Context, msg cli.Message) error {
|
||||
_, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET metadata = metadata::jsonb || jsonb_build_object($1::text, $2::text) WHERE "offset" = $3`, r.topic()),
|
||||
delay.DelayedUntilKey, time.Now().UTC().Format(time.RFC3339), msg.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgresBackend) Ack(ctx context.Context, msg cli.Message) error {
|
||||
_, err := r.db.ExecContext(ctx, fmt.Sprintf(`UPDATE %v SET acked = true WHERE "offset" = %v`, r.topic(), msg.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PostgresBackend) topic() string {
|
||||
if r.config.Topic != "" {
|
||||
return fmt.Sprintf(`"watermill_%v"`, r.config.Topic)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`"%v"`, r.config.RawTopic)
|
||||
}
|
||||
32
tools/pq/cli/backend.go
Normal file
32
tools/pq/cli/backend.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BackendConfig struct {
|
||||
Topic string
|
||||
RawTopic string
|
||||
}
|
||||
|
||||
func (c BackendConfig) Validate() error {
|
||||
if c.Topic == "" && c.RawTopic == "" {
|
||||
return errors.New("topic or raw topic must be provided")
|
||||
}
|
||||
|
||||
if c.Topic != "" && c.RawTopic != "" {
|
||||
return errors.New("only one of topic or raw topic must be provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BackendConstructor func(ctx context.Context, cfg BackendConfig) (Backend, error)
|
||||
|
||||
type Backend interface {
|
||||
AllMessages(ctx context.Context) ([]Message, error)
|
||||
Requeue(ctx context.Context, msg Message) error
|
||||
Ack(ctx context.Context, msg Message) error
|
||||
}
|
||||
45
tools/pq/cli/message.go
Normal file
45
tools/pq/cli/message.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/components/delay"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
// ID is a unique message ID across the Pub/Sub's topic.
|
||||
ID string
|
||||
UUID string
|
||||
Payload string
|
||||
Metadata map[string]string
|
||||
|
||||
OriginalTopic string
|
||||
DelayedUntil string
|
||||
DelayedFor string
|
||||
RequeueIn time.Duration
|
||||
}
|
||||
|
||||
func NewMessage(id string, uuid string, payload string, metadata map[string]string) (Message, error) {
|
||||
originalTopic := metadata[middleware.PoisonedTopicKey]
|
||||
|
||||
// Calculate the time until the message should be requeued
|
||||
delayedUntil, err := time.Parse(time.RFC3339, metadata[delay.DelayedUntilKey])
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
delayedFor := metadata[delay.DelayedForKey]
|
||||
requeueIn := delayedUntil.Sub(time.Now().UTC()).Round(time.Second)
|
||||
|
||||
return Message{
|
||||
ID: id,
|
||||
UUID: uuid,
|
||||
Payload: payload,
|
||||
Metadata: metadata,
|
||||
OriginalTopic: originalTopic,
|
||||
DelayedUntil: delayedUntil.String(),
|
||||
DelayedFor: delayedFor,
|
||||
RequeueIn: requeueIn,
|
||||
}, nil
|
||||
}
|
||||
403
tools/pq/cli/model.go
Normal file
403
tools/pq/cli/model.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
var warningStyle = lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("196")).
|
||||
Align(lipgloss.Center).
|
||||
Padding(1, 10)
|
||||
|
||||
var dialogStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("241")).
|
||||
Padding(1, 4)
|
||||
|
||||
var buttonStyle = lipgloss.NewStyle()
|
||||
|
||||
var buttonSelectedStyle = lipgloss.NewStyle().
|
||||
Background(lipgloss.Color("57"))
|
||||
|
||||
var readOnlyMessageActions = []string{"<- Back", "Show payload"}
|
||||
var writeMessageActions = []string{"Requeue", "Ack (drop)"}
|
||||
|
||||
var dialogActions = []string{"Cancel", "Confirm"}
|
||||
|
||||
type MessagesUpdated struct {
|
||||
Messages []Message
|
||||
}
|
||||
|
||||
type DialogResult struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (m Model) FetchMessages() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
for {
|
||||
msgs, err := m.backend.AllMessages(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
m.sub <- MessagesUpdated{
|
||||
Messages: msgs,
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) WaitForMessages() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.sub
|
||||
}
|
||||
}
|
||||
|
||||
var baseStyle = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240"))
|
||||
|
||||
type Model struct {
|
||||
backend Backend
|
||||
sub chan MessagesUpdated
|
||||
|
||||
chosenMessage *Message
|
||||
chosenMessageGone bool
|
||||
|
||||
table table.Model
|
||||
messages []Message
|
||||
|
||||
chosenAction int
|
||||
currentDialog *Dialog
|
||||
|
||||
showingPayload bool
|
||||
payloadViewport viewport.Model
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
m.FetchMessages(),
|
||||
m.WaitForMessages(),
|
||||
)
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case MessagesUpdated:
|
||||
rows := make([]table.Row, len(msg.Messages))
|
||||
for i, message := range msg.Messages {
|
||||
rows[i] = table.Row{
|
||||
message.ID,
|
||||
message.UUID,
|
||||
message.OriginalTopic,
|
||||
message.DelayedFor,
|
||||
message.RequeueIn.String(),
|
||||
}
|
||||
}
|
||||
m.table.SetRows(rows)
|
||||
m.messages = msg.Messages
|
||||
|
||||
// If the chosen message is no longer in the list, go back to the table.
|
||||
// This is to avoid accidentally making an action on a message that has been requeued or deleted.
|
||||
if m.chosenMessage != nil {
|
||||
found := false
|
||||
for _, message := range m.messages {
|
||||
if message.ID == m.chosenMessage.ID {
|
||||
foundMessage := message
|
||||
m.chosenMessage = &foundMessage
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
m.chosenMessageGone = false
|
||||
} else {
|
||||
if !m.chosenMessageGone {
|
||||
m.chosenAction = 0
|
||||
}
|
||||
|
||||
m.chosenMessageGone = true
|
||||
}
|
||||
}
|
||||
|
||||
return m, m.WaitForMessages()
|
||||
}
|
||||
|
||||
if m.chosenMessage == nil {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case " ", "enter":
|
||||
c := m.table.Cursor()
|
||||
m.chosenAction = 0
|
||||
chosenMessage := m.messages[c]
|
||||
m.chosenMessage = &chosenMessage
|
||||
m.chosenMessageGone = false
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.table, cmd = m.table.Update(msg)
|
||||
return m, cmd
|
||||
} else if m.showingPayload {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc", "backspace":
|
||||
m.showingPayload = false
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.payloadViewport, cmd = m.payloadViewport.Update(msg)
|
||||
return m, cmd
|
||||
} else if m.currentDialog != nil {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc", "backspace":
|
||||
m.currentDialog = nil
|
||||
case "h", "left":
|
||||
m.currentDialog.Choice--
|
||||
if m.currentDialog.Choice < 0 {
|
||||
m.currentDialog.Choice = 0
|
||||
}
|
||||
case "l", "right":
|
||||
m.currentDialog.Choice++
|
||||
if m.currentDialog.Choice >= len(dialogActions) {
|
||||
m.currentDialog.Choice = len(dialogActions) - 1
|
||||
}
|
||||
case " ", "enter":
|
||||
switch m.currentDialog.Choice {
|
||||
case 0:
|
||||
m.currentDialog = nil
|
||||
case 1:
|
||||
m.currentDialog.Running = true
|
||||
return m, m.currentDialog.Action
|
||||
}
|
||||
}
|
||||
case DialogResult:
|
||||
if msg.Err != nil {
|
||||
// TODO Could be handled better
|
||||
panic(msg.Err)
|
||||
}
|
||||
|
||||
m.currentDialog = nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
} else {
|
||||
messageActions := len(readOnlyMessageActions)
|
||||
if !m.chosenMessageGone {
|
||||
messageActions += len(writeMessageActions)
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc", "backspace":
|
||||
m.chosenMessage = nil
|
||||
m.chosenMessageGone = false
|
||||
case "j", "down":
|
||||
m.chosenAction++
|
||||
if m.chosenAction >= messageActions {
|
||||
m.chosenAction = messageActions - 1
|
||||
}
|
||||
case "k", "up":
|
||||
m.chosenAction--
|
||||
if m.chosenAction < 0 {
|
||||
m.chosenAction = 0
|
||||
}
|
||||
case " ", "enter":
|
||||
switch m.chosenAction {
|
||||
case 0:
|
||||
m.chosenMessage = nil
|
||||
m.chosenMessageGone = false
|
||||
case 1:
|
||||
// Show payload
|
||||
m.showingPayload = true
|
||||
m.payloadViewport = viewport.New(80, 20)
|
||||
b := lipgloss.RoundedBorder()
|
||||
m.payloadViewport.Style = lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
|
||||
|
||||
payload := m.chosenMessage.Payload
|
||||
|
||||
var jsonPayload any
|
||||
err := json.Unmarshal([]byte(payload), &jsonPayload)
|
||||
if err == nil {
|
||||
pretty, err := json.MarshalIndent(jsonPayload, "", " ")
|
||||
if err == nil {
|
||||
payload = string(pretty)
|
||||
}
|
||||
}
|
||||
|
||||
m.payloadViewport.SetContent(payload)
|
||||
case 2:
|
||||
chosenMessage := *m.chosenMessage
|
||||
m.currentDialog = &Dialog{
|
||||
Prompt: "Requeue message? It will go back to the original topic.",
|
||||
Action: func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
return DialogResult{
|
||||
Err: m.backend.Requeue(ctx, chosenMessage),
|
||||
}
|
||||
},
|
||||
}
|
||||
case 3:
|
||||
chosenMessage := *m.chosenMessage
|
||||
m.currentDialog = &Dialog{
|
||||
Prompt: "Acknowledge message? It will be dropped from the topic.",
|
||||
Action: func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
return DialogResult{
|
||||
Err: m.backend.Ack(ctx, chosenMessage),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.chosenMessage == nil {
|
||||
return baseStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n"
|
||||
}
|
||||
|
||||
msg := m.chosenMessage
|
||||
|
||||
var out string
|
||||
|
||||
if m.chosenMessageGone {
|
||||
out += warningStyle.Render("Read only — the message is gone.")
|
||||
out += "\n"
|
||||
}
|
||||
|
||||
out += fmt.Sprintf(
|
||||
"ID: %v\nUUID: %v\nOriginal Topic: %v\nDelayed For: %v\nDelayed Until: %v\nRequeue In: %v\n\n",
|
||||
msg.ID,
|
||||
msg.UUID,
|
||||
msg.OriginalTopic,
|
||||
msg.DelayedFor,
|
||||
msg.DelayedUntil,
|
||||
msg.RequeueIn,
|
||||
)
|
||||
|
||||
if m.showingPayload {
|
||||
out += m.payloadViewport.View()
|
||||
return out
|
||||
}
|
||||
|
||||
out += "Metadata:\n"
|
||||
|
||||
keys := maps.Keys(msg.Metadata)
|
||||
slices.Sort(keys)
|
||||
for _, k := range keys {
|
||||
v := msg.Metadata[k]
|
||||
out += fmt.Sprintf(" %v: %v\n", k, v)
|
||||
}
|
||||
|
||||
if m.currentDialog != nil {
|
||||
prompt := m.currentDialog.Prompt + "\n\n"
|
||||
|
||||
if m.currentDialog.Running {
|
||||
prompt += "Running..."
|
||||
} else {
|
||||
for i, action := range dialogActions {
|
||||
style := buttonStyle
|
||||
if i == m.currentDialog.Choice {
|
||||
style = buttonSelectedStyle
|
||||
}
|
||||
|
||||
prompt += fmt.Sprintf("%v", style.MarginLeft(13).Render(action))
|
||||
}
|
||||
}
|
||||
|
||||
out += dialogStyle.Render(prompt)
|
||||
} else {
|
||||
out += "\nActions:\n"
|
||||
|
||||
messageActions := readOnlyMessageActions
|
||||
if !m.chosenMessageGone {
|
||||
messageActions = append(messageActions, writeMessageActions...)
|
||||
}
|
||||
|
||||
for i, action := range messageActions {
|
||||
style := buttonStyle
|
||||
if i == m.chosenAction {
|
||||
style = buttonSelectedStyle
|
||||
}
|
||||
|
||||
out += fmt.Sprintf("%v\n", style.MarginLeft(2).Render(action))
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func NewModel(backend Backend) Model {
|
||||
columns := []table.Column{
|
||||
{Title: "ID", Width: 8},
|
||||
{Title: "UUID", Width: 40},
|
||||
{Title: "Original Topic", Width: 20},
|
||||
{Title: "Delayed For", Width: 14},
|
||||
{Title: "Requeue In", Width: 14},
|
||||
}
|
||||
|
||||
t := table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithFocused(true),
|
||||
table.WithHeight(20),
|
||||
)
|
||||
|
||||
s := table.DefaultStyles()
|
||||
s.Header = s.Header.
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
BorderBottom(true).
|
||||
Bold(false)
|
||||
s.Selected = s.Selected.
|
||||
Foreground(lipgloss.Color("229")).
|
||||
Background(lipgloss.Color("57")).
|
||||
Bold(false)
|
||||
t.SetStyles(s)
|
||||
|
||||
return Model{
|
||||
backend: backend,
|
||||
sub: make(chan MessagesUpdated),
|
||||
table: t,
|
||||
}
|
||||
}
|
||||
|
||||
type Dialog struct {
|
||||
Prompt string
|
||||
Action func() tea.Msg
|
||||
Choice int
|
||||
Running bool
|
||||
}
|
||||
41
tools/pq/go.mod
Normal file
41
tools/pq/go.mod
Normal file
@@ -0,0 +1,41 @@
|
||||
module github.com/ThreeDotsLabs/watermill/tools/pq
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
)
|
||||
88
tools/pq/go.sum
Normal file
88
tools/pq/go.sum
Normal file
@@ -0,0 +1,88 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
|
||||
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
51
tools/pq/main.go
Normal file
51
tools/pq/main.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/tools/pq/backend"
|
||||
"github.com/ThreeDotsLabs/watermill/tools/pq/cli"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
var (
|
||||
backendFlag = flag.String("backend", "", "backend to use")
|
||||
topicFlag = flag.String("topic", "", "topic to use")
|
||||
rawTopicFlag = flag.String("raw-topic", "", "raw topic to use")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
config := cli.BackendConfig{
|
||||
Topic: *topicFlag,
|
||||
RawTopic: *rawTopicFlag,
|
||||
}
|
||||
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var b cli.Backend
|
||||
switch *backendFlag {
|
||||
case "postgres":
|
||||
b, err = backend.NewPostgresBackend(context.Background(), config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
default:
|
||||
log.Fatalf("unknown backend: %s", *backendFlag)
|
||||
}
|
||||
|
||||
m := cli.NewModel(b)
|
||||
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
_, err = p.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user