github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/mqtt.go (about)

     1  // Copyright (c) 2015-2023 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package target
    19  
    20  import (
    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  	"github.com/minio/minio/internal/event"
    33  	"github.com/minio/minio/internal/logger"
    34  	"github.com/minio/minio/internal/once"
    35  	"github.com/minio/minio/internal/store"
    36  	xnet "github.com/minio/pkg/v2/net"
    37  )
    38  
    39  const (
    40  	reconnectInterval = 5 * time.Second
    41  	storePrefix       = "minio"
    42  )
    43  
    44  // MQTT input constants
    45  const (
    46  	MqttBroker            = "broker"
    47  	MqttTopic             = "topic"
    48  	MqttQoS               = "qos"
    49  	MqttUsername          = "username"
    50  	MqttPassword          = "password"
    51  	MqttReconnectInterval = "reconnect_interval"
    52  	MqttKeepAliveInterval = "keep_alive_interval"
    53  	MqttQueueDir          = "queue_dir"
    54  	MqttQueueLimit        = "queue_limit"
    55  
    56  	EnvMQTTEnable            = "MINIO_NOTIFY_MQTT_ENABLE"
    57  	EnvMQTTBroker            = "MINIO_NOTIFY_MQTT_BROKER"
    58  	EnvMQTTTopic             = "MINIO_NOTIFY_MQTT_TOPIC"
    59  	EnvMQTTQoS               = "MINIO_NOTIFY_MQTT_QOS"
    60  	EnvMQTTUsername          = "MINIO_NOTIFY_MQTT_USERNAME"
    61  	EnvMQTTPassword          = "MINIO_NOTIFY_MQTT_PASSWORD"
    62  	EnvMQTTReconnectInterval = "MINIO_NOTIFY_MQTT_RECONNECT_INTERVAL"
    63  	EnvMQTTKeepAliveInterval = "MINIO_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL"
    64  	EnvMQTTQueueDir          = "MINIO_NOTIFY_MQTT_QUEUE_DIR"
    65  	EnvMQTTQueueLimit        = "MINIO_NOTIFY_MQTT_QUEUE_LIMIT"
    66  )
    67  
    68  // MQTTArgs - MQTT target arguments.
    69  type MQTTArgs struct {
    70  	Enable               bool           `json:"enable"`
    71  	Broker               xnet.URL       `json:"broker"`
    72  	Topic                string         `json:"topic"`
    73  	QoS                  byte           `json:"qos"`
    74  	User                 string         `json:"username"`
    75  	Password             string         `json:"password"`
    76  	MaxReconnectInterval time.Duration  `json:"reconnectInterval"`
    77  	KeepAlive            time.Duration  `json:"keepAliveInterval"`
    78  	RootCAs              *x509.CertPool `json:"-"`
    79  	QueueDir             string         `json:"queueDir"`
    80  	QueueLimit           uint64         `json:"queueLimit"`
    81  }
    82  
    83  // Validate MQTTArgs fields
    84  func (m MQTTArgs) Validate() error {
    85  	if !m.Enable {
    86  		return nil
    87  	}
    88  	u, err := xnet.ParseURL(m.Broker.String())
    89  	if err != nil {
    90  		return err
    91  	}
    92  	switch u.Scheme {
    93  	case "ws", "wss", "tcp", "ssl", "tls", "tcps":
    94  	default:
    95  		return errors.New("unknown protocol in broker address")
    96  	}
    97  	if m.QueueDir != "" {
    98  		if !filepath.IsAbs(m.QueueDir) {
    99  			return errors.New("queueDir path should be absolute")
   100  		}
   101  		if m.QoS == 0 {
   102  			return errors.New("qos should be set to 1 or 2 if queueDir is set")
   103  		}
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // MQTTTarget - MQTT target.
   110  type MQTTTarget struct {
   111  	initOnce once.Init
   112  
   113  	id         event.TargetID
   114  	args       MQTTArgs
   115  	client     mqtt.Client
   116  	store      store.Store[event.Event]
   117  	quitCh     chan struct{}
   118  	loggerOnce logger.LogOnce
   119  }
   120  
   121  // ID - returns target ID.
   122  func (target *MQTTTarget) ID() event.TargetID {
   123  	return target.id
   124  }
   125  
   126  // Name - returns the Name of the target.
   127  func (target *MQTTTarget) Name() string {
   128  	return target.ID().String()
   129  }
   130  
   131  // Store returns any underlying store if set.
   132  func (target *MQTTTarget) Store() event.TargetStore {
   133  	return target.store
   134  }
   135  
   136  // IsActive - Return true if target is up and active
   137  func (target *MQTTTarget) IsActive() (bool, error) {
   138  	if err := target.init(); err != nil {
   139  		return false, err
   140  	}
   141  	return target.isActive()
   142  }
   143  
   144  func (target *MQTTTarget) isActive() (bool, error) {
   145  	if !target.client.IsConnectionOpen() {
   146  		return false, store.ErrNotConnected
   147  	}
   148  	return true, nil
   149  }
   150  
   151  // send - sends an event to the mqtt.
   152  func (target *MQTTTarget) send(eventData event.Event) error {
   153  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	key := eventData.S3.Bucket.Name + "/" + objectName
   158  
   159  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	token := target.client.Publish(target.args.Topic, target.args.QoS, false, string(data))
   165  	if !token.WaitTimeout(reconnectInterval) {
   166  		return store.ErrNotConnected
   167  	}
   168  	return token.Error()
   169  }
   170  
   171  // SendFromStore - reads an event from store and sends it to MQTT.
   172  func (target *MQTTTarget) SendFromStore(key store.Key) error {
   173  	if err := target.init(); err != nil {
   174  		return err
   175  	}
   176  
   177  	// Do not send if the connection is not active.
   178  	_, err := target.isActive()
   179  	if err != nil {
   180  		return err
   181  	}
   182  
   183  	eventData, err := target.store.Get(key.Name)
   184  	if err != nil {
   185  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   186  		// Such events will not exist and wouldve been already been sent successfully.
   187  		if os.IsNotExist(err) {
   188  			return nil
   189  		}
   190  		return err
   191  	}
   192  
   193  	if err = target.send(eventData); err != nil {
   194  		return err
   195  	}
   196  
   197  	// Delete the event from store.
   198  	return target.store.Del(key.Name)
   199  }
   200  
   201  // Save - saves the events to the store if queuestore is configured, which will
   202  // be replayed when the mqtt connection is active.
   203  func (target *MQTTTarget) Save(eventData event.Event) error {
   204  	if target.store != nil {
   205  		return target.store.Put(eventData)
   206  	}
   207  	if err := target.init(); err != nil {
   208  		return err
   209  	}
   210  
   211  	// Do not send if the connection is not active.
   212  	_, err := target.isActive()
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	return target.send(eventData)
   218  }
   219  
   220  // Close - does nothing and available for interface compatibility.
   221  func (target *MQTTTarget) Close() error {
   222  	if target.client != nil {
   223  		target.client.Disconnect(100)
   224  	}
   225  	close(target.quitCh)
   226  	return nil
   227  }
   228  
   229  func (target *MQTTTarget) init() error {
   230  	return target.initOnce.Do(target.initMQTT)
   231  }
   232  
   233  func (target *MQTTTarget) initMQTT() error {
   234  	args := target.args
   235  
   236  	// Using hex here, to make sure we avoid 23
   237  	// character limit on client_id according to
   238  	// MQTT spec.
   239  	clientID := fmt.Sprintf("%x", time.Now().UnixNano())
   240  
   241  	options := mqtt.NewClientOptions().
   242  		SetClientID(clientID).
   243  		SetCleanSession(true).
   244  		SetUsername(args.User).
   245  		SetPassword(args.Password).
   246  		SetMaxReconnectInterval(args.MaxReconnectInterval).
   247  		SetKeepAlive(args.KeepAlive).
   248  		SetTLSConfig(&tls.Config{RootCAs: args.RootCAs}).
   249  		AddBroker(args.Broker.String())
   250  
   251  	target.client = mqtt.NewClient(options)
   252  
   253  	token := target.client.Connect()
   254  	ok := token.WaitTimeout(reconnectInterval)
   255  	if !ok {
   256  		return store.ErrNotConnected
   257  	}
   258  	if token.Error() != nil {
   259  		return token.Error()
   260  	}
   261  
   262  	yes, err := target.isActive()
   263  	if err != nil {
   264  		return err
   265  	}
   266  	if !yes {
   267  		return store.ErrNotConnected
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  // NewMQTTTarget - creates new MQTT target.
   274  func NewMQTTTarget(id string, args MQTTArgs, loggerOnce logger.LogOnce) (*MQTTTarget, error) {
   275  	if args.MaxReconnectInterval == 0 {
   276  		// Default interval
   277  		// https://github.com/eclipse/paho.mqtt.golang/blob/master/options.go#L115
   278  		args.MaxReconnectInterval = 10 * time.Minute
   279  	}
   280  
   281  	if args.KeepAlive == 0 {
   282  		args.KeepAlive = 10 * time.Second
   283  	}
   284  
   285  	var queueStore store.Store[event.Event]
   286  	if args.QueueDir != "" {
   287  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-mqtt-"+id)
   288  		queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension)
   289  		if err := queueStore.Open(); err != nil {
   290  			return nil, fmt.Errorf("unable to initialize the queue store of MQTT `%s`: %w", id, err)
   291  		}
   292  	}
   293  
   294  	target := &MQTTTarget{
   295  		id:         event.TargetID{ID: id, Name: "mqtt"},
   296  		args:       args,
   297  		store:      queueStore,
   298  		quitCh:     make(chan struct{}),
   299  		loggerOnce: loggerOnce,
   300  	}
   301  
   302  	if target.store != nil {
   303  		store.StreamItems(target.store, target, target.quitCh, target.loggerOnce)
   304  	}
   305  
   306  	return target, nil
   307  }