github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/dashboard/api/pkg/store/postgres.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"embed"
     8  	"time"
     9  
    10  	"database/sql"
    11  
    12  	sync "github.com/bacalhau-project/golang-mutex-tracer"
    13  	"github.com/filecoin-project/bacalhau/dashboard/api/pkg/types"
    14  	"github.com/golang-migrate/migrate/v4"
    15  	"github.com/golang-migrate/migrate/v4/source/iofs"
    16  )
    17  
    18  type PostgresStore struct {
    19  	mtx              sync.RWMutex
    20  	connectionString string
    21  	db               *sql.DB
    22  }
    23  
    24  func NewPostgresStore(
    25  	host string,
    26  	port int,
    27  	database string,
    28  	username string,
    29  	password string,
    30  	autoMigrate bool,
    31  ) (*PostgresStore, error) {
    32  	connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable", username, password, host, port, database)
    33  	db, err := sql.Open("postgres", connectionString)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	store := &PostgresStore{
    38  		connectionString: connectionString,
    39  		db:               db,
    40  	}
    41  	store.mtx.EnableTracerWithOpts(sync.Opts{
    42  		Threshold: 10 * time.Millisecond,
    43  		Id:        "PostgresStore.mtx",
    44  	})
    45  	if autoMigrate {
    46  		err = store.MigrateUp()
    47  		if err != nil {
    48  			return nil, fmt.Errorf("there was an error doing the migration: %s", err.Error())
    49  		}
    50  	}
    51  	return store, nil
    52  }
    53  
    54  func (d *PostgresStore) LoadUser(
    55  	ctx context.Context,
    56  	username string,
    57  ) (*types.User, error) {
    58  	d.mtx.RLock()
    59  	defer d.mtx.RUnlock()
    60  	var id int
    61  	var created time.Time
    62  	var hashedPassword string
    63  	row := d.db.QueryRow("select id, created, hashed_password from useraccount where username = $1 limit 1", username)
    64  	err := row.Scan(&id, &created, &hashedPassword)
    65  	if err != nil {
    66  		if err == sql.ErrNoRows {
    67  			return nil, fmt.Errorf("user not found: %s %s", username, err.Error())
    68  		} else {
    69  			return nil, err
    70  		}
    71  	}
    72  	return &types.User{
    73  		ID:             id,
    74  		Created:        created,
    75  		Username:       username,
    76  		HashedPassword: hashedPassword,
    77  	}, nil
    78  }
    79  
    80  func (d *PostgresStore) LoadUserByID(
    81  	ctx context.Context,
    82  	queryID int,
    83  ) (*types.User, error) {
    84  	d.mtx.RLock()
    85  	defer d.mtx.RUnlock()
    86  	var username string
    87  	var created time.Time
    88  	var hashedPassword string
    89  	row := d.db.QueryRow("select username, created, hashed_password from useraccount where id = $1 limit 1", queryID)
    90  	err := row.Scan(&username, &created, &hashedPassword)
    91  	if err != nil {
    92  		if err == sql.ErrNoRows {
    93  			return nil, fmt.Errorf("user not found: %s %s", username, err.Error())
    94  		} else {
    95  			return nil, err
    96  		}
    97  	}
    98  	return &types.User{
    99  		ID:             queryID,
   100  		Created:        created,
   101  		Username:       username,
   102  		HashedPassword: hashedPassword,
   103  	}, nil
   104  }
   105  
   106  func (d *PostgresStore) GetJobModeration(
   107  	ctx context.Context,
   108  	queryJobID string,
   109  ) (*types.JobModeration, error) {
   110  	d.mtx.Lock()
   111  	defer d.mtx.Unlock()
   112  	var id int
   113  	var jobID string
   114  	var userAccountID int
   115  	var created time.Time
   116  	var status string
   117  	var notes string
   118  	row := d.db.QueryRow("select id, job_id, useraccount_id, created, status, notes from job_moderation where job_id = $1 limit 1", queryJobID)
   119  	err := row.Scan(&id, &jobID, &userAccountID, &created, &status, &notes)
   120  	if err != nil {
   121  		if err == sql.ErrNoRows {
   122  			return nil, nil
   123  		} else {
   124  			return nil, err
   125  		}
   126  	}
   127  	return &types.JobModeration{
   128  		ID:            id,
   129  		JobID:         jobID,
   130  		UserAccountID: userAccountID,
   131  		Created:       created,
   132  		Status:        status,
   133  		Notes:         notes,
   134  	}, nil
   135  }
   136  
   137  func (d *PostgresStore) GetAnnotationSummary(
   138  	ctx context.Context,
   139  ) ([]*types.AnnotationSummary, error) {
   140  	sqlStatement := `
   141  select
   142  	annotation,
   143  	count(*) as count
   144  from
   145  	job_annotation
   146  group by
   147  	annotation
   148  order by
   149  	annotation
   150  `
   151  
   152  	rows, err := d.db.Query(sqlStatement)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	defer rows.Close()
   157  	entries := []*types.AnnotationSummary{}
   158  	for rows.Next() {
   159  		var annotation string
   160  		var count int
   161  		if err = rows.Scan(&annotation, &count); err != nil {
   162  			return entries, err
   163  		}
   164  		entry := types.AnnotationSummary{
   165  			Annotation: annotation,
   166  			Count:      count,
   167  		}
   168  		entries = append(entries, &entry)
   169  	}
   170  	if err = rows.Err(); err != nil {
   171  		return entries, err
   172  	}
   173  	return entries, nil
   174  }
   175  
   176  func (d *PostgresStore) GetJobMonthSummary(
   177  	ctx context.Context,
   178  ) ([]*types.JobMonthSummary, error) {
   179  	sqlStatement := `
   180  select
   181  	concat(
   182  		extract(year from created),
   183  		'-',
   184  		extract(month from created)
   185  	) as month,
   186  	count(*) as count
   187  from
   188  	job
   189  group by
   190  	month
   191  order by
   192  	month
   193  `
   194  
   195  	rows, err := d.db.Query(sqlStatement)
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  	defer rows.Close()
   200  	entries := []*types.JobMonthSummary{}
   201  	for rows.Next() {
   202  		var month string
   203  		var count int
   204  		if err = rows.Scan(&month, &count); err != nil {
   205  			return entries, err
   206  		}
   207  		entry := types.JobMonthSummary{
   208  			Month: month,
   209  			Count: count,
   210  		}
   211  		entries = append(entries, &entry)
   212  	}
   213  	if err = rows.Err(); err != nil {
   214  		return entries, err
   215  	}
   216  	return entries, nil
   217  }
   218  
   219  func (d *PostgresStore) GetJobExecutorSummary(
   220  	ctx context.Context,
   221  ) ([]*types.JobExecutorSummary, error) {
   222  	sqlStatement := `
   223  select
   224  	executor,
   225  	count(*) as count
   226  from
   227  	job
   228  group by
   229  	executor
   230  order by
   231  	executor
   232  `
   233  
   234  	rows, err := d.db.Query(sqlStatement)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	defer rows.Close()
   239  	entries := []*types.JobExecutorSummary{}
   240  	for rows.Next() {
   241  		var executor string
   242  		var count int
   243  		if err = rows.Scan(&executor, &count); err != nil {
   244  			return entries, err
   245  		}
   246  		entry := types.JobExecutorSummary{
   247  			Executor: executor,
   248  			Count:    count,
   249  		}
   250  		entries = append(entries, &entry)
   251  	}
   252  	if err = rows.Err(); err != nil {
   253  		return entries, err
   254  	}
   255  	return entries, nil
   256  }
   257  
   258  func (d *PostgresStore) GetTotalJobsCount(
   259  	ctx context.Context,
   260  ) (*types.Counter, error) {
   261  	var count int
   262  	row := d.db.QueryRow("select count(*) as count from job")
   263  	err := row.Scan(&count)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	return &types.Counter{
   268  		Count: count,
   269  	}, nil
   270  }
   271  
   272  func (d *PostgresStore) GetTotalEventCount(
   273  	ctx context.Context,
   274  ) (*types.Counter, error) {
   275  	var count int
   276  	row := d.db.QueryRow("select count(*) as count from job_event")
   277  	err := row.Scan(&count)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  	return &types.Counter{
   282  		Count: count,
   283  	}, nil
   284  }
   285  
   286  func (d *PostgresStore) GetTotalUserCount(
   287  	ctx context.Context,
   288  ) (*types.Counter, error) {
   289  	var count int
   290  	row := d.db.QueryRow("select count(distinct clientid) as count from job")
   291  	err := row.Scan(&count)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	return &types.Counter{
   296  		Count: count,
   297  	}, nil
   298  }
   299  
   300  func (d *PostgresStore) GetTotalExecutorCount(
   301  	ctx context.Context,
   302  ) (*types.Counter, error) {
   303  	var count int
   304  	row := d.db.QueryRow("select count(distinct executor) as count from job")
   305  	err := row.Scan(&count)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	return &types.Counter{
   310  		Count: count,
   311  	}, nil
   312  }
   313  
   314  func (d *PostgresStore) AddUser(
   315  	ctx context.Context,
   316  	username string,
   317  	hashedPassword string,
   318  ) error {
   319  	d.mtx.Lock()
   320  	defer d.mtx.Unlock()
   321  	sqlStatement := `
   322  INSERT INTO useraccount (username, hashed_password)
   323  VALUES ($1, $2)`
   324  	_, err := d.db.Exec(
   325  		sqlStatement,
   326  		username,
   327  		hashedPassword,
   328  	)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	return nil
   333  }
   334  
   335  func (d *PostgresStore) UpdateUserPassword(
   336  	ctx context.Context,
   337  	username string,
   338  	hashedPassword string,
   339  ) error {
   340  	d.mtx.Lock()
   341  	defer d.mtx.Unlock()
   342  	sqlStatement := `UPDATE useraccount SET hashed_password = $1 WHERE username = $2`
   343  	_, err := d.db.Exec(
   344  		sqlStatement,
   345  		hashedPassword,
   346  		username,
   347  	)
   348  	return err
   349  }
   350  
   351  func (d *PostgresStore) CreateJobModeration(
   352  	ctx context.Context,
   353  	moderation types.JobModeration,
   354  ) error {
   355  	d.mtx.Lock()
   356  	defer d.mtx.Unlock()
   357  	sqlStatement := `
   358  INSERT INTO job_moderation (
   359  	job_id,
   360  	useraccount_id,
   361  	status,
   362  	notes
   363  )
   364  VALUES ($1, $2, $3, $4)`
   365  	_, err := d.db.Exec(
   366  		sqlStatement,
   367  		moderation.JobID,
   368  		moderation.UserAccountID,
   369  		moderation.Status,
   370  		moderation.Notes,
   371  	)
   372  	if err != nil {
   373  		return err
   374  	}
   375  	return nil
   376  }
   377  
   378  //go:embed migrations/*.sql
   379  var fs embed.FS
   380  
   381  func (d *PostgresStore) GetMigrations() (*migrate.Migrate, error) {
   382  	files, err := iofs.New(fs, "migrations")
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  	migrations, err := migrate.NewWithSourceInstance(
   387  		"iofs",
   388  		files,
   389  		fmt.Sprintf("%s&&x-migrations-table=dashboard_schema_migrations", d.connectionString),
   390  	)
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	return migrations, nil
   395  }
   396  
   397  func (d *PostgresStore) MigrateUp() error {
   398  	migrations, err := d.GetMigrations()
   399  	if err != nil {
   400  		return err
   401  	}
   402  	err = migrations.Up()
   403  	if err != migrate.ErrNoChange {
   404  		return err
   405  	}
   406  	return nil
   407  }
   408  
   409  func (d *PostgresStore) MigrateDown() error {
   410  	migrations, err := d.GetMigrations()
   411  	if err != nil {
   412  		return err
   413  	}
   414  	err = migrations.Down()
   415  	if err != migrate.ErrNoChange {
   416  		return err
   417  	}
   418  	return nil
   419  }