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

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