blob: aae1f77bd09a17f62080c4720a5162216a7379f4 [file] [log] [blame]
Serge Bazanskia5baa872022-09-15 18:49:35 +02001package component
2
3import (
4 "database/sql"
Serge Bazanski2fd37ea2023-04-06 14:48:53 +02005 "errors"
Serge Bazanskia5baa872022-09-15 18:49:35 +02006 "flag"
Serge Bazanski48e9bab2023-02-20 15:28:59 +01007 "fmt"
Serge Bazanskia5baa872022-09-15 18:49:35 +02008 "net/url"
9 "os"
10 "sync"
11
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010012 "github.com/bazelbuild/rules_go/go/runfiles"
Serge Bazanskia5baa872022-09-15 18:49:35 +020013 "github.com/cockroachdb/cockroach-go/v2/testserver"
14 "github.com/golang-migrate/migrate/v4"
15 _ "github.com/golang-migrate/migrate/v4/database/cockroachdb"
16 "github.com/golang-migrate/migrate/v4/source"
17 _ "github.com/lib/pq"
18 "k8s.io/klog/v2"
Serge Bazanskia5baa872022-09-15 18:49:35 +020019)
20
21// CockroachConfig is the common configuration of a components' connection to
22// CockroachDB. It's supposed to be instantiated within a Configuration struct
23// of a component.
24//
25// It can be configured by flags (via RegisterFlags) or manually (eg. in tests).
26type CockroachConfig struct {
27 // Migrations is the go-migrate source of migrations for this database. Usually
28 // this can be taken from a go-embedded set of migration files.
29 Migrations source.Driver
30
31 // EndpointHost is the host part of the endpoint address of the database server.
32 EndpointHost string
33 // TLSKeyPath is the filesystem path of the x509 key used to authenticate to the
34 // database server.
35 TLSKeyPath string
36 // TLSKeyPath is the filesystem path of the x509 certificate used to
37 // authenticate to the database server.
38 TLSCertificatePath string
39 // TLSCACertificatePath is the filesystem path of the x509 CA certificate used
40 // to verify the database server's certificate.
41 TLSCACertificatePath string
42 // UserName is the username to be used on the database server.
43 UserName string
44 // UserName is the database name to be used on the database server.
45 DatabaseName string
46
47 // InMemory indicates that an in-memory CockroachDB instance should be used.
48 // Data will be lost after the component shuts down.
49 InMemory bool
50
51 // mu guards inMemoryInstance.
52 mu sync.Mutex
53 // inMemoryInstance is populated with a CockroachDB test server handle when
54 // InMemory is set and Connect()/MigrateUp() is called.
55 inMemoryInstance testserver.TestServer
56}
57
58// RegisterFlags registers the connection configuration to be provided by flags.
59// This must be called exactly once before then calling flags.Parse().
60func (c *CockroachConfig) RegisterFlags(prefix string) {
61 flag.StringVar(&c.EndpointHost, prefix+"_endpoint_host", "", "Host of CockroachDB endpoint for "+prefix)
62 flag.StringVar(&c.TLSKeyPath, prefix+"_tls_key_path", "", "Path to CockroachDB TLS client key for "+prefix)
63 flag.StringVar(&c.TLSCertificatePath, prefix+"_tls_certificate_path", "", "Path to CockroachDB TLS client certificate for "+prefix)
64 flag.StringVar(&c.TLSCACertificatePath, prefix+"_tls_ca_certificate_path", "", "Path to CockroachDB CA certificate for "+prefix)
65 flag.StringVar(&c.UserName, prefix+"_user_name", prefix, "CockroachDB user name for "+prefix)
66 flag.StringVar(&c.DatabaseName, prefix+"_database_name", prefix, "CockroachDB database name for "+prefix)
67 flag.BoolVar(&c.InMemory, prefix+"_eat_my_data", false, "Use in-memory CockroachDB for "+prefix+". Warning: Data will be lost at process shutdown!")
68}
69
70// startInMemory starts an in-memory cockroachdb server as a subprocess, and
71// returns a DSN that connects to the newly created database.
72func (c *CockroachConfig) startInMemory(scheme string) string {
73 c.mu.Lock()
74 defer c.mu.Unlock()
75
76 klog.Warningf("STARTING IN-MEMORY COCKROACHDB FOR TESTS")
77 klog.Warningf("ALL DATA WILL BE LOST AFTER SERVER SHUTDOWN!")
78
79 if c.inMemoryInstance == nil {
80 opts := []testserver.TestServerOpt{
81 testserver.SecureOpt(),
82 }
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010083 if path, err := runfiles.Rlocation("cockroach/cockroach"); err == nil {
Serge Bazanskia5baa872022-09-15 18:49:35 +020084 opts = append(opts, testserver.CockroachBinaryPathOpt(path))
85 } else {
86 if os.Getenv("TEST_TMPDIR") != "" {
87 klog.Exitf("In test which requires in-memory cockroachdb, but @cockroach//:cockroach missing as a dependency. Failing.")
88 }
89 klog.Warningf("CockroachDB in-memory database requested, but not available as a build dependency. Trying to download it...")
90 }
91
92 inst, err := testserver.NewTestServer(opts...)
93 if err != nil {
94 klog.Exitf("Failed to create crdb test server: %v", err)
95 }
96 c.inMemoryInstance = inst
97 }
98
99 u := *c.inMemoryInstance.PGURL()
100 u.Scheme = scheme
101 return u.String()
102}
103
104// buildDSN returns a DSN to the configured database connection with a given DSN
105// scheme. The scheme will usually be 'postgres' or 'cockroach', depending on
106// whether it's used for lib/pq or for golang-migrate.
107func (c *CockroachConfig) buildDSN(scheme string) string {
108 if c.InMemory {
109 return c.startInMemory(scheme)
110 }
111
112 query := make(url.Values)
113 query.Set("sslmode", "verify-full")
114 query.Set("sslcert", c.TLSCertificatePath)
115 query.Set("sslkey", c.TLSKeyPath)
116 query.Set("sslrootcert", c.TLSCACertificatePath)
117 u := url.URL{
118 Scheme: scheme,
119 User: url.User(c.UserName),
120 Host: c.EndpointHost,
121 Path: c.DatabaseName,
122 RawQuery: query.Encode(),
123 }
124 return u.String()
125}
126
127// Connect returns a working *sql.DB handle to the database described by this
128// CockroachConfig.
129func (d *CockroachConfig) Connect() (*sql.DB, error) {
130 dsn := d.buildDSN("postgres")
131 klog.Infof("Connecting to %s...", dsn)
132 return sql.Open("postgres", d.buildDSN("postgres"))
133}
134
135// MigrateUp performs all possible migrations upwards for the database described
136// by this CockroachConfig.
137func (d *CockroachConfig) MigrateUp() error {
138 dsn := d.buildDSN("cockroachdb")
Serge Bazanski3ea40da2023-04-19 14:32:37 +0200139 klog.Infof("Running migrations up...")
Serge Bazanskia5baa872022-09-15 18:49:35 +0200140 m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn)
141 if err != nil {
142 return err
143 }
Serge Bazanski2fd37ea2023-04-06 14:48:53 +0200144 err = m.Up()
145 switch {
146 case err == nil:
147 return nil
148 case errors.Is(err, migrate.ErrNoChange):
149 return nil
150 default:
151 return err
152 }
Serge Bazanskia5baa872022-09-15 18:49:35 +0200153}
Serge Bazanski48e9bab2023-02-20 15:28:59 +0100154
Serge Bazanski3ea40da2023-04-19 14:32:37 +0200155func (d *CockroachConfig) MigrateUpToIncluding(ver uint) error {
156 dsn := d.buildDSN("cockroachdb")
157 klog.Infof("Running migrations up to %d...", ver)
158 m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn)
159 if err != nil {
160 return err
161 }
162
163 return m.Migrate(ver)
164}
165
Serge Bazanski48e9bab2023-02-20 15:28:59 +0100166// MigrateDownDangerDanger removes all data from the database by performing a
167// full migration down.
168//
169// Let me reiterate: this function, by design, DESTROYS YOUR DATA.
170//
171// Obviously, this is a dangerous method. Thus, to prevent accidental nuking of
172// production data, we currently only allow this to be performed on InMemory
173// databases.
174func (d *CockroachConfig) MigrateDownDangerDanger() error {
175 if !d.InMemory {
176 return fmt.Errorf("refusing to migrate down a non-in-memory database")
177 }
178 // Sneaky extra check to make sure the caller didn't just set InMemory after
179 // connecting to an external database. We really need to be safe here.
180 if d.inMemoryInstance == nil {
181 return fmt.Errorf("no really, this cannot be run on non-in-memory databases")
182 }
183 dsn := d.buildDSN("cockroachdb")
Serge Bazanski3ea40da2023-04-19 14:32:37 +0200184 klog.Infof("Running migrations down...")
Serge Bazanski48e9bab2023-02-20 15:28:59 +0100185 m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn)
186 if err != nil {
187 return err
188 }
189 // Final sneaky check, make sure the remote schema version is our maximum locally
190 // supported version.
191 v, _, err := m.Version()
192 if err != nil {
193 return fmt.Errorf("could not retrieve remote version: %w", err)
194 }
195 if v2, err := d.Migrations.Next(v); !os.IsNotExist(err) {
196 return fmt.Errorf("remote running version %d, but we know %d which is newer", v, v2)
197 }
198 return m.Down()
199}