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