| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 1 | package component |
| 2 | |
| 3 | import ( |
| 4 | "database/sql" |
| Serge Bazanski | 2fd37ea | 2023-04-06 14:48:53 +0200 | [diff] [blame] | 5 | "errors" |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 6 | "flag" |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 7 | "fmt" |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 8 | "net/url" |
| 9 | "os" |
| 10 | "sync" |
| 11 | |
| Tim Windelschmidt | 2a1d1b2 | 2024-02-06 07:07:42 +0100 | [diff] [blame] | 12 | "github.com/bazelbuild/rules_go/go/runfiles" |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 13 | "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 Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 19 | ) |
| 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). |
| 26 | type 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(). |
| 60 | func (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. |
| 72 | func (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 Windelschmidt | 2a1d1b2 | 2024-02-06 07:07:42 +0100 | [diff] [blame] | 83 | if path, err := runfiles.Rlocation("cockroach/cockroach"); err == nil { |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 84 | 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. |
| 107 | func (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. |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 129 | func (c *CockroachConfig) Connect() (*sql.DB, error) { |
| 130 | dsn := c.buildDSN("postgres") |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 131 | klog.Infof("Connecting to %s...", dsn) |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 132 | return sql.Open("postgres", c.buildDSN("postgres")) |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 133 | } |
| 134 | |
| 135 | // MigrateUp performs all possible migrations upwards for the database described |
| 136 | // by this CockroachConfig. |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 137 | func (c *CockroachConfig) MigrateUp() error { |
| 138 | dsn := c.buildDSN("cockroachdb") |
| Serge Bazanski | 3ea40da | 2023-04-19 14:32:37 +0200 | [diff] [blame] | 139 | klog.Infof("Running migrations up...") |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 140 | m, err := migrate.NewWithSourceInstance("iofs", c.Migrations, dsn) |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 141 | if err != nil { |
| 142 | return err |
| 143 | } |
| Serge Bazanski | 2fd37ea | 2023-04-06 14:48:53 +0200 | [diff] [blame] | 144 | 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 Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 153 | } |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 154 | |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 155 | func (c *CockroachConfig) MigrateUpToIncluding(ver uint) error { |
| 156 | dsn := c.buildDSN("cockroachdb") |
| Serge Bazanski | 3ea40da | 2023-04-19 14:32:37 +0200 | [diff] [blame] | 157 | klog.Infof("Running migrations up to %d...", ver) |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 158 | m, err := migrate.NewWithSourceInstance("iofs", c.Migrations, dsn) |
| Serge Bazanski | 3ea40da | 2023-04-19 14:32:37 +0200 | [diff] [blame] | 159 | if err != nil { |
| 160 | return err |
| 161 | } |
| 162 | |
| 163 | return m.Migrate(ver) |
| 164 | } |
| 165 | |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 166 | // 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. |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 174 | func (c *CockroachConfig) MigrateDownDangerDanger() error { |
| 175 | if !c.InMemory { |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 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. |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 180 | if c.inMemoryInstance == nil { |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 181 | return fmt.Errorf("no really, this cannot be run on non-in-memory databases") |
| 182 | } |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 183 | dsn := c.buildDSN("cockroachdb") |
| Serge Bazanski | 3ea40da | 2023-04-19 14:32:37 +0200 | [diff] [blame] | 184 | klog.Infof("Running migrations down...") |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 185 | m, err := migrate.NewWithSourceInstance("iofs", c.Migrations, dsn) |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 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 | } |
| Tim Windelschmidt | 0c57d34 | 2024-04-11 01:38:47 +0200 | [diff] [blame^] | 195 | if v2, err := c.Migrations.Next(v); !os.IsNotExist(err) { |
| Serge Bazanski | 48e9bab | 2023-02-20 15:28:59 +0100 | [diff] [blame] | 196 | return fmt.Errorf("remote running version %d, but we know %d which is newer", v, v2) |
| 197 | } |
| 198 | return m.Down() |
| 199 | } |