Path: blob/main/components/service-waiter/cmd/database.go
2498 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cmd56import (7"context"8"crypto/tls"9"crypto/x509"10"database/sql"11_ "embed"12"errors"13"fmt"14"net"15"os"16"strings"17"time"1819"github.com/go-sql-driver/mysql"20"github.com/spf13/cobra"21"github.com/spf13/viper"2223"github.com/gitpod-io/gitpod/common-go/log"24)2526const migrationTableName = "migrations"2728//go:embed resources/latest-migration.txt29var latestMigrationName string3031// databaseCmd represents the database command32var databaseCmd = &cobra.Command{33Use: "database",34Short: "waits for a MySQL database to become available",35Long: `Uses the default db env config of a Gitpod deployment to try and36connect to a MySQL database, specifically DB_HOST, DB_PORT, DB_PASSWORD,37DB_CA_CERT and DB_USER(=gitpod)`,38PreRun: func(cmd *cobra.Command, args []string) {39err := viper.BindPFlags(cmd.Flags())40if err != nil {41log.WithError(err).Fatal("cannot bind Viper to pflags")42}43},44Run: func(cmd *cobra.Command, args []string) {45timeout := getTimeout()46ctx, cancel := context.WithTimeout(cmd.Context(), timeout)47defer cancel()4849cfg := mysql.NewConfig()50cfg.Addr = net.JoinHostPort(viper.GetString("host"), viper.GetString("port"))51cfg.Net = "tcp"52cfg.User = viper.GetString("username")53cfg.Passwd = viper.GetString("password")5455// Must be "gitpod"56// Align to https://github.com/gitpod-io/gitpod/blob/884d922e8e33d8b936ec18d7fe3c8dcffde42b5a/components/gitpod-db/go/conn.go#L3757cfg.DBName = "gitpod"58cfg.Timeout = 1 * time.Second5960dsn := cfg.FormatDSN()61censoredDSN := dsn62if cfg.Passwd != "" {63censoredDSN = strings.Replace(dsn, cfg.Passwd, "*****", -1)64}6566caCert := viper.GetString("caCert")67if caCert != "" {68rootCertPool := x509.NewCertPool()69if ok := rootCertPool.AppendCertsFromPEM([]byte(caCert)); !ok {70fail("Failed to append DB CA cert.")71}7273tlsConfigName := "custom"74err := mysql.RegisterTLSConfig(tlsConfigName, &tls.Config{75RootCAs: rootCertPool,76MinVersion: tls.VersionTLS12, // semgrep finding: set lower boundary to exclude insecure TLS1.077})78if err != nil {79fail(fmt.Sprintf("Failed to register DB CA cert: %+v", err))80}81cfg.TLSConfig = tlsConfigName82}8384migrationName := GetLatestMigrationName()85migrationCheck := viper.GetBool("migration-check")86log.WithField("timeout", timeout.String()).WithField("dsn", censoredDSN).WithField("migration", migrationName).WithField("migrationCheck", migrationCheck).Info("waiting for database")87var lastErr error88log.Info("attempting to check if database is available")89for ctx.Err() == nil {90if err := checkDbAvailable(ctx, cfg, migrationName, migrationCheck); err != nil {91if lastErr == nil || (lastErr != nil && err.Error() != lastErr.Error()) {92// log if error is new or changed93log.WithError(err).Error("database not available")94log.Info("attempting to check if database is available")95} else {96log.WithError(err).Debug("database not available, attempting to check again")97}98lastErr = err99<-time.After(time.Second)100} else {101break102}103}104105if ctx.Err() != nil {106log.WithError(ctx.Err()).WithError(lastErr).Error("database did not become available in time")107fail(fmt.Sprintf("database did not become available in time(%s): %+v", timeout.String(), lastErr))108} else {109log.Info("database became available")110}111},112}113114func GetLatestMigrationName() string {115return strings.TrimSpace(latestMigrationName)116}117118// checkDbAvailable will connect and check if database is connectable119// with migrations and migrationCheck set, it will also check if the latest migration has been applied120func checkDbAvailable(ctx context.Context, cfg *mysql.Config, migration string, migrationCheck bool) (err error) {121db, err := sql.Open("mysql", cfg.FormatDSN())122if err != nil {123return err124}125// ignore error126defer db.Close()127128// if migration name is not set, just ping the database129if migration == "" || !migrationCheck {130return db.PingContext(ctx)131}132133row := db.QueryRowContext(ctx, "SELECT name FROM "+migrationTableName+" ORDER BY `timestamp` DESC LIMIT 1")134var dbLatest string135if err := row.Scan(&dbLatest); err != nil {136if errors.Is(err, sql.ErrNoRows) {137return fmt.Errorf("failed to check migrations: no row found")138}139return fmt.Errorf("failed to check migrations: %w", err)140}141if dbLatest != migration {142return fmt.Errorf("expected migration %s, but found %s", migration, dbLatest)143}144145log.WithField("want", migration).WithField("got", dbLatest).Info("migrated")146147return nil148}149150func init() {151rootCmd.AddCommand(databaseCmd)152153databaseCmd.Flags().StringP("host", "H", os.Getenv("DB_HOST"), "Host to try and connect to")154databaseCmd.Flags().StringP("port", "p", envOrDefault("DB_PORT", "3306"), "Port to connect on")155databaseCmd.Flags().StringP("password", "P", os.Getenv("DB_PASSWORD"), "Password to use when connecting")156databaseCmd.Flags().StringP("username", "u", envOrDefault("DB_USERNAME", "gitpod"), "Username to use when connected")157databaseCmd.Flags().StringP("caCert", "", os.Getenv("DB_CA_CERT"), "Custom CA cert (chain) to use when connected")158159databaseCmd.Flags().BoolP("migration-check", "", false, "Enable to check if the latest migration has been applied")160}161162163