storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/event/target/mqtt.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2018-2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package target
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"time"
    30  
    31  	mqtt "github.com/eclipse/paho.mqtt.golang"
    32  
    33  	"storj.io/minio/pkg/event"
    34  	xnet "storj.io/minio/pkg/net"
    35  )
    36  
    37  const (
    38  	reconnectInterval = 5 // In Seconds
    39  	storePrefix       = "minio"
    40  )
    41  
    42  // MQTT input constants
    43  const (
    44  	MqttBroker            = "broker"
    45  	MqttTopic             = "topic"
    46  	MqttQoS               = "qos"
    47  	MqttUsername          = "username"
    48  	MqttPassword          = "password"
    49  	MqttReconnectInterval = "reconnect_interval"
    50  	MqttKeepAliveInterval = "keep_alive_interval"
    51  	MqttQueueDir          = "queue_dir"
    52  	MqttQueueLimit        = "queue_limit"
    53  
    54  	EnvMQTTEnable            = "MINIO_NOTIFY_MQTT_ENABLE"
    55  	EnvMQTTBroker            = "MINIO_NOTIFY_MQTT_BROKER"
    56  	EnvMQTTTopic             = "MINIO_NOTIFY_MQTT_TOPIC"
    57  	EnvMQTTQoS               = "MINIO_NOTIFY_MQTT_QOS"
    58  	EnvMQTTUsername          = "MINIO_NOTIFY_MQTT_USERNAME"
    59  	EnvMQTTPassword          = "MINIO_NOTIFY_MQTT_PASSWORD"
    60  	EnvMQTTReconnectInterval = "MINIO_NOTIFY_MQTT_RECONNECT_INTERVAL"
    61  	EnvMQTTKeepAliveInterval = "MINIO_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL"
    62  	EnvMQTTQueueDir          = "MINIO_NOTIFY_MQTT_QUEUE_DIR"
    63  	EnvMQTTQueueLimit        = "MINIO_NOTIFY_MQTT_QUEUE_LIMIT"
    64  )
    65  
    66  // MQTTArgs - MQTT target arguments.
    67  type MQTTArgs struct {
    68  	Enable               bool           `json:"enable"`
    69  	Broker               xnet.URL       `json:"broker"`
    70  	Topic                string         `json:"topic"`
    71  	QoS                  byte           `json:"qos"`
    72  	User                 string         `json:"username"`
    73  	Password             string         `json:"password"`
    74  	MaxReconnectInterval time.Duration  `json:"reconnectInterval"`
    75  	KeepAlive            time.Duration  `json:"keepAliveInterval"`
    76  	RootCAs              *x509.CertPool `json:"-"`
    77  	QueueDir             string         `json:"queueDir"`
    78  	QueueLimit           uint64         `json:"queueLimit"`
    79  }
    80  
    81  // Validate MQTTArgs fields
    82  func (m MQTTArgs) Validate() error {
    83  	if !m.Enable {
    84  		return nil
    85  	}
    86  	u, err := xnet.ParseURL(m.Broker.String())
    87  	if err != nil {
    88  		return err
    89  	}
    90  	switch u.Scheme {
    91  	case "ws", "wss", "tcp", "ssl", "tls", "tcps":
    92  	default:
    93  		return errors.New("unknown protocol in broker address")
    94  	}
    95  	if m.QueueDir != "" {
    96  		if !filepath.IsAbs(m.QueueDir) {
    97  			return errors.New("queueDir path should be absolute")
    98  		}
    99  		if m.QoS == 0 {
   100  			return errors.New("qos should be set to 1 or 2 if queueDir is set")
   101  		}
   102  	}
   103  
   104  	return nil
   105  }
   106  
   107  // MQTTTarget - MQTT target.
   108  type MQTTTarget struct {
   109  	id         event.TargetID
   110  	args       MQTTArgs
   111  	client     mqtt.Client
   112  	store      Store
   113  	quitCh     chan struct{}
   114  	loggerOnce func(ctx context.Context, err error, id interface{}, kind ...interface{})
   115  }
   116  
   117  // ID - returns target ID.
   118  func (target *MQTTTarget) ID() event.TargetID {
   119  	return target.id
   120  }
   121  
   122  // HasQueueStore - Checks if the queueStore has been configured for the target
   123  func (target *MQTTTarget) HasQueueStore() bool {
   124  	return target.store != nil
   125  }
   126  
   127  // IsActive - Return true if target is up and active
   128  func (target *MQTTTarget) IsActive() (bool, error) {
   129  	if !target.client.IsConnectionOpen() {
   130  		return false, errNotConnected
   131  	}
   132  	return true, nil
   133  }
   134  
   135  // send - sends an event to the mqtt.
   136  func (target *MQTTTarget) send(eventData event.Event) error {
   137  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	key := eventData.S3.Bucket.Name + "/" + objectName
   142  
   143  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	token := target.client.Publish(target.args.Topic, target.args.QoS, false, string(data))
   149  	if !token.WaitTimeout(reconnectInterval * time.Second) {
   150  		return errNotConnected
   151  	}
   152  	return token.Error()
   153  }
   154  
   155  // Send - reads an event from store and sends it to MQTT.
   156  func (target *MQTTTarget) Send(eventKey string) error {
   157  	// Do not send if the connection is not active.
   158  	_, err := target.IsActive()
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	eventData, err := target.store.Get(eventKey)
   164  	if err != nil {
   165  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   166  		// Such events will not exist and wouldve been already been sent successfully.
   167  		if os.IsNotExist(err) {
   168  			return nil
   169  		}
   170  		return err
   171  	}
   172  
   173  	if err = target.send(eventData); err != nil {
   174  		return err
   175  	}
   176  
   177  	// Delete the event from store.
   178  	return target.store.Del(eventKey)
   179  }
   180  
   181  // Save - saves the events to the store if queuestore is configured, which will
   182  // be replayed when the mqtt connection is active.
   183  func (target *MQTTTarget) Save(eventData event.Event) error {
   184  	if target.store != nil {
   185  		return target.store.Put(eventData)
   186  	}
   187  
   188  	// Do not send if the connection is not active.
   189  	_, err := target.IsActive()
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	return target.send(eventData)
   195  }
   196  
   197  // Close - does nothing and available for interface compatibility.
   198  func (target *MQTTTarget) Close() error {
   199  	target.client.Disconnect(100)
   200  	close(target.quitCh)
   201  	return nil
   202  }
   203  
   204  // NewMQTTTarget - creates new MQTT target.
   205  func NewMQTTTarget(id string, args MQTTArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, kind ...interface{}), test bool) (*MQTTTarget, error) {
   206  	if args.MaxReconnectInterval == 0 {
   207  		// Default interval
   208  		// https://github.com/eclipse/paho.mqtt.golang/blob/master/options.go#L115
   209  		args.MaxReconnectInterval = 10 * time.Minute
   210  	}
   211  
   212  	options := mqtt.NewClientOptions().
   213  		SetClientID("").
   214  		SetCleanSession(true).
   215  		SetUsername(args.User).
   216  		SetPassword(args.Password).
   217  		SetMaxReconnectInterval(args.MaxReconnectInterval).
   218  		SetKeepAlive(args.KeepAlive).
   219  		SetTLSConfig(&tls.Config{RootCAs: args.RootCAs}).
   220  		AddBroker(args.Broker.String())
   221  
   222  	client := mqtt.NewClient(options)
   223  
   224  	target := &MQTTTarget{
   225  		id:         event.TargetID{ID: id, Name: "mqtt"},
   226  		args:       args,
   227  		client:     client,
   228  		quitCh:     make(chan struct{}),
   229  		loggerOnce: loggerOnce,
   230  	}
   231  
   232  	token := client.Connect()
   233  	retryRegister := func() {
   234  		for {
   235  		retry:
   236  			select {
   237  			case <-doneCh:
   238  				return
   239  			case <-target.quitCh:
   240  				return
   241  			default:
   242  				ok := token.WaitTimeout(reconnectInterval * time.Second)
   243  				if ok && token.Error() != nil {
   244  					target.loggerOnce(context.Background(),
   245  						fmt.Errorf("Previous connect failed with %w attempting a reconnect",
   246  							token.Error()),
   247  						target.ID())
   248  					time.Sleep(reconnectInterval * time.Second)
   249  					token = client.Connect()
   250  					goto retry
   251  				}
   252  				if ok {
   253  					// Successfully connected.
   254  					return
   255  				}
   256  			}
   257  		}
   258  	}
   259  
   260  	if args.QueueDir != "" {
   261  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-mqtt-"+id)
   262  		target.store = NewQueueStore(queueDir, args.QueueLimit)
   263  		if err := target.store.Open(); err != nil {
   264  			target.loggerOnce(context.Background(), err, target.ID())
   265  			return target, err
   266  		}
   267  
   268  		if !test {
   269  			go retryRegister()
   270  			// Replays the events from the store.
   271  			eventKeyCh := replayEvents(target.store, doneCh, target.loggerOnce, target.ID())
   272  			// Start replaying events from the store.
   273  			go sendEvents(target, eventKeyCh, doneCh, target.loggerOnce)
   274  		}
   275  	} else {
   276  		if token.Wait() && token.Error() != nil {
   277  			return target, token.Error()
   278  		}
   279  	}
   280  	return target, nil
   281  }