17755: Merge branch 'main' into 17755-add-singularity-to-compute-image
[arvados.git] / lib / boot / postgresql.go
index 86328e110c49233e14be141b2e40434894a66b58..d105b0b62342b4defc60838e14accd9f38ce9ed8 100644 (file)
@@ -8,9 +8,12 @@ import (
        "bytes"
        "context"
        "database/sql"
+       "fmt"
        "os"
        "os/exec"
+       "os/user"
        "path/filepath"
+       "strconv"
        "strings"
        "time"
 
@@ -18,83 +21,142 @@ import (
        "github.com/lib/pq"
 )
 
-func runPostgres(ctx context.Context, boot *Booter, ready chan<- bool) error {
-       buf := bytes.NewBuffer(nil)
-       err := boot.RunProgram(ctx, boot.tempdir, buf, nil, "pg_config", "--bindir")
+// Run a postgresql server in a private data directory. Set up a db
+// user, database, and TCP listener that match the supervisor's
+// configured database connection info.
+type runPostgreSQL struct{}
+
+func (runPostgreSQL) String() string {
+       return "postgresql"
+}
+
+func (runPostgreSQL) Run(ctx context.Context, fail func(error), super *Supervisor) error {
+       err := super.wait(ctx, createCertificates{})
        if err != nil {
                return err
        }
-       datadir := filepath.Join(boot.tempdir, "pgdata")
 
-       err = os.Mkdir(datadir, 0755)
-       if err != nil {
-               return err
+       if super.ClusterType == "production" {
+               return nil
+       }
+
+       iamroot := false
+       if u, err := user.Current(); err != nil {
+               return fmt.Errorf("user.Current(): %w", err)
+       } else if u.Uid == "0" {
+               iamroot = true
        }
-       bindir := strings.TrimSpace(buf.String())
 
-       err = boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "initdb"), "-D", datadir)
+       buf := bytes.NewBuffer(nil)
+       err = super.RunProgram(ctx, super.tempdir, runOptions{output: buf}, "pg_config", "--bindir")
        if err != nil {
                return err
        }
+       bindir := strings.TrimSpace(buf.String())
 
-       err = boot.RunProgram(ctx, boot.tempdir, nil, nil, "cp", "server.crt", "server.key", datadir)
+       datadir := filepath.Join(super.tempdir, "pgdata")
+       err = os.Mkdir(datadir, 0700)
        if err != nil {
                return err
        }
-
-       port := boot.cluster.PostgreSQL.Connection["port"]
-
-       ctx, cancel := context.WithCancel(ctx)
-       defer cancel()
-
-       go func() {
-               for {
-                       if ctx.Err() != nil {
-                               return
-                       }
-                       if exec.CommandContext(ctx, "pg_isready", "--timeout=10", "--host="+boot.cluster.PostgreSQL.Connection["host"], "--port="+port).Run() == nil {
-                               break
-                       }
-                       time.Sleep(time.Second / 2)
+       prog, args := filepath.Join(bindir, "initdb"), []string{"-D", datadir, "-E", "utf8"}
+       opts := runOptions{}
+       if iamroot {
+               postgresUser, err := user.Lookup("postgres")
+               if err != nil {
+                       return fmt.Errorf("user.Lookup(\"postgres\"): %s", err)
+               }
+               postgresUID, err := strconv.Atoi(postgresUser.Uid)
+               if err != nil {
+                       return fmt.Errorf("user.Lookup(\"postgres\"): non-numeric uid?: %q", postgresUser.Uid)
                }
-               db, err := sql.Open("postgres", arvados.PostgreSQLConnection{
-                       "host":   datadir,
-                       "port":   port,
-                       "dbname": "postgres",
-               }.String())
+               postgresGid, err := strconv.Atoi(postgresUser.Gid)
                if err != nil {
-                       boot.logger.WithError(err).Error("db open failed")
-                       cancel()
-                       return
+                       return fmt.Errorf("user.Lookup(\"postgres\"): non-numeric gid?: %q", postgresUser.Gid)
                }
-               defer db.Close()
-               conn, err := db.Conn(ctx)
+               err = os.Chown(super.tempdir, 0, postgresGid)
                if err != nil {
-                       boot.logger.WithError(err).Error("db conn failed")
-                       cancel()
-                       return
+                       return err
                }
-               defer conn.Close()
-               _, err = conn.ExecContext(ctx, `CREATE USER `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(boot.cluster.PostgreSQL.Connection["password"]))
+               err = os.Chmod(super.tempdir, 0710)
                if err != nil {
-                       boot.logger.WithError(err).Error("createuser failed")
-                       cancel()
-                       return
+                       return err
                }
-               _, err = conn.ExecContext(ctx, `CREATE DATABASE `+pq.QuoteIdentifier(boot.cluster.PostgreSQL.Connection["dbname"]))
+               err = os.Chown(datadir, postgresUID, 0)
                if err != nil {
-                       boot.logger.WithError(err).Error("createdb failed")
-                       cancel()
-                       return
+                       return err
                }
-               close(ready)
-               return
+               opts.user = "postgres"
+       }
+       err = super.RunProgram(ctx, super.tempdir, opts, prog, args...)
+       if err != nil {
+               return err
+       }
+
+       err = super.RunProgram(ctx, super.tempdir, runOptions{}, "cp", "server.crt", "server.key", datadir)
+       if err != nil {
+               return err
+       }
+       if iamroot {
+               err = super.RunProgram(ctx, super.tempdir, runOptions{}, "chown", "postgres", datadir+"/server.crt", datadir+"/server.key")
+               if err != nil {
+                       return err
+               }
+       }
+
+       port := super.cluster.PostgreSQL.Connection["port"]
+
+       super.waitShutdown.Add(1)
+       go func() {
+               defer super.waitShutdown.Done()
+               prog, args := filepath.Join(bindir, "postgres"), []string{
+                       "-l",          // enable ssl
+                       "-D", datadir, // data dir
+                       "-k", datadir, // socket dir
+                       "-h", super.cluster.PostgreSQL.Connection["host"],
+                       "-p", super.cluster.PostgreSQL.Connection["port"],
+               }
+               opts := runOptions{}
+               if iamroot {
+                       opts.user = "postgres"
+               }
+               fail(super.RunProgram(ctx, super.tempdir, opts, prog, args...))
        }()
 
-       return boot.RunProgram(ctx, boot.tempdir, nil, nil, filepath.Join(bindir, "postgres"),
-               "-l",          // enable ssl
-               "-D", datadir, // data dir
-               "-k", datadir, // socket dir
-               "-p", boot.cluster.PostgreSQL.Connection["port"],
-       )
+       for {
+               if ctx.Err() != nil {
+                       return ctx.Err()
+               }
+               if exec.CommandContext(ctx, "pg_isready", "--timeout=10", "--host="+super.cluster.PostgreSQL.Connection["host"], "--port="+port).Run() == nil {
+                       break
+               }
+               time.Sleep(time.Second / 2)
+       }
+       pgconn := arvados.PostgreSQLConnection{
+               "host":   datadir,
+               "port":   port,
+               "dbname": "postgres",
+       }
+       if iamroot {
+               pgconn["user"] = "postgres"
+       }
+       db, err := sql.Open("postgres", pgconn.String())
+       if err != nil {
+               return fmt.Errorf("db open failed: %s", err)
+       }
+       defer db.Close()
+       conn, err := db.Conn(ctx)
+       if err != nil {
+               return fmt.Errorf("db conn failed: %s", err)
+       }
+       defer conn.Close()
+       _, err = conn.ExecContext(ctx, `CREATE USER `+pq.QuoteIdentifier(super.cluster.PostgreSQL.Connection["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(super.cluster.PostgreSQL.Connection["password"]))
+       if err != nil {
+               return fmt.Errorf("createuser failed: %s", err)
+       }
+       _, err = conn.ExecContext(ctx, `CREATE DATABASE `+pq.QuoteIdentifier(super.cluster.PostgreSQL.Connection["dbname"])+` WITH TEMPLATE template0 ENCODING 'utf8'`)
+       if err != nil {
+               return fmt.Errorf("createdb failed: %s", err)
+       }
+       return nil
 }