mirror of https://github.com/minio/minio.git
				
				
				
			
		
			
				
	
	
		
			264 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
|  * Minio Cloud Storage, (C) 2014-2016 Minio, Inc.
 | |
|  *
 | |
|  * Licensed under the Apache License, Version 2.0 (the "License");
 | |
|  * you may not use this file except in compliance with the License.
 | |
|  * You may obtain a copy of the License at
 | |
|  *
 | |
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | |
|  *
 | |
|  * Unless required by applicable law or agreed to in writing, software
 | |
|  * distributed under the License is distributed on an "AS IS" BASIS,
 | |
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|  * See the License for the specific language governing permissions and
 | |
|  * limitations under the License.
 | |
|  */
 | |
| 
 | |
| // PostgreSQL Notifier implementation. A table with a specific
 | |
| // structure (column names, column types, and primary key/uniqueness
 | |
| // constraint) is used. The user may set the table name in the
 | |
| // configuration. A sample SQL command that creates a command with the
 | |
| // required structure is:
 | |
| //
 | |
| //     CREATE TABLE myminio (
 | |
| //         key VARCHAR PRIMARY KEY,
 | |
| //         value JSONB
 | |
| //     );
 | |
| //
 | |
| // PostgreSQL's "INSERT ... ON CONFLICT ... DO UPDATE ..." feature
 | |
| // (UPSERT) is used here, so the minimum version of PostgreSQL
 | |
| // required is 9.5.
 | |
| //
 | |
| // On each create or update object event in Minio Object storage
 | |
| // server, a row is created or updated in the table in Postgres. On
 | |
| // each object removal, the corresponding row is deleted from the
 | |
| // table.
 | |
| 
 | |
| package cmd
 | |
| 
 | |
| import (
 | |
| 	"database/sql"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/Sirupsen/logrus"
 | |
| 
 | |
| 	// libpq db driver is usually imported blank - see examples in
 | |
| 	// https://godoc.org/github.com/lib/pq
 | |
| 	_ "github.com/lib/pq"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	upsertRow = `INSERT INTO %s (key, value)
 | |
| VALUES ($1, $2)
 | |
| ON CONFLICT (key)
 | |
| DO UPDATE SET value = EXCLUDED.value;`
 | |
| 	deleteRow = ` DELETE FROM %s
 | |
| WHERE key = $1;`
 | |
| 	createTable = `CREATE TABLE %s (
 | |
|     key VARCHAR PRIMARY KEY,
 | |
|     value JSONB
 | |
| );`
 | |
| 	tableExists = `SELECT 1 FROM %s;`
 | |
| )
 | |
| 
 | |
| type postgreSQLNotify struct {
 | |
| 	Enable bool `json:"enable"`
 | |
| 
 | |
| 	// pass connection string in config directly. This string is
 | |
| 	// formatted according to
 | |
| 	// https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
 | |
| 	ConnectionString string `json:"connectionString"`
 | |
| 	// specifying a table name is required.
 | |
| 	Table string `json:"table"`
 | |
| 
 | |
| 	// uses the values below if no connection string is specified
 | |
| 	// - however the connection string method offers more
 | |
| 	// flexibility.
 | |
| 	Host     string `json:"host"`
 | |
| 	Port     string `json:"port"`
 | |
| 	User     string `json:"user"`
 | |
| 	Password string `json:"password"`
 | |
| 	Database string `json:"database"`
 | |
| }
 | |
| 
 | |
| func (p *postgreSQLNotify) Validate() error {
 | |
| 	if !p.Enable {
 | |
| 		return nil
 | |
| 	}
 | |
| 	if _, err := checkNetURL(p.Host); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type pgConn struct {
 | |
| 	connStr       string
 | |
| 	table         string
 | |
| 	preparedStmts map[string]*sql.Stmt
 | |
| 	*sql.DB
 | |
| }
 | |
| 
 | |
| func dialPostgreSQL(pgN postgreSQLNotify) (pgConn, error) {
 | |
| 	if !pgN.Enable {
 | |
| 		return pgConn{}, errNotifyNotEnabled
 | |
| 	}
 | |
| 
 | |
| 	// check that table is specified
 | |
| 	if pgN.Table == "" {
 | |
| 		return pgConn{}, fmt.Errorf(
 | |
| 			"PostgreSQL Notifier Error: Table was not specified in configuration")
 | |
| 	}
 | |
| 
 | |
| 	connStr := pgN.ConnectionString
 | |
| 	// check if connection string is specified
 | |
| 	if connStr == "" {
 | |
| 		// build from other parameters
 | |
| 		params := []string{}
 | |
| 		if pgN.Host != "" {
 | |
| 			params = append(params, "host="+pgN.Host)
 | |
| 		}
 | |
| 		if pgN.Port != "" {
 | |
| 			params = append(params, "port="+pgN.Port)
 | |
| 		}
 | |
| 		if pgN.User != "" {
 | |
| 			params = append(params, "user="+pgN.User)
 | |
| 		}
 | |
| 		if pgN.Password != "" {
 | |
| 			params = append(params, "password="+pgN.Password)
 | |
| 		}
 | |
| 		if pgN.Database != "" {
 | |
| 			params = append(params, "dbname="+pgN.Database)
 | |
| 		}
 | |
| 		connStr = strings.Join(params, " ")
 | |
| 	}
 | |
| 
 | |
| 	db, err := sql.Open("postgres", connStr)
 | |
| 	if err != nil {
 | |
| 		return pgConn{}, fmt.Errorf(
 | |
| 			"PostgreSQL Notifier Error: Connection opening failure (connectionString=%s): %v",
 | |
| 			connStr, err,
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	// ping to check that server is actually reachable.
 | |
| 	err = db.Ping()
 | |
| 	if err != nil {
 | |
| 		return pgConn{}, fmt.Errorf(
 | |
| 			"PostgreSQL Notifier Error: Ping to server failed with: %v",
 | |
| 			err,
 | |
| 		)
 | |
| 	}
 | |
| 
 | |
| 	// check that table exists - if not, create it.
 | |
| 	_, err = db.Exec(fmt.Sprintf(tableExists, pgN.Table))
 | |
| 	if err != nil {
 | |
| 		// most likely, table does not exist. try to create it:
 | |
| 		_, errCreate := db.Exec(fmt.Sprintf(createTable, pgN.Table))
 | |
| 		if errCreate != nil {
 | |
| 			// failed to create the table. error out.
 | |
| 			return pgConn{}, fmt.Errorf(
 | |
| 				"PostgreSQL Notifier Error: 'Select' failed with %v, then 'Create Table' failed with %v",
 | |
| 				err, errCreate,
 | |
| 			)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// create prepared statements
 | |
| 	stmts := make(map[string]*sql.Stmt)
 | |
| 	// insert or update statement
 | |
| 	stmts["upsertRow"], err = db.Prepare(fmt.Sprintf(upsertRow, pgN.Table))
 | |
| 	if err != nil {
 | |
| 		return pgConn{},
 | |
| 			fmt.Errorf("PostgreSQL Notifier Error: create UPSERT prepared statement failed with: %v", err)
 | |
| 	}
 | |
| 	stmts["deleteRow"], err = db.Prepare(fmt.Sprintf(deleteRow, pgN.Table))
 | |
| 	if err != nil {
 | |
| 		return pgConn{},
 | |
| 			fmt.Errorf("PostgreSQL Notifier Error: create DELETE prepared statement failed with: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return pgConn{connStr, pgN.Table, stmts, db}, nil
 | |
| }
 | |
| 
 | |
| func newPostgreSQLNotify(accountID string) (*logrus.Logger, error) {
 | |
| 	pgNotify := serverConfig.Notify.GetPostgreSQLByID(accountID)
 | |
| 
 | |
| 	// Dial postgres
 | |
| 	pgC, err := dialPostgreSQL(pgNotify)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	pgLog := logrus.New()
 | |
| 
 | |
| 	pgLog.Out = ioutil.Discard
 | |
| 
 | |
| 	pgLog.Formatter = new(logrus.JSONFormatter)
 | |
| 
 | |
| 	pgLog.Hooks.Add(pgC)
 | |
| 
 | |
| 	return pgLog, nil
 | |
| }
 | |
| 
 | |
| func (pgC pgConn) Close() {
 | |
| 	// first close all prepared statements
 | |
| 	for _, v := range pgC.preparedStmts {
 | |
| 		_ = v.Close()
 | |
| 	}
 | |
| 	// close db connection
 | |
| 	_ = pgC.DB.Close()
 | |
| }
 | |
| 
 | |
| func (pgC pgConn) Fire(entry *logrus.Entry) error {
 | |
| 	// get event type by trying to convert to string
 | |
| 	entryEventType, ok := entry.Data["EventType"].(string)
 | |
| 	if !ok {
 | |
| 		// ignore event if converting EventType to string
 | |
| 		// fails.
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// Check for event delete
 | |
| 	if eventMatch(entryEventType, []string{"s3:ObjectRemoved:*"}) {
 | |
| 		// delete row from the table
 | |
| 		_, err := pgC.preparedStmts["deleteRow"].Exec(entry.Data["Key"])
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf(
 | |
| 				"Error deleting event with key = %v - got postgres error - %v",
 | |
| 				entry.Data["Key"], err,
 | |
| 			)
 | |
| 		}
 | |
| 	} else {
 | |
| 		// json encode the value for the row
 | |
| 		value, err := json.Marshal(map[string]interface{}{
 | |
| 			"Records": entry.Data["Records"],
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf(
 | |
| 				"Unable to encode event %v to JSON - got error - %v",
 | |
| 				entry.Data["Records"], err,
 | |
| 			)
 | |
| 		}
 | |
| 
 | |
| 		// upsert row into the table
 | |
| 		_, err = pgC.preparedStmts["upsertRow"].Exec(entry.Data["Key"], value)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf(
 | |
| 				"Unable to upsert event with Key=%v and Value=%v - got postgres error - %v",
 | |
| 				entry.Data["Key"], entry.Data["Records"], err,
 | |
| 			)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (pgC pgConn) Levels() []logrus.Level {
 | |
| 	return []logrus.Level{
 | |
| 		logrus.InfoLevel,
 | |
| 	}
 | |
| }
 |