github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/postgresql.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  	"database/sql"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  
    33  	_ "github.com/lib/pq" // Register postgres driver
    34  
    35  	"github.com/minio/minio/internal/event"
    36  	"github.com/minio/minio/internal/logger"
    37  	"github.com/minio/minio/internal/once"
    38  	"github.com/minio/minio/internal/store"
    39  	xnet "github.com/minio/pkg/v2/net"
    40  )
    41  
    42  const (
    43  	psqlTableExists          = `SELECT 1 FROM %s;`
    44  	psqlCreateNamespaceTable = `CREATE TABLE %s (key VARCHAR PRIMARY KEY, value JSONB);`
    45  	psqlCreateAccessTable    = `CREATE TABLE %s (event_time TIMESTAMP WITH TIME ZONE NOT NULL, event_data JSONB);`
    46  
    47  	psqlUpdateRow = `INSERT INTO %s (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;`
    48  	psqlDeleteRow = `DELETE FROM %s WHERE key = $1;`
    49  	psqlInsertRow = `INSERT INTO %s (event_time, event_data) VALUES ($1, $2);`
    50  )
    51  
    52  // Postgres constants
    53  const (
    54  	PostgresFormat             = "format"
    55  	PostgresConnectionString   = "connection_string"
    56  	PostgresTable              = "table"
    57  	PostgresHost               = "host"
    58  	PostgresPort               = "port"
    59  	PostgresUsername           = "username"
    60  	PostgresPassword           = "password"
    61  	PostgresDatabase           = "database"
    62  	PostgresQueueDir           = "queue_dir"
    63  	PostgresQueueLimit         = "queue_limit"
    64  	PostgresMaxOpenConnections = "max_open_connections"
    65  
    66  	EnvPostgresEnable             = "MINIO_NOTIFY_POSTGRES_ENABLE"
    67  	EnvPostgresFormat             = "MINIO_NOTIFY_POSTGRES_FORMAT"
    68  	EnvPostgresConnectionString   = "MINIO_NOTIFY_POSTGRES_CONNECTION_STRING"
    69  	EnvPostgresTable              = "MINIO_NOTIFY_POSTGRES_TABLE"
    70  	EnvPostgresHost               = "MINIO_NOTIFY_POSTGRES_HOST"
    71  	EnvPostgresPort               = "MINIO_NOTIFY_POSTGRES_PORT"
    72  	EnvPostgresUsername           = "MINIO_NOTIFY_POSTGRES_USERNAME"
    73  	EnvPostgresPassword           = "MINIO_NOTIFY_POSTGRES_PASSWORD"
    74  	EnvPostgresDatabase           = "MINIO_NOTIFY_POSTGRES_DATABASE"
    75  	EnvPostgresQueueDir           = "MINIO_NOTIFY_POSTGRES_QUEUE_DIR"
    76  	EnvPostgresQueueLimit         = "MINIO_NOTIFY_POSTGRES_QUEUE_LIMIT"
    77  	EnvPostgresMaxOpenConnections = "MINIO_NOTIFY_POSTGRES_MAX_OPEN_CONNECTIONS"
    78  )
    79  
    80  // PostgreSQLArgs - PostgreSQL target arguments.
    81  type PostgreSQLArgs struct {
    82  	Enable             bool      `json:"enable"`
    83  	Format             string    `json:"format"`
    84  	ConnectionString   string    `json:"connectionString"`
    85  	Table              string    `json:"table"`
    86  	Host               xnet.Host `json:"host"`     // default: localhost
    87  	Port               string    `json:"port"`     // default: 5432
    88  	Username           string    `json:"username"` // default: user running minio
    89  	Password           string    `json:"password"` // default: no password
    90  	Database           string    `json:"database"` // default: same as user
    91  	QueueDir           string    `json:"queueDir"`
    92  	QueueLimit         uint64    `json:"queueLimit"`
    93  	MaxOpenConnections int       `json:"maxOpenConnections"`
    94  }
    95  
    96  // Validate PostgreSQLArgs fields
    97  func (p PostgreSQLArgs) Validate() error {
    98  	if !p.Enable {
    99  		return nil
   100  	}
   101  	if p.Table == "" {
   102  		return fmt.Errorf("empty table name")
   103  	}
   104  	if p.Format != "" {
   105  		f := strings.ToLower(p.Format)
   106  		if f != event.NamespaceFormat && f != event.AccessFormat {
   107  			return fmt.Errorf("unrecognized format value")
   108  		}
   109  	}
   110  
   111  	if p.ConnectionString != "" {
   112  		// No pq API doesn't help to validate connection string
   113  		// prior connection, so no validation for now.
   114  	} else {
   115  		// Some fields need to be specified when ConnectionString is unspecified
   116  		if p.Port == "" {
   117  			return fmt.Errorf("unspecified port")
   118  		}
   119  		if _, err := strconv.Atoi(p.Port); err != nil {
   120  			return fmt.Errorf("invalid port")
   121  		}
   122  		if p.Database == "" {
   123  			return fmt.Errorf("database unspecified")
   124  		}
   125  	}
   126  
   127  	if p.QueueDir != "" {
   128  		if !filepath.IsAbs(p.QueueDir) {
   129  			return errors.New("queueDir path should be absolute")
   130  		}
   131  	}
   132  
   133  	if p.MaxOpenConnections < 0 {
   134  		return errors.New("maxOpenConnections cannot be less than zero")
   135  	}
   136  
   137  	return nil
   138  }
   139  
   140  // PostgreSQLTarget - PostgreSQL target.
   141  type PostgreSQLTarget struct {
   142  	initOnce once.Init
   143  
   144  	id         event.TargetID
   145  	args       PostgreSQLArgs
   146  	updateStmt *sql.Stmt
   147  	deleteStmt *sql.Stmt
   148  	insertStmt *sql.Stmt
   149  	db         *sql.DB
   150  	store      store.Store[event.Event]
   151  	firstPing  bool
   152  	connString string
   153  	loggerOnce logger.LogOnce
   154  	quitCh     chan struct{}
   155  }
   156  
   157  // ID - returns target ID.
   158  func (target *PostgreSQLTarget) ID() event.TargetID {
   159  	return target.id
   160  }
   161  
   162  // Name - returns the Name of the target.
   163  func (target *PostgreSQLTarget) Name() string {
   164  	return target.ID().String()
   165  }
   166  
   167  // Store returns any underlying store if set.
   168  func (target *PostgreSQLTarget) Store() event.TargetStore {
   169  	return target.store
   170  }
   171  
   172  // IsActive - Return true if target is up and active
   173  func (target *PostgreSQLTarget) IsActive() (bool, error) {
   174  	if err := target.init(); err != nil {
   175  		return false, err
   176  	}
   177  	return target.isActive()
   178  }
   179  
   180  func (target *PostgreSQLTarget) isActive() (bool, error) {
   181  	if err := target.db.Ping(); err != nil {
   182  		if IsConnErr(err) {
   183  			return false, store.ErrNotConnected
   184  		}
   185  		return false, err
   186  	}
   187  	return true, nil
   188  }
   189  
   190  // Save - saves the events to the store if questore is configured, which will be replayed when the PostgreSQL connection is active.
   191  func (target *PostgreSQLTarget) Save(eventData event.Event) error {
   192  	if target.store != nil {
   193  		return target.store.Put(eventData)
   194  	}
   195  
   196  	if err := target.init(); err != nil {
   197  		return err
   198  	}
   199  
   200  	_, err := target.isActive()
   201  	if err != nil {
   202  		return err
   203  	}
   204  	return target.send(eventData)
   205  }
   206  
   207  // IsConnErr - To detect a connection error.
   208  func IsConnErr(err error) bool {
   209  	return xnet.IsConnRefusedErr(err) || err.Error() == "sql: database is closed" || err.Error() == "sql: statement is closed" || err.Error() == "invalid connection"
   210  }
   211  
   212  // send - sends an event to the PostgreSQL.
   213  func (target *PostgreSQLTarget) send(eventData event.Event) error {
   214  	if target.args.Format == event.NamespaceFormat {
   215  		objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   216  		if err != nil {
   217  			return err
   218  		}
   219  		key := eventData.S3.Bucket.Name + "/" + objectName
   220  
   221  		if eventData.EventName == event.ObjectRemovedDelete {
   222  			_, err = target.deleteStmt.Exec(key)
   223  		} else {
   224  			var data []byte
   225  			if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil {
   226  				return err
   227  			}
   228  
   229  			_, err = target.updateStmt.Exec(key, data)
   230  		}
   231  		return err
   232  	}
   233  
   234  	if target.args.Format == event.AccessFormat {
   235  		eventTime, err := time.Parse(event.AMZTimeFormat, eventData.EventTime)
   236  		if err != nil {
   237  			return err
   238  		}
   239  
   240  		data, err := json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}})
   241  		if err != nil {
   242  			return err
   243  		}
   244  
   245  		if _, err = target.insertStmt.Exec(eventTime, data); err != nil {
   246  			return err
   247  		}
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  // SendFromStore - reads an event from store and sends it to PostgreSQL.
   254  func (target *PostgreSQLTarget) SendFromStore(key store.Key) error {
   255  	if err := target.init(); err != nil {
   256  		return err
   257  	}
   258  
   259  	_, err := target.isActive()
   260  	if err != nil {
   261  		return err
   262  	}
   263  	if !target.firstPing {
   264  		if err := target.executeStmts(); err != nil {
   265  			if IsConnErr(err) {
   266  				return store.ErrNotConnected
   267  			}
   268  			return err
   269  		}
   270  	}
   271  
   272  	eventData, eErr := target.store.Get(key.Name)
   273  	if eErr != nil {
   274  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   275  		// Such events will not exist and wouldve been already been sent successfully.
   276  		if os.IsNotExist(eErr) {
   277  			return nil
   278  		}
   279  		return eErr
   280  	}
   281  
   282  	if err := target.send(eventData); err != nil {
   283  		if IsConnErr(err) {
   284  			return store.ErrNotConnected
   285  		}
   286  		return err
   287  	}
   288  
   289  	// Delete the event from store.
   290  	return target.store.Del(key.Name)
   291  }
   292  
   293  // Close - closes underneath connections to PostgreSQL database.
   294  func (target *PostgreSQLTarget) Close() error {
   295  	close(target.quitCh)
   296  	if target.updateStmt != nil {
   297  		// FIXME: log returned error. ignore time being.
   298  		_ = target.updateStmt.Close()
   299  	}
   300  
   301  	if target.deleteStmt != nil {
   302  		// FIXME: log returned error. ignore time being.
   303  		_ = target.deleteStmt.Close()
   304  	}
   305  
   306  	if target.insertStmt != nil {
   307  		// FIXME: log returned error. ignore time being.
   308  		_ = target.insertStmt.Close()
   309  	}
   310  
   311  	if target.db != nil {
   312  		target.db.Close()
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  // Executes the table creation statements.
   319  func (target *PostgreSQLTarget) executeStmts() error {
   320  	_, err := target.db.Exec(fmt.Sprintf(psqlTableExists, target.args.Table))
   321  	if err != nil {
   322  		createStmt := psqlCreateNamespaceTable
   323  		if target.args.Format == event.AccessFormat {
   324  			createStmt = psqlCreateAccessTable
   325  		}
   326  
   327  		if _, dbErr := target.db.Exec(fmt.Sprintf(createStmt, target.args.Table)); dbErr != nil {
   328  			return dbErr
   329  		}
   330  	}
   331  
   332  	switch target.args.Format {
   333  	case event.NamespaceFormat:
   334  		// insert or update statement
   335  		if target.updateStmt, err = target.db.Prepare(fmt.Sprintf(psqlUpdateRow, target.args.Table)); err != nil {
   336  			return err
   337  		}
   338  		// delete statement
   339  		if target.deleteStmt, err = target.db.Prepare(fmt.Sprintf(psqlDeleteRow, target.args.Table)); err != nil {
   340  			return err
   341  		}
   342  	case event.AccessFormat:
   343  		// insert statement
   344  		if target.insertStmt, err = target.db.Prepare(fmt.Sprintf(psqlInsertRow, target.args.Table)); err != nil {
   345  			return err
   346  		}
   347  	}
   348  
   349  	return nil
   350  }
   351  
   352  func (target *PostgreSQLTarget) init() error {
   353  	return target.initOnce.Do(target.initPostgreSQL)
   354  }
   355  
   356  func (target *PostgreSQLTarget) initPostgreSQL() error {
   357  	args := target.args
   358  
   359  	db, err := sql.Open("postgres", target.connString)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	target.db = db
   364  
   365  	if args.MaxOpenConnections > 0 {
   366  		// Set the maximum connections limit
   367  		target.db.SetMaxOpenConns(args.MaxOpenConnections)
   368  	}
   369  
   370  	err = target.db.Ping()
   371  	if err != nil {
   372  		if !(xnet.IsConnRefusedErr(err) || xnet.IsConnResetErr(err)) {
   373  			target.loggerOnce(context.Background(), err, target.ID().String())
   374  		}
   375  	} else {
   376  		if err = target.executeStmts(); err != nil {
   377  			target.loggerOnce(context.Background(), err, target.ID().String())
   378  		} else {
   379  			target.firstPing = true
   380  		}
   381  	}
   382  
   383  	if err != nil {
   384  		target.db.Close()
   385  		return err
   386  	}
   387  
   388  	yes, err := target.isActive()
   389  	if err != nil {
   390  		return err
   391  	}
   392  	if !yes {
   393  		return store.ErrNotConnected
   394  	}
   395  
   396  	return nil
   397  }
   398  
   399  // NewPostgreSQLTarget - creates new PostgreSQL target.
   400  func NewPostgreSQLTarget(id string, args PostgreSQLArgs, loggerOnce logger.LogOnce) (*PostgreSQLTarget, error) {
   401  	params := []string{args.ConnectionString}
   402  	if args.ConnectionString == "" {
   403  		params = []string{}
   404  		if !args.Host.IsEmpty() {
   405  			params = append(params, "host="+args.Host.String())
   406  		}
   407  		if args.Port != "" {
   408  			params = append(params, "port="+args.Port)
   409  		}
   410  		if args.Username != "" {
   411  			params = append(params, "username="+args.Username)
   412  		}
   413  		if args.Password != "" {
   414  			params = append(params, "password="+args.Password)
   415  		}
   416  		if args.Database != "" {
   417  			params = append(params, "dbname="+args.Database)
   418  		}
   419  	}
   420  	connStr := strings.Join(params, " ")
   421  
   422  	var queueStore store.Store[event.Event]
   423  	if args.QueueDir != "" {
   424  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-postgresql-"+id)
   425  		queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension)
   426  		if err := queueStore.Open(); err != nil {
   427  			return nil, fmt.Errorf("unable to initialize the queue store of PostgreSQL `%s`: %w", id, err)
   428  		}
   429  	}
   430  
   431  	target := &PostgreSQLTarget{
   432  		id:         event.TargetID{ID: id, Name: "postgresql"},
   433  		args:       args,
   434  		firstPing:  false,
   435  		store:      queueStore,
   436  		connString: connStr,
   437  		loggerOnce: loggerOnce,
   438  		quitCh:     make(chan struct{}),
   439  	}
   440  
   441  	if target.store != nil {
   442  		store.StreamItems(target.store, target, target.quitCh, target.loggerOnce)
   443  	}
   444  
   445  	return target, nil
   446  }