-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker.go
More file actions
106 lines (94 loc) · 2.81 KB
/
Copy pathdocker.go
File metadata and controls
106 lines (94 loc) · 2.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package cli
import (
"context"
"fmt"
"net/url"
"os/exec"
"strings"
"time"
"github.com/dvflw/mantle/internal/config"
"github.com/dvflw/mantle/internal/db"
"github.com/dvflw/mantle/internal/dbdefaults"
)
// dockerRunArgs returns the arguments for `docker run` to start a Postgres
// container matching Mantle's default configuration.
func dockerRunArgs() []string {
return []string{
"run", "-d",
"--name", dbdefaults.ContainerName,
"-p", "5432:5432",
"-e", "POSTGRES_USER=" + dbdefaults.User,
"-e", "POSTGRES_PASSWORD=" + dbdefaults.Password,
"-e", "POSTGRES_DB=" + dbdefaults.Database,
"-v", "mantle-pgdata:/var/lib/postgresql/data",
dbdefaults.PostgresImage,
}
}
// parseHostFromURL extracts the hostname from a Postgres connection URL.
func parseHostFromURL(rawURL string) string {
if rawURL == "" {
return ""
}
parsed, err := url.Parse(rawURL)
if err != nil {
return ""
}
return parsed.Hostname()
}
// dockerAvailable checks whether the Docker CLI is installed and the daemon is responsive.
func dockerAvailable() bool {
cmd := exec.Command("docker", "info")
return cmd.Run() == nil
}
// dockerContainerStatus returns "running", "exited", or "" (not found)
// for the mantle-postgres container.
func dockerContainerStatus() string {
out, err := exec.Command("docker", "inspect", "-f", "{{.State.Status}}", dbdefaults.ContainerName).Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
// dockerRemoveContainer removes the mantle-postgres container (stopped or otherwise).
func dockerRemoveContainer() error {
return exec.Command("docker", "rm", "-f", dbdefaults.ContainerName).Run()
}
// dockerStartPostgres starts a new Postgres container and waits for it to accept connections.
func dockerStartPostgres(cfg config.DatabaseConfig) error {
// Handle existing container.
switch dockerContainerStatus() {
case "running":
// Already running — just wait for readiness.
return waitForPostgres(cfg)
case "exited", "created", "dead":
_ = dockerRemoveContainer()
}
args := dockerRunArgs()
out, err := exec.Command("docker", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("docker run failed: %w\n%s", err, string(out))
}
return waitForPostgres(cfg)
}
// waitForPostgres polls db.Open with backoff until the database accepts connections
// or the timeout (~15s) is exceeded.
func waitForPostgres(cfg config.DatabaseConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
delay := 500 * time.Millisecond
for {
database, err := db.Open(cfg)
if err == nil {
database.Close()
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("container started but Postgres isn't accepting connections after 15s: %w", err)
case <-time.After(delay):
if delay < 2*time.Second {
delay *= 2
}
}
}
}