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