github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/nats.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  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  
    31  	"github.com/google/uuid"
    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  	"github.com/nats-io/nats.go"
    38  	"github.com/nats-io/stan.go"
    39  )
    40  
    41  // NATS related constants
    42  const (
    43  	NATSAddress       = "address"
    44  	NATSSubject       = "subject"
    45  	NATSUsername      = "username"
    46  	NATSPassword      = "password"
    47  	NATSToken         = "token"
    48  	NATSTLS           = "tls"
    49  	NATSTLSSkipVerify = "tls_skip_verify"
    50  	NATSPingInterval  = "ping_interval"
    51  	NATSQueueDir      = "queue_dir"
    52  	NATSQueueLimit    = "queue_limit"
    53  	NATSCertAuthority = "cert_authority"
    54  	NATSClientCert    = "client_cert"
    55  	NATSClientKey     = "client_key"
    56  
    57  	// Streaming constants - deprecated
    58  	NATSStreaming                   = "streaming"
    59  	NATSStreamingClusterID          = "streaming_cluster_id"
    60  	NATSStreamingAsync              = "streaming_async"
    61  	NATSStreamingMaxPubAcksInFlight = "streaming_max_pub_acks_in_flight"
    62  
    63  	// JetStream constants
    64  	NATSJetStream = "jetstream"
    65  
    66  	EnvNATSEnable        = "MINIO_NOTIFY_NATS_ENABLE"
    67  	EnvNATSAddress       = "MINIO_NOTIFY_NATS_ADDRESS"
    68  	EnvNATSSubject       = "MINIO_NOTIFY_NATS_SUBJECT"
    69  	EnvNATSUsername      = "MINIO_NOTIFY_NATS_USERNAME"
    70  	NATSUserCredentials  = "MINIO_NOTIFY_NATS_USER_CREDENTIALS"
    71  	EnvNATSPassword      = "MINIO_NOTIFY_NATS_PASSWORD"
    72  	EnvNATSToken         = "MINIO_NOTIFY_NATS_TOKEN"
    73  	EnvNATSTLS           = "MINIO_NOTIFY_NATS_TLS"
    74  	EnvNATSTLSSkipVerify = "MINIO_NOTIFY_NATS_TLS_SKIP_VERIFY"
    75  	EnvNATSPingInterval  = "MINIO_NOTIFY_NATS_PING_INTERVAL"
    76  	EnvNATSQueueDir      = "MINIO_NOTIFY_NATS_QUEUE_DIR"
    77  	EnvNATSQueueLimit    = "MINIO_NOTIFY_NATS_QUEUE_LIMIT"
    78  	EnvNATSCertAuthority = "MINIO_NOTIFY_NATS_CERT_AUTHORITY"
    79  	EnvNATSClientCert    = "MINIO_NOTIFY_NATS_CLIENT_CERT"
    80  	EnvNATSClientKey     = "MINIO_NOTIFY_NATS_CLIENT_KEY"
    81  
    82  	// Streaming constants - deprecated
    83  	EnvNATSStreaming                   = "MINIO_NOTIFY_NATS_STREAMING"
    84  	EnvNATSStreamingClusterID          = "MINIO_NOTIFY_NATS_STREAMING_CLUSTER_ID"
    85  	EnvNATSStreamingAsync              = "MINIO_NOTIFY_NATS_STREAMING_ASYNC"
    86  	EnvNATSStreamingMaxPubAcksInFlight = "MINIO_NOTIFY_NATS_STREAMING_MAX_PUB_ACKS_IN_FLIGHT"
    87  
    88  	// Jetstream constants
    89  	EnvNATSJetStream = "MINIO_NOTIFY_NATS_JETSTREAM"
    90  )
    91  
    92  // NATSArgs - NATS target arguments.
    93  type NATSArgs struct {
    94  	Enable          bool      `json:"enable"`
    95  	Address         xnet.Host `json:"address"`
    96  	Subject         string    `json:"subject"`
    97  	Username        string    `json:"username"`
    98  	UserCredentials string    `json:"userCredentials"`
    99  	Password        string    `json:"password"`
   100  	Token           string    `json:"token"`
   101  	TLS             bool      `json:"tls"`
   102  	TLSSkipVerify   bool      `json:"tlsSkipVerify"`
   103  	Secure          bool      `json:"secure"`
   104  	CertAuthority   string    `json:"certAuthority"`
   105  	ClientCert      string    `json:"clientCert"`
   106  	ClientKey       string    `json:"clientKey"`
   107  	PingInterval    int64     `json:"pingInterval"`
   108  	QueueDir        string    `json:"queueDir"`
   109  	QueueLimit      uint64    `json:"queueLimit"`
   110  	JetStream       struct {
   111  		Enable bool `json:"enable"`
   112  	} `json:"jetStream"`
   113  	Streaming struct {
   114  		Enable             bool   `json:"enable"`
   115  		ClusterID          string `json:"clusterID"`
   116  		Async              bool   `json:"async"`
   117  		MaxPubAcksInflight int    `json:"maxPubAcksInflight"`
   118  	} `json:"streaming"`
   119  
   120  	RootCAs *x509.CertPool `json:"-"`
   121  }
   122  
   123  // Validate NATSArgs fields
   124  func (n NATSArgs) Validate() error {
   125  	if !n.Enable {
   126  		return nil
   127  	}
   128  
   129  	if n.Address.IsEmpty() {
   130  		return errors.New("empty address")
   131  	}
   132  
   133  	if n.Subject == "" {
   134  		return errors.New("empty subject")
   135  	}
   136  
   137  	if n.ClientCert != "" && n.ClientKey == "" || n.ClientCert == "" && n.ClientKey != "" {
   138  		return errors.New("cert and key must be specified as a pair")
   139  	}
   140  
   141  	if n.Username != "" && n.Password == "" || n.Username == "" && n.Password != "" {
   142  		return errors.New("username and password must be specified as a pair")
   143  	}
   144  
   145  	if n.Streaming.Enable {
   146  		if n.Streaming.ClusterID == "" {
   147  			return errors.New("empty cluster id")
   148  		}
   149  	}
   150  
   151  	if n.JetStream.Enable {
   152  		if n.Subject == "" {
   153  			return errors.New("empty subject")
   154  		}
   155  	}
   156  
   157  	if n.QueueDir != "" {
   158  		if !filepath.IsAbs(n.QueueDir) {
   159  			return errors.New("queueDir path should be absolute")
   160  		}
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  // To obtain a nats connection from args.
   167  func (n NATSArgs) connectNats() (*nats.Conn, error) {
   168  	connOpts := []nats.Option{nats.Name("Minio Notification"), nats.MaxReconnects(-1)}
   169  	if n.Username != "" && n.Password != "" {
   170  		connOpts = append(connOpts, nats.UserInfo(n.Username, n.Password))
   171  	}
   172  	if n.UserCredentials != "" {
   173  		connOpts = append(connOpts, nats.UserCredentials(n.UserCredentials))
   174  	}
   175  	if n.Token != "" {
   176  		connOpts = append(connOpts, nats.Token(n.Token))
   177  	}
   178  	if n.Secure || n.TLS && n.TLSSkipVerify {
   179  		connOpts = append(connOpts, nats.Secure(nil))
   180  	} else if n.TLS {
   181  		connOpts = append(connOpts, nats.Secure(&tls.Config{RootCAs: n.RootCAs}))
   182  	}
   183  	if n.CertAuthority != "" {
   184  		connOpts = append(connOpts, nats.RootCAs(n.CertAuthority))
   185  	}
   186  	if n.ClientCert != "" && n.ClientKey != "" {
   187  		connOpts = append(connOpts, nats.ClientCert(n.ClientCert, n.ClientKey))
   188  	}
   189  	return nats.Connect(n.Address.String(), connOpts...)
   190  }
   191  
   192  // To obtain a streaming connection from args.
   193  func (n NATSArgs) connectStan() (stan.Conn, error) {
   194  	scheme := "nats"
   195  	if n.Secure {
   196  		scheme = "tls"
   197  	}
   198  
   199  	var addressURL string
   200  	//nolint:gocritic
   201  	if n.Username != "" && n.Password != "" {
   202  		addressURL = scheme + "://" + n.Username + ":" + n.Password + "@" + n.Address.String()
   203  	} else if n.Token != "" {
   204  		addressURL = scheme + "://" + n.Token + "@" + n.Address.String()
   205  	} else {
   206  		addressURL = scheme + "://" + n.Address.String()
   207  	}
   208  
   209  	u, err := uuid.NewRandom()
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	clientID := u.String()
   214  
   215  	connOpts := []stan.Option{stan.NatsURL(addressURL)}
   216  	if n.Streaming.MaxPubAcksInflight > 0 {
   217  		connOpts = append(connOpts, stan.MaxPubAcksInflight(n.Streaming.MaxPubAcksInflight))
   218  	}
   219  	if n.UserCredentials != "" {
   220  		connOpts = append(connOpts, stan.NatsOptions(nats.UserCredentials(n.UserCredentials)))
   221  	}
   222  
   223  	return stan.Connect(n.Streaming.ClusterID, clientID, connOpts...)
   224  }
   225  
   226  // NATSTarget - NATS target.
   227  type NATSTarget struct {
   228  	initOnce once.Init
   229  
   230  	id         event.TargetID
   231  	args       NATSArgs
   232  	natsConn   *nats.Conn
   233  	stanConn   stan.Conn
   234  	jstream    nats.JetStream
   235  	store      store.Store[event.Event]
   236  	loggerOnce logger.LogOnce
   237  	quitCh     chan struct{}
   238  }
   239  
   240  // ID - returns target ID.
   241  func (target *NATSTarget) ID() event.TargetID {
   242  	return target.id
   243  }
   244  
   245  // Name - returns the Name of the target.
   246  func (target *NATSTarget) Name() string {
   247  	return target.ID().String()
   248  }
   249  
   250  // Store returns any underlying store if set.
   251  func (target *NATSTarget) Store() event.TargetStore {
   252  	return target.store
   253  }
   254  
   255  // IsActive - Return true if target is up and active
   256  func (target *NATSTarget) IsActive() (bool, error) {
   257  	if err := target.init(); err != nil {
   258  		return false, err
   259  	}
   260  	return target.isActive()
   261  }
   262  
   263  func (target *NATSTarget) isActive() (bool, error) {
   264  	var connErr error
   265  	if target.args.Streaming.Enable {
   266  		if target.stanConn == nil || target.stanConn.NatsConn() == nil {
   267  			target.stanConn, connErr = target.args.connectStan()
   268  		} else if !target.stanConn.NatsConn().IsConnected() {
   269  			return false, store.ErrNotConnected
   270  		}
   271  	} else {
   272  		if target.natsConn == nil {
   273  			target.natsConn, connErr = target.args.connectNats()
   274  		} else if !target.natsConn.IsConnected() {
   275  			return false, store.ErrNotConnected
   276  		}
   277  	}
   278  
   279  	if connErr != nil {
   280  		if connErr.Error() == nats.ErrNoServers.Error() {
   281  			return false, store.ErrNotConnected
   282  		}
   283  		return false, connErr
   284  	}
   285  
   286  	if target.natsConn != nil && target.args.JetStream.Enable {
   287  		target.jstream, connErr = target.natsConn.JetStream()
   288  		if connErr != nil {
   289  			if connErr.Error() == nats.ErrNoServers.Error() {
   290  				return false, store.ErrNotConnected
   291  			}
   292  			return false, connErr
   293  		}
   294  	}
   295  
   296  	return true, nil
   297  }
   298  
   299  // Save - saves the events to the store which will be replayed when the Nats connection is active.
   300  func (target *NATSTarget) Save(eventData event.Event) error {
   301  	if target.store != nil {
   302  		return target.store.Put(eventData)
   303  	}
   304  
   305  	if err := target.init(); err != nil {
   306  		return err
   307  	}
   308  
   309  	_, err := target.isActive()
   310  	if err != nil {
   311  		return err
   312  	}
   313  	return target.send(eventData)
   314  }
   315  
   316  // send - sends an event to the Nats.
   317  func (target *NATSTarget) send(eventData event.Event) error {
   318  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   319  	if err != nil {
   320  		return err
   321  	}
   322  	key := eventData.S3.Bucket.Name + "/" + objectName
   323  
   324  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	if target.stanConn != nil {
   330  		if target.args.Streaming.Async {
   331  			_, err = target.stanConn.PublishAsync(target.args.Subject, data, nil)
   332  		} else {
   333  			err = target.stanConn.Publish(target.args.Subject, data)
   334  		}
   335  	} else {
   336  		if target.jstream != nil {
   337  			_, err = target.jstream.Publish(target.args.Subject, data)
   338  		} else {
   339  			err = target.natsConn.Publish(target.args.Subject, data)
   340  		}
   341  	}
   342  	return err
   343  }
   344  
   345  // SendFromStore - reads an event from store and sends it to Nats.
   346  func (target *NATSTarget) SendFromStore(key store.Key) error {
   347  	if err := target.init(); err != nil {
   348  		return err
   349  	}
   350  
   351  	_, err := target.isActive()
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	eventData, eErr := target.store.Get(key.Name)
   357  	if eErr != nil {
   358  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   359  		// Such events will not exist and wouldve been already been sent successfully.
   360  		if os.IsNotExist(eErr) {
   361  			return nil
   362  		}
   363  		return eErr
   364  	}
   365  
   366  	if err := target.send(eventData); err != nil {
   367  		return err
   368  	}
   369  
   370  	return target.store.Del(key.Name)
   371  }
   372  
   373  // Close - closes underneath connections to NATS server.
   374  func (target *NATSTarget) Close() (err error) {
   375  	close(target.quitCh)
   376  	if target.stanConn != nil {
   377  		// closing the streaming connection does not close the provided NATS connection.
   378  		if target.stanConn.NatsConn() != nil {
   379  			target.stanConn.NatsConn().Close()
   380  		}
   381  		return target.stanConn.Close()
   382  	}
   383  
   384  	if target.natsConn != nil {
   385  		target.natsConn.Close()
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  func (target *NATSTarget) init() error {
   392  	return target.initOnce.Do(target.initNATS)
   393  }
   394  
   395  func (target *NATSTarget) initNATS() error {
   396  	args := target.args
   397  
   398  	var err error
   399  	if args.Streaming.Enable {
   400  		target.loggerOnce(context.Background(), errors.New("NATS Streaming is deprecated please migrate to JetStream"), target.ID().String())
   401  		var stanConn stan.Conn
   402  		stanConn, err = args.connectStan()
   403  		target.stanConn = stanConn
   404  	} else {
   405  		var natsConn *nats.Conn
   406  		natsConn, err = args.connectNats()
   407  		target.natsConn = natsConn
   408  	}
   409  	if err != nil {
   410  		if err.Error() != nats.ErrNoServers.Error() {
   411  			target.loggerOnce(context.Background(), err, target.ID().String())
   412  		}
   413  		return err
   414  	}
   415  
   416  	if target.natsConn != nil && args.JetStream.Enable {
   417  		var jstream nats.JetStream
   418  		jstream, err = target.natsConn.JetStream()
   419  		if err != nil {
   420  			if err.Error() != nats.ErrNoServers.Error() {
   421  				target.loggerOnce(context.Background(), err, target.ID().String())
   422  			}
   423  			return err
   424  		}
   425  		target.jstream = jstream
   426  	}
   427  
   428  	yes, err := target.isActive()
   429  	if err != nil {
   430  		return err
   431  	}
   432  	if !yes {
   433  		return store.ErrNotConnected
   434  	}
   435  
   436  	return nil
   437  }
   438  
   439  // NewNATSTarget - creates new NATS target.
   440  func NewNATSTarget(id string, args NATSArgs, loggerOnce logger.LogOnce) (*NATSTarget, error) {
   441  	var queueStore store.Store[event.Event]
   442  	if args.QueueDir != "" {
   443  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-nats-"+id)
   444  		queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension)
   445  		if err := queueStore.Open(); err != nil {
   446  			return nil, fmt.Errorf("unable to initialize the queue store of NATS `%s`: %w", id, err)
   447  		}
   448  	}
   449  
   450  	target := &NATSTarget{
   451  		id:         event.TargetID{ID: id, Name: "nats"},
   452  		args:       args,
   453  		loggerOnce: loggerOnce,
   454  		store:      queueStore,
   455  		quitCh:     make(chan struct{}),
   456  	}
   457  
   458  	if target.store != nil {
   459  		store.StreamItems(target.store, target, target.quitCh, target.loggerOnce)
   460  	}
   461  
   462  	return target, nil
   463  }