19709: Apply pending rails migrations at service start/restart.
[arvados.git] / lib / boot / rails_db.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package boot
6
7 import (
8         "context"
9         "io/fs"
10         "os"
11         "path/filepath"
12         "strings"
13
14         "git.arvados.org/arvados.git/lib/controller/dblock"
15         "git.arvados.org/arvados.git/lib/ctrlctx"
16 )
17
18 type railsDatabase struct{}
19
20 func (runner railsDatabase) String() string {
21         return "railsDatabase"
22 }
23
24 func (runner railsDatabase) Run(ctx context.Context, fail func(error), super *Supervisor) error {
25         err := super.wait(ctx, runPostgreSQL{}, installPassenger{src: "services/api"})
26         if err != nil {
27                 return err
28         }
29
30         // determine path to installed rails app or source tree
31         var appdir string
32         if super.ClusterType == "production" {
33                 appdir = "/var/lib/arvados/railsapi"
34         } else {
35                 appdir = filepath.Join(super.SourcePath, "services/api")
36         }
37
38         // list versions in db/migrate/{version}_{name}.rb
39         todo := map[string]bool{}
40         fs.WalkDir(os.DirFS(appdir), "db/migrate", func(path string, d fs.DirEntry, err error) error {
41                 if cut := strings.Index(d.Name(), "_"); cut > 0 && strings.HasSuffix(d.Name(), ".rb") {
42                         todo[d.Name()[:cut]] = true
43                 }
44                 return nil
45         })
46
47         // read schema_migrations table (list of migrations already
48         // applied) and remove those entries from todo
49         dbconnector := ctrlctx.DBConnector{PostgreSQL: super.cluster.PostgreSQL}
50         defer dbconnector.Close()
51         db, err := dbconnector.GetDB(ctx)
52         if err != nil {
53                 return err
54         }
55         rows, err := db.QueryContext(ctx, `SELECT version FROM schema_migrations`)
56         if err != nil {
57                 if super.ClusterType == "production" {
58                         return err
59                 }
60                 super.logger.WithError(err).Info("schema_migrations query failed, trying db:setup")
61                 return super.RunProgram(ctx, "services/api", runOptions{env: railsEnv}, "bundle", "exec", "rake", "db:setup")
62         }
63         for rows.Next() {
64                 var v string
65                 err = rows.Scan(&v)
66                 if err != nil {
67                         return err
68                 }
69                 delete(todo, v)
70         }
71         err = rows.Close()
72         if err != nil {
73                 return err
74         }
75
76         // if nothing remains in todo, all available migrations are
77         // done, so return without running any [relatively slow]
78         // ruby/rake commands
79         if len(todo) == 0 {
80                 return nil
81         }
82
83         super.logger.Infof("%d migrations pending", len(todo))
84         if !dblock.RailsMigrations.Lock(ctx, dbconnector.GetDB) {
85                 return context.Canceled
86         }
87         defer dblock.RailsMigrations.Unlock()
88         return super.RunProgram(ctx, appdir, runOptions{env: railsEnv}, "bundle", "exec", "rake", "db:migrate")
89 }