Skip to content

Commit 73a3da5

Browse files
committed
Make db waiter to wait latest migration
1 parent 667bd2f commit 73a3da5

File tree

8 files changed

+130
-47
lines changed

8 files changed

+130
-47
lines changed

components/gitpod-db/BUILD.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ packages:
9696
image:
9797
- ${imageRepoBase}/db-migrations:${version}
9898
- ${imageRepoBase}/db-migrations:commit-${__git_commit}
99+
- name: latest-migration
100+
type: generic
101+
deps:
102+
- :lib
103+
config:
104+
commands:
105+
- ["sh", "-c", "node components-gitpod-db--lib/package/lib/typeorm/print-migration.js > latest-migration.json"]
106+
- ["sh", "-c", "rm -r components*"]
99107
scripts:
100108
- name: init-testdb
101109
description: "Starts a properly initialized MySQL instance to run tests against. Usage: '. $(leeway run components/gitpod-db:init-testdb)'"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as fs from "node:fs";
8+
9+
// list migration folder files and find the latest one
10+
const files = fs.readdirSync(__dirname + "/migration");
11+
const latest = files
12+
.filter((f) => f.endsWith(".js") && f.includes("-"))
13+
.sort()
14+
.pop();
15+
16+
if (!latest) {
17+
console.log(JSON.stringify({ name: "", timestamp: "" }));
18+
} else {
19+
const [timestamp, name] = latest.split("-");
20+
console.log(JSON.stringify({ name, timestamp }));
21+
}

components/service-waiter/cmd/database.go

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
package cmd
66

77
import (
8+
"context"
89
"crypto/tls"
910
"crypto/x509"
1011
"database/sql"
12+
"fmt"
1113
"net"
1214
"os"
1315
"strings"
@@ -20,6 +22,13 @@ import (
2022
"github.com/gitpod-io/gitpod/common-go/log"
2123
)
2224

25+
const migrationTableName = "migrations"
26+
27+
type migrationInfo struct {
28+
Name string
29+
Timestamp string
30+
}
31+
2332
// databaseCmd represents the database command
2433
var databaseCmd = &cobra.Command{
2534
Use: "database",
@@ -34,6 +43,10 @@ DB_CA_CERT and DB_USER(=gitpod)`,
3443
}
3544
},
3645
Run: func(cmd *cobra.Command, args []string) {
46+
timeout := getTimeout()
47+
ctx, cancel := context.WithTimeout(cmd.Context(), timeout)
48+
defer cancel()
49+
3750
cfg := mysql.NewConfig()
3851
cfg.Addr = net.JoinHostPort(viper.GetString("host"), viper.GetString("port"))
3952
cfg.Net = "tcp"
@@ -65,52 +78,71 @@ DB_CA_CERT and DB_USER(=gitpod)`,
6578
cfg.TLSConfig = tlsConfigName
6679
}
6780

68-
timeout := getTimeout()
69-
done := make(chan bool)
70-
go func() {
71-
log.WithField("timeout", timeout.String()).WithField("dsn", censoredDSN).Info("attempting to connect to DB")
72-
for {
73-
db, err := sql.Open("mysql", cfg.FormatDSN())
74-
if err != nil {
75-
continue
76-
}
77-
err = db.Ping()
78-
if err != nil {
79-
if strings.Contains(err.Error(), "Access denied") {
80-
fail("Invalid credentials for the database. Check DB_USERNAME and DB_PASSWORD.")
81-
}
82-
83-
log.WithError(err).Debug("retry")
84-
<-time.After(time.Second)
85-
continue
86-
}
87-
88-
err = db.Close()
89-
if err != nil {
90-
log.WithError(err).Warn("cannot close DB connection used for probing")
91-
}
81+
migration := migrationInfo{
82+
Name: viper.GetString("migration-name"),
83+
Timestamp: viper.GetString("migration-timestamp"),
84+
}
85+
86+
log.WithField("timeout", timeout.String()).WithField("dsn", censoredDSN).WithField("migration", migration).Info("waiting for database")
87+
for {
88+
log.Info("attempting to check if database is available")
89+
if err := checkDbAvailable(ctx, cfg, &migration); err != nil {
90+
log.WithError(err).Debug("retry")
91+
<-time.After(time.Second)
92+
} else {
9293
break
9394
}
94-
95-
done <- true
96-
}()
95+
}
9796

9897
select {
99-
case <-done:
100-
log.Info("database became available")
101-
return
102-
case <-time.After(timeout):
98+
case <-ctx.Done():
10399
log.WithField("timeout", timeout.String()).Fatal("database did not become available in time")
100+
default:
101+
log.Info("database became available")
104102
}
105103
},
106104
}
107105

106+
// checkDbAvailable will connect and check if migrations table contains the latest migration
107+
func checkDbAvailable(ctx context.Context, cfg *mysql.Config, migration *migrationInfo) error {
108+
db, err := sql.Open("mysql", cfg.FormatDSN())
109+
if err != nil {
110+
return err
111+
}
112+
// ignore error
113+
defer db.Close()
114+
115+
// if migration info is not set, just ping the database
116+
if migration.Name == "" || migration.Timestamp == "" {
117+
return db.PingContext(ctx)
118+
}
119+
120+
log.Info("checking if database is migrated")
121+
row := db.QueryRowContext(ctx, "SELECT name FROM "+migrationTableName+" WHERE 1690915807191 = ?", migration.Timestamp)
122+
var name string
123+
if err := row.Scan(&name); err != nil {
124+
return err
125+
}
126+
// migration name should be `{migrationName}{timestamp}`. i.e. AddGitStatusColumnToWorkspaceInstance1690915807191
127+
if name != fmt.Sprintf("%s%s", migration.Name, migration.Timestamp) {
128+
log.WithError(err).WithField("latest", name).WithField("checking", migration).Error("migrations table does not contain the latest migration")
129+
return err
130+
}
131+
132+
return nil
133+
}
134+
108135
func init() {
109136
rootCmd.AddCommand(databaseCmd)
110137

138+
// Database config
111139
databaseCmd.Flags().StringP("host", "H", os.Getenv("DB_HOST"), "Host to try and connect to")
112140
databaseCmd.Flags().StringP("port", "p", envOrDefault("DB_PORT", "3306"), "Port to connect on")
113141
databaseCmd.Flags().StringP("password", "P", os.Getenv("DB_PASSWORD"), "Password to use when connecting")
114142
databaseCmd.Flags().StringP("username", "u", envOrDefault("DB_USERNAME", "gitpod"), "Username to use when connected")
115143
databaseCmd.Flags().StringP("caCert", "", os.Getenv("DB_CA_CERT"), "Custom CA cert (chain) to use when connected")
144+
145+
// Database migration config
146+
databaseCmd.Flags().StringP("migration-name", "", os.Getenv("DB_MIGRATION_NAME"), "The latest migration name")
147+
databaseCmd.Flags().StringP("migration-timestamp", "", os.Getenv("DB_MIGRATION_TIMESTAMP"), "The latest migration timestamp")
116148
}

components/service-waiter/cmd/root.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,6 @@ func getTimeout() time.Duration {
100100
return timeout
101101
}
102102

103-
// fail ends the waiting process propagating the message on its way out
104-
func fail(message string) {
105-
terminationLog := "/dev/termination-log"
106-
107-
log.WithField("message", message).Warn("failed to wait for a service")
108-
109-
if _, err := os.Stat(terminationLog); !os.IsNotExist(err) {
110-
err := os.WriteFile(terminationLog, []byte(message), 0600)
111-
if err != nil {
112-
log.WithError(err).Error("cannot write termination log")
113-
}
114-
}
115-
116-
os.Exit(1)
117-
}
118-
119103
func envOrDefault(env, def string) (res string) {
120104
res = os.Getenv(env)
121105
if res == "" {

install/installer/BUILD.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ packages:
1919
- components/blobserve:lib
2020
- components/common-go:lib
2121
- components/gitpod-db/go:lib
22+
- components/gitpod-db:latest-migration
2223
- components/content-service-api/go:lib
2324
- components/content-service:lib
2425
- components/ee/agent-smith:lib
@@ -46,6 +47,7 @@ packages:
4647
- ["sh", "-c", "ls -d third_party/charts/*/ | while read f; do echo \"cd $f && helm dep up && cd -\"; done | sh"]
4748
- ["mv", "_deps/components-ws-manager-mk2--crd/workspace.gitpod.io_workspaces.yaml", "pkg/components/ws-manager-mk2/crd.yaml"]
4849
- ["sh", "-c", "cat _deps/components-ws-manager-mk2--crd/workspace.gitpod.io_snapshots.yaml >> pkg/components/ws-manager-mk2/crd.yaml"]
50+
- ["sh", "-c", "cat _deps/components-gitpod-db--latest-migration/latest-migration.txt > pkg/common/latest-migration.txt"]
4951
config:
5052
packaging: app
5153
buildCommand: ["go", "build", "-trimpath", "-ldflags", "-buildid= -w -s -X 'github.com/gitpod-io/gitpod/installer/cmd.Version=commit-${__git_commit}'"]

install/installer/pkg/common/common.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package common
66

77
import (
88
"crypto/sha256"
9+
_ "embed"
910
"encoding/json"
1011
"fmt"
1112
"io"
@@ -446,13 +447,29 @@ func ConfigcatProxyEnv(ctx *RenderContext) []corev1.EnvVar {
446447
return envs
447448
}
448449

450+
//go:embed resources/latest-migration.json
451+
var LatestMigrationInfo []byte
452+
449453
func DatabaseWaiterContainer(ctx *RenderContext) *corev1.Container {
454+
type migrationInfo struct {
455+
Name string `json:"name,omitempty"`
456+
Timestamp string `json:"timestamp,omitempty"`
457+
}
458+
var migration migrationInfo
459+
if err := json.Unmarshal(LatestMigrationInfo, &migration); err != nil {
460+
panic(fmt.Sprintf("cannot unmarshal latest migration info: %v", err))
461+
}
462+
450463
return &corev1.Container{
451464
Name: "database-waiter",
452465
Image: ctx.ImageName(ctx.Config.Repository, "service-waiter", ctx.VersionManifest.Components.ServiceWaiter.Version),
453466
Args: []string{
454467
"-v",
455468
"database",
469+
"--migration-name",
470+
migration.Name,
471+
"--migration-timestamp",
472+
migration.Timestamp,
456473
},
457474
SecurityContext: &corev1.SecurityContext{
458475
Privileged: pointer.Bool(false),

install/installer/pkg/common/common_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package common_test
66

77
import (
8+
"encoding/json"
89
"fmt"
910
"testing"
1011

@@ -46,3 +47,20 @@ func TestKubeRBACProxyContainerWithConfig(t *testing.T) {
4647
{Name: baseserver.BuiltinMetricsPortName, ContainerPort: baseserver.BuiltinMetricsPort},
4748
}, container.Ports)
4849
}
50+
51+
func TestDatabaseWaiterContainer2(t *testing.T) {
52+
t.Run("helloaaaaaaaa", func(t *testing.T) {
53+
type data struct {
54+
Name string `json:"name,omitempty"`
55+
Timestamp string `json:"timestamp,omitempty"`
56+
}
57+
d := data{}
58+
json.Unmarshal(common.LatestMigrationInfo, &d)
59+
if d.Name != "AddGitStatusColumnToWorkspaceInstance" {
60+
t.Errorf("DatabaseWaiterContainer() = %v, want %v", d.Name, "AddGitStatusColumnToWorkspaceInstance")
61+
}
62+
if d.Timestamp != "1690915807191" {
63+
t.Errorf("DatabaseWaiterContainer() = %v, want %v", d.Timestamp, "1690915807191")
64+
}
65+
})
66+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

0 commit comments

Comments
 (0)