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 }