| package component |
| |
| import ( |
| "database/sql" |
| "errors" |
| "flag" |
| "fmt" |
| "net/url" |
| "os" |
| "sync" |
| |
| "github.com/cockroachdb/cockroach-go/v2/testserver" |
| "github.com/golang-migrate/migrate/v4" |
| _ "github.com/golang-migrate/migrate/v4/database/cockroachdb" |
| "github.com/golang-migrate/migrate/v4/source" |
| _ "github.com/lib/pq" |
| "k8s.io/klog/v2" |
| |
| "source.monogon.dev/metropolis/cli/pkg/datafile" |
| ) |
| |
| // CockroachConfig is the common configuration of a components' connection to |
| // CockroachDB. It's supposed to be instantiated within a Configuration struct |
| // of a component. |
| // |
| // It can be configured by flags (via RegisterFlags) or manually (eg. in tests). |
| type CockroachConfig struct { |
| // Migrations is the go-migrate source of migrations for this database. Usually |
| // this can be taken from a go-embedded set of migration files. |
| Migrations source.Driver |
| |
| // EndpointHost is the host part of the endpoint address of the database server. |
| EndpointHost string |
| // TLSKeyPath is the filesystem path of the x509 key used to authenticate to the |
| // database server. |
| TLSKeyPath string |
| // TLSKeyPath is the filesystem path of the x509 certificate used to |
| // authenticate to the database server. |
| TLSCertificatePath string |
| // TLSCACertificatePath is the filesystem path of the x509 CA certificate used |
| // to verify the database server's certificate. |
| TLSCACertificatePath string |
| // UserName is the username to be used on the database server. |
| UserName string |
| // UserName is the database name to be used on the database server. |
| DatabaseName string |
| |
| // InMemory indicates that an in-memory CockroachDB instance should be used. |
| // Data will be lost after the component shuts down. |
| InMemory bool |
| |
| // mu guards inMemoryInstance. |
| mu sync.Mutex |
| // inMemoryInstance is populated with a CockroachDB test server handle when |
| // InMemory is set and Connect()/MigrateUp() is called. |
| inMemoryInstance testserver.TestServer |
| } |
| |
| // RegisterFlags registers the connection configuration to be provided by flags. |
| // This must be called exactly once before then calling flags.Parse(). |
| func (c *CockroachConfig) RegisterFlags(prefix string) { |
| flag.StringVar(&c.EndpointHost, prefix+"_endpoint_host", "", "Host of CockroachDB endpoint for "+prefix) |
| flag.StringVar(&c.TLSKeyPath, prefix+"_tls_key_path", "", "Path to CockroachDB TLS client key for "+prefix) |
| flag.StringVar(&c.TLSCertificatePath, prefix+"_tls_certificate_path", "", "Path to CockroachDB TLS client certificate for "+prefix) |
| flag.StringVar(&c.TLSCACertificatePath, prefix+"_tls_ca_certificate_path", "", "Path to CockroachDB CA certificate for "+prefix) |
| flag.StringVar(&c.UserName, prefix+"_user_name", prefix, "CockroachDB user name for "+prefix) |
| flag.StringVar(&c.DatabaseName, prefix+"_database_name", prefix, "CockroachDB database name for "+prefix) |
| flag.BoolVar(&c.InMemory, prefix+"_eat_my_data", false, "Use in-memory CockroachDB for "+prefix+". Warning: Data will be lost at process shutdown!") |
| } |
| |
| // startInMemory starts an in-memory cockroachdb server as a subprocess, and |
| // returns a DSN that connects to the newly created database. |
| func (c *CockroachConfig) startInMemory(scheme string) string { |
| c.mu.Lock() |
| defer c.mu.Unlock() |
| |
| klog.Warningf("STARTING IN-MEMORY COCKROACHDB FOR TESTS") |
| klog.Warningf("ALL DATA WILL BE LOST AFTER SERVER SHUTDOWN!") |
| |
| if c.inMemoryInstance == nil { |
| opts := []testserver.TestServerOpt{ |
| testserver.SecureOpt(), |
| } |
| if path, err := datafile.ResolveRunfile("external/cockroach/cockroach"); err == nil { |
| opts = append(opts, testserver.CockroachBinaryPathOpt(path)) |
| } else { |
| if os.Getenv("TEST_TMPDIR") != "" { |
| klog.Exitf("In test which requires in-memory cockroachdb, but @cockroach//:cockroach missing as a dependency. Failing.") |
| } |
| klog.Warningf("CockroachDB in-memory database requested, but not available as a build dependency. Trying to download it...") |
| } |
| |
| inst, err := testserver.NewTestServer(opts...) |
| if err != nil { |
| klog.Exitf("Failed to create crdb test server: %v", err) |
| } |
| c.inMemoryInstance = inst |
| } |
| |
| u := *c.inMemoryInstance.PGURL() |
| u.Scheme = scheme |
| return u.String() |
| } |
| |
| // buildDSN returns a DSN to the configured database connection with a given DSN |
| // scheme. The scheme will usually be 'postgres' or 'cockroach', depending on |
| // whether it's used for lib/pq or for golang-migrate. |
| func (c *CockroachConfig) buildDSN(scheme string) string { |
| if c.InMemory { |
| return c.startInMemory(scheme) |
| } |
| |
| query := make(url.Values) |
| query.Set("sslmode", "verify-full") |
| query.Set("sslcert", c.TLSCertificatePath) |
| query.Set("sslkey", c.TLSKeyPath) |
| query.Set("sslrootcert", c.TLSCACertificatePath) |
| u := url.URL{ |
| Scheme: scheme, |
| User: url.User(c.UserName), |
| Host: c.EndpointHost, |
| Path: c.DatabaseName, |
| RawQuery: query.Encode(), |
| } |
| return u.String() |
| } |
| |
| // Connect returns a working *sql.DB handle to the database described by this |
| // CockroachConfig. |
| func (d *CockroachConfig) Connect() (*sql.DB, error) { |
| dsn := d.buildDSN("postgres") |
| klog.Infof("Connecting to %s...", dsn) |
| return sql.Open("postgres", d.buildDSN("postgres")) |
| } |
| |
| // MigrateUp performs all possible migrations upwards for the database described |
| // by this CockroachConfig. |
| func (d *CockroachConfig) MigrateUp() error { |
| dsn := d.buildDSN("cockroachdb") |
| klog.Infof("Running migrations up...") |
| m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn) |
| if err != nil { |
| return err |
| } |
| err = m.Up() |
| switch { |
| case err == nil: |
| return nil |
| case errors.Is(err, migrate.ErrNoChange): |
| return nil |
| default: |
| return err |
| } |
| } |
| |
| func (d *CockroachConfig) MigrateUpToIncluding(ver uint) error { |
| dsn := d.buildDSN("cockroachdb") |
| klog.Infof("Running migrations up to %d...", ver) |
| m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn) |
| if err != nil { |
| return err |
| } |
| |
| return m.Migrate(ver) |
| } |
| |
| // MigrateDownDangerDanger removes all data from the database by performing a |
| // full migration down. |
| // |
| // Let me reiterate: this function, by design, DESTROYS YOUR DATA. |
| // |
| // Obviously, this is a dangerous method. Thus, to prevent accidental nuking of |
| // production data, we currently only allow this to be performed on InMemory |
| // databases. |
| func (d *CockroachConfig) MigrateDownDangerDanger() error { |
| if !d.InMemory { |
| return fmt.Errorf("refusing to migrate down a non-in-memory database") |
| } |
| // Sneaky extra check to make sure the caller didn't just set InMemory after |
| // connecting to an external database. We really need to be safe here. |
| if d.inMemoryInstance == nil { |
| return fmt.Errorf("no really, this cannot be run on non-in-memory databases") |
| } |
| dsn := d.buildDSN("cockroachdb") |
| klog.Infof("Running migrations down...") |
| m, err := migrate.NewWithSourceInstance("iofs", d.Migrations, dsn) |
| if err != nil { |
| return err |
| } |
| // Final sneaky check, make sure the remote schema version is our maximum locally |
| // supported version. |
| v, _, err := m.Version() |
| if err != nil { |
| return fmt.Errorf("could not retrieve remote version: %w", err) |
| } |
| if v2, err := d.Migrations.Next(v); !os.IsNotExist(err) { |
| return fmt.Errorf("remote running version %d, but we know %d which is newer", v, v2) |
| } |
| return m.Down() |
| } |