github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/postgresrunner/postgresrunner.go (about)

     1  package postgresrunner
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"os/exec"
     9  	"os/user"
    10  	"path/filepath"
    11  	"strconv"
    12  	"syscall"
    13  
    14  	"code.cloudfoundry.org/lager/lagertest"
    15  
    16  	"github.com/pf-qiu/concourse/v6/atc/db"
    17  	"github.com/pf-qiu/concourse/v6/atc/db/migration"
    18  	"github.com/onsi/ginkgo"
    19  	. "github.com/onsi/gomega"
    20  	"github.com/onsi/gomega/gexec"
    21  	"github.com/tedsuo/ifrit/ginkgomon"
    22  )
    23  
    24  type Runner struct {
    25  	Port int
    26  }
    27  
    28  func (runner Runner) Run(signals <-chan os.Signal, ready chan<- struct{}) error {
    29  	defer ginkgo.GinkgoRecover()
    30  
    31  	pgBase := filepath.Join(os.TempDir(), "concourse-pg-runner")
    32  
    33  	err := os.MkdirAll(pgBase, 0755)
    34  	Expect(err).NotTo(HaveOccurred())
    35  
    36  	tmpdir, err := ioutil.TempDir(pgBase, "postgres")
    37  	Expect(err).NotTo(HaveOccurred())
    38  
    39  	currentUser, err := user.Current()
    40  	Expect(err).NotTo(HaveOccurred())
    41  
    42  	initdbPath, err := exec.LookPath("initdb")
    43  	Expect(err).NotTo(HaveOccurred())
    44  
    45  	postgresPath, err := exec.LookPath("postgres")
    46  	Expect(err).NotTo(HaveOccurred())
    47  
    48  	initCmd := exec.Command(initdbPath, "-U", "postgres", "-D", tmpdir, "-E", "UTF8", "--no-local")
    49  	startCmd := exec.Command(postgresPath, "-k", "/tmp", "-D", tmpdir, "-h", "127.0.0.1", "-p", strconv.Itoa(runner.Port))
    50  
    51  	if currentUser.Uid == "0" {
    52  		pgUser, err := user.Lookup("postgres")
    53  		Expect(err).NotTo(HaveOccurred())
    54  
    55  		var uid, gid uint32
    56  		_, err = fmt.Sscanf(pgUser.Uid, "%d", &uid)
    57  		Expect(err).NotTo(HaveOccurred())
    58  
    59  		_, err = fmt.Sscanf(pgUser.Gid, "%d", &gid)
    60  		Expect(err).NotTo(HaveOccurred())
    61  
    62  		err = os.Chown(tmpdir, int(uid), int(gid))
    63  		Expect(err).NotTo(HaveOccurred())
    64  
    65  		credential := &syscall.Credential{Uid: uid, Gid: gid}
    66  
    67  		initCmd.SysProcAttr = &syscall.SysProcAttr{}
    68  		initCmd.SysProcAttr.Credential = credential
    69  
    70  		startCmd.SysProcAttr = &syscall.SysProcAttr{}
    71  		startCmd.SysProcAttr.Credential = credential
    72  	}
    73  
    74  	session, err := gexec.Start(
    75  		initCmd,
    76  		gexec.NewPrefixedWriter("[o][initdb] ", ginkgo.GinkgoWriter),
    77  		gexec.NewPrefixedWriter("[e][initdb] ", ginkgo.GinkgoWriter),
    78  	)
    79  	Expect(err).NotTo(HaveOccurred())
    80  
    81  	<-session.Exited
    82  
    83  	Expect(session).To(gexec.Exit(0))
    84  
    85  	ginkgoRunner := &ginkgomon.Runner{
    86  		Name:          "postgres",
    87  		Command:       startCmd,
    88  		AnsiColorCode: "90m",
    89  		StartCheck:    "database system is ready to accept connections",
    90  		Cleanup: func() {
    91  			os.RemoveAll(tmpdir)
    92  		},
    93  	}
    94  
    95  	return ginkgoRunner.Run(signals, ready)
    96  }
    97  
    98  func (runner *Runner) MigrateToVersion(version int) {
    99  	err := migration.NewOpenHelper(
   100  		"postgres",
   101  		runner.DataSourceName(),
   102  		nil,
   103  		nil,
   104  		nil,
   105  	).MigrateToVersion(version)
   106  	Expect(err).NotTo(HaveOccurred())
   107  }
   108  
   109  func (runner *Runner) TryOpenDBAtVersion(version int) (*sql.DB, error) {
   110  	dbConn, err := migration.NewOpenHelper(
   111  		"postgres",
   112  		runner.DataSourceName(),
   113  		nil,
   114  		nil,
   115  		nil,
   116  	).OpenAtVersion(version)
   117  
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	// only allow one connection so that we can detect any code paths that
   123  	// require more than one, which will deadlock if it's at the limit
   124  	dbConn.SetMaxOpenConns(1)
   125  
   126  	return dbConn, nil
   127  }
   128  
   129  func (runner *Runner) OpenDBAtVersion(version int) *sql.DB {
   130  	dbConn, err := runner.TryOpenDBAtVersion(version)
   131  	Expect(err).NotTo(HaveOccurred())
   132  	return dbConn
   133  }
   134  
   135  func (runner *Runner) OpenDB() *sql.DB {
   136  	dbConn, err := migration.NewOpenHelper(
   137  		"postgres",
   138  		runner.DataSourceName(),
   139  		nil,
   140  		nil,
   141  		nil,
   142  	).Open()
   143  	Expect(err).NotTo(HaveOccurred())
   144  
   145  	// only allow one connection so that we can detect any code paths that
   146  	// require more than one, which will deadlock if it's at the limit
   147  	dbConn.SetMaxOpenConns(1)
   148  
   149  	return dbConn
   150  }
   151  
   152  func (runner *Runner) OpenConn() db.Conn {
   153  	dbConn, err := db.Open(
   154  		lagertest.NewTestLogger("postgres-runner"),
   155  		"postgres",
   156  		runner.DataSourceName(),
   157  		nil,
   158  		nil,
   159  		"postgresrunner",
   160  		nil,
   161  	)
   162  	Expect(err).NotTo(HaveOccurred())
   163  
   164  	// only allow one connection so that we can detect any code paths that
   165  	// require more than one, which will deadlock if it's at the limit
   166  	dbConn.SetMaxOpenConns(1)
   167  
   168  	return dbConn
   169  }
   170  
   171  func (runner *Runner) OpenSingleton() *sql.DB {
   172  	dbConn, err := sql.Open("postgres", runner.DataSourceName())
   173  	Expect(err).NotTo(HaveOccurred())
   174  
   175  	// only allow one connection, period. this matches production code use case,
   176  	// as this is used for advisory locks.
   177  	dbConn.SetMaxIdleConns(1)
   178  	dbConn.SetMaxOpenConns(1)
   179  	dbConn.SetConnMaxLifetime(0)
   180  
   181  	return dbConn
   182  }
   183  
   184  func (runner *Runner) DataSourceName() string {
   185  	return fmt.Sprintf("host=/tmp user=postgres dbname=testdb sslmode=disable port=%d", runner.Port)
   186  }
   187  
   188  func (runner *Runner) CreateTestDB() {
   189  	createdb := exec.Command("createdb", "-h", "/tmp", "-U", "postgres", "-p", strconv.Itoa(runner.Port), "testdb")
   190  
   191  	createS, err := gexec.Start(createdb, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter)
   192  	Expect(err).NotTo(HaveOccurred())
   193  
   194  	<-createS.Exited
   195  
   196  	if createS.ExitCode() != 0 {
   197  		runner.DropTestDB()
   198  
   199  		createdb := exec.Command("createdb", "-h", "/tmp", "-U", "postgres", "-p", strconv.Itoa(runner.Port), "testdb")
   200  		createS, err = gexec.Start(createdb, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter)
   201  		Expect(err).NotTo(HaveOccurred())
   202  	}
   203  
   204  	<-createS.Exited
   205  
   206  	Expect(createS).To(gexec.Exit(0))
   207  }
   208  
   209  func (runner *Runner) DropTestDB() {
   210  	dropdb := exec.Command("dropdb", "-h", "/tmp", "-U", "postgres", "-p", strconv.Itoa(runner.Port), "testdb")
   211  	dropS, err := gexec.Start(dropdb, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter)
   212  	Expect(err).NotTo(HaveOccurred())
   213  
   214  	<-dropS.Exited
   215  
   216  	Expect(dropS).To(gexec.Exit(0))
   217  }
   218  
   219  func (runner *Runner) Truncate() {
   220  	truncate := exec.Command(
   221  		"psql",
   222  		"-h", "/tmp",
   223  		"-U", "postgres",
   224  		"-p", strconv.Itoa(runner.Port),
   225  		"testdb",
   226  		"-c", `
   227  			SET client_min_messages TO WARNING;
   228  
   229  			CREATE OR REPLACE FUNCTION truncate_tables() RETURNS void AS $$
   230  			DECLARE
   231  					statements CURSOR FOR
   232  							SELECT tablename FROM pg_tables
   233  							WHERE schemaname = 'public' AND tablename != 'migrations_history';
   234  			BEGIN
   235  					FOR stmt IN statements LOOP
   236  							EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' RESTART IDENTITY CASCADE;';
   237  					END LOOP;
   238  			END;
   239  			$$ LANGUAGE plpgsql;
   240  
   241  			CREATE OR REPLACE FUNCTION drop_ephemeral_sequences() RETURNS void AS $$
   242  			DECLARE
   243  					statements CURSOR FOR
   244  							SELECT relname FROM pg_class
   245  							WHERE relname LIKE 'build_event_id_seq_%';
   246  			BEGIN
   247  					FOR stmt IN statements LOOP
   248  							EXECUTE 'DROP SEQUENCE ' || quote_ident(stmt.relname) || ';';
   249  					END LOOP;
   250  			END;
   251  			$$ LANGUAGE plpgsql;
   252  
   253  			CREATE OR REPLACE FUNCTION drop_ephemeral_tables() RETURNS void AS $$
   254  			DECLARE
   255  					statements CURSOR FOR
   256  							SELECT relname FROM pg_class
   257  							WHERE relname LIKE 'pipeline_build_events_%'
   258  							AND relkind = 'r';
   259  					team_statements CURSOR FOR
   260  							SELECT relname FROM pg_class
   261  							WHERE relname LIKE 'team_build_events_%'
   262  							AND relkind = 'r';
   263  			BEGIN
   264  					FOR stmt IN statements LOOP
   265  							EXECUTE 'DROP TABLE ' || quote_ident(stmt.relname) || ';';
   266  					END LOOP;
   267  					FOR stmt IN team_statements LOOP
   268  							EXECUTE 'DROP TABLE ' || quote_ident(stmt.relname) || ';';
   269  					END LOOP;
   270  			END;
   271  			$$ LANGUAGE plpgsql;
   272  
   273  			CREATE OR REPLACE FUNCTION reset_global_sequences() RETURNS void AS $$
   274  			DECLARE
   275  					statements CURSOR FOR
   276  							SELECT relname FROM pg_class
   277  							WHERE relname IN ('one_off_name', 'config_version_seq');
   278  			BEGIN
   279  					FOR stmt IN statements LOOP
   280  							EXECUTE 'ALTER SEQUENCE ' || quote_ident(stmt.relname) || ' RESTART WITH 1;';
   281  					END LOOP;
   282  			END;
   283  			$$ LANGUAGE plpgsql;
   284  
   285  			SELECT truncate_tables();
   286  			SELECT drop_ephemeral_sequences();
   287  			SELECT drop_ephemeral_tables();
   288  			SELECT reset_global_sequences();
   289  		`,
   290  	)
   291  
   292  	truncateS, err := gexec.Start(truncate, ginkgo.GinkgoWriter, ginkgo.GinkgoWriter)
   293  	Expect(err).NotTo(HaveOccurred())
   294  
   295  	<-truncateS.Exited
   296  
   297  	Expect(truncateS).To(gexec.Exit(0))
   298  }