Skip to content

Commit 90f7fb2

Browse files
authored
Merge pull request #202 from fly-apps/barman
2 parents e017993 + ae2bd87 commit 90f7fb2

File tree

11 files changed

+495
-6
lines changed

11 files changed

+495
-6
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ LABEL fly.pg-version=${PG_VERSION}
3030
LABEL fly.pg-manager=repmgr
3131

3232
RUN apt-get update && apt-get install --no-install-recommends -y \
33-
ca-certificates iproute2 postgresql-$PG_MAJOR_VERSION-repmgr curl bash dnsutils vim socat procps ssh gnupg \
33+
ca-certificates iproute2 postgresql-$PG_MAJOR_VERSION-repmgr curl bash dnsutils vim socat procps ssh gnupg rsync barman-cli barman cron \
3434
&& apt autoremove -y
3535

3636
# PostGIS

Dockerfile-timescaledb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ LABEL fly.pg-version=${PG_VERSION}
3131
LABEL fly.pg-manager=repmgr
3232

3333
RUN apt-get update && apt-get install --no-install-recommends -y \
34-
ca-certificates iproute2 postgresql-$PG_MAJOR_VERSION-repmgr curl bash dnsutils vim haproxy socat procps ssh gnupg \
34+
ca-certificates iproute2 postgresql-$PG_MAJOR_VERSION-repmgr curl bash dnsutils vim haproxy socat procps ssh gnupg rsync barman-cli barman cron \
3535
&& apt autoremove -y
3636

3737
RUN echo "deb https://packagecloud.io/timescale/timescaledb/debian/ $(cat /etc/os-release | grep VERSION_CODENAME | cut -d'=' -f2) main" > /etc/apt/sources.list.d/timescaledb.list \

cmd/start/main.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"syscall"
99
"time"
1010

11+
"github.com/fly-apps/postgres-flex/internal/flybarman"
1112
"github.com/fly-apps/postgres-flex/internal/flypg"
1213
"github.com/fly-apps/postgres-flex/internal/supervisor"
1314
)
@@ -22,6 +23,36 @@ func main() {
2223
}
2324
}
2425

26+
if os.Getenv("IS_BARMAN") != "" {
27+
node, err := flybarman.NewNode()
28+
if err != nil {
29+
panicHandler(err)
30+
return
31+
}
32+
33+
ctx := context.Background()
34+
35+
if err = node.Init(ctx); err != nil {
36+
panicHandler(err)
37+
return
38+
}
39+
40+
svisor := supervisor.New("flybarman", 1*time.Minute)
41+
svisor.AddProcess("barman", fmt.Sprintf("tail -f %s", node.LogFile))
42+
svisor.AddProcess("admin", "/usr/local/bin/start_admin_server",
43+
supervisor.WithRestart(0, 5*time.Second),
44+
)
45+
46+
svisor.StopOnSignal(syscall.SIGINT, syscall.SIGTERM)
47+
48+
if err := svisor.Run(); err != nil {
49+
fmt.Println(err)
50+
os.Exit(1)
51+
}
52+
53+
return
54+
}
55+
2556
node, err := flypg.NewNode()
2657
if err != nil {
2758
panicHandler(err)

internal/flybarman/node.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package flybarman
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"os/exec"
9+
10+
"github.com/fly-apps/postgres-flex/internal/flypg"
11+
"github.com/fly-apps/postgres-flex/internal/flypg/admin"
12+
)
13+
14+
var (
15+
dataDir = "/data"
16+
barmanConfigFile = dataDir + "/barman.conf"
17+
barmanCronFile = dataDir + "/barman.cron"
18+
globalBarmanConfigFile = "/etc/barman.conf"
19+
barmanHome = dataDir + "/barman.d"
20+
logFile = dataDir + "/barman.log"
21+
passwordConfigPath = "/root/.pgpass"
22+
rootPasswordConfigPath = "/.pgpass"
23+
)
24+
25+
type Node struct {
26+
AppName string
27+
PrivateIP string
28+
PrimaryRegion string
29+
DataDir string
30+
Port int
31+
32+
BarmanConfigFile string
33+
BarmanCronFile string
34+
GlobalBarmanConfigFile string
35+
BarmanHome string
36+
LogFile string
37+
PasswordConfigPath string
38+
RootPasswordConfigPath string
39+
40+
SUCredentials admin.Credential
41+
OperatorCredentials admin.Credential
42+
ReplCredentials admin.Credential
43+
}
44+
45+
func NewNode() (*Node, error) {
46+
node := &Node{
47+
AppName: "local",
48+
BarmanConfigFile: barmanConfigFile,
49+
BarmanCronFile: barmanCronFile,
50+
GlobalBarmanConfigFile: globalBarmanConfigFile,
51+
BarmanHome: barmanHome,
52+
LogFile: logFile,
53+
PasswordConfigPath: passwordConfigPath,
54+
RootPasswordConfigPath: rootPasswordConfigPath,
55+
}
56+
57+
if appName := os.Getenv("FLY_APP_NAME"); appName != "" {
58+
node.AppName = appName
59+
}
60+
61+
// Internal user
62+
node.SUCredentials = admin.Credential{
63+
Username: "flypgadmin",
64+
Password: os.Getenv("SU_PASSWORD"),
65+
}
66+
67+
// Postgres user
68+
node.OperatorCredentials = admin.Credential{
69+
Username: "postgres",
70+
Password: os.Getenv("OPERATOR_PASSWORD"),
71+
}
72+
73+
// Repmgr user
74+
node.ReplCredentials = admin.Credential{
75+
Username: "repmgr",
76+
Password: os.Getenv("REPL_PASSWORD"),
77+
}
78+
79+
return node, nil
80+
}
81+
82+
func (n *Node) Init(_ context.Context) error {
83+
if os.Getenv("UNIT_TESTING") == "" {
84+
err := flypg.WriteSSHKey()
85+
if err != nil {
86+
return fmt.Errorf("failed write ssh keys: %s", err)
87+
}
88+
}
89+
90+
if _, err := os.Stat(n.BarmanConfigFile); os.IsNotExist(err) {
91+
barmanConfigFileContent := fmt.Sprintf(`[barman]
92+
barman_user = root
93+
barman_home = /data/barman.d
94+
log_level = info
95+
log_file = /data/barman.log
96+
97+
[pg]
98+
description = "Fly.io Postgres Cluster"
99+
conninfo = host=%s.internal user=repmgr dbname=postgres
100+
streaming_conninfo = host=%s.internal user=repmgr dbname=postgres
101+
backup_method = postgres
102+
streaming_archiver = on
103+
slot_name = barman
104+
create_slot = auto
105+
retention_policy_mode = auto
106+
retention_policy = RECOVERY WINDOW OF 7 days
107+
wal_retention_policy = main
108+
`, n.AppName, n.AppName)
109+
110+
if err := os.WriteFile(n.BarmanConfigFile, []byte(barmanConfigFileContent), 0644); err != nil {
111+
return fmt.Errorf("failed write %s: %s", n.BarmanConfigFile, err)
112+
}
113+
114+
log.Println(n.BarmanConfigFile + " created successfully.")
115+
}
116+
117+
if err := n.deleteGlobalBarmanFile(); err != nil {
118+
return fmt.Errorf("failed delete /etc/barman.conf: %s", err)
119+
}
120+
121+
if err := os.Symlink(n.BarmanConfigFile, n.GlobalBarmanConfigFile); err != nil {
122+
return fmt.Errorf("failed symlink %s to %s: %s", n.BarmanConfigFile, n.GlobalBarmanConfigFile, err)
123+
}
124+
125+
log.Println("Symbolic link to barman config created successfully.")
126+
127+
if err := os.MkdirAll(n.BarmanHome, os.ModePerm); err != nil {
128+
return fmt.Errorf("failed to mkdir %s: %s", n.BarmanHome, err)
129+
}
130+
131+
log.Println("Barman home directory successfully.")
132+
133+
passStr := fmt.Sprintf("*:*:*:%s:%s", n.ReplCredentials.Username, n.ReplCredentials.Password)
134+
if err := os.WriteFile(n.PasswordConfigPath, []byte(passStr), 0700); err != nil {
135+
return fmt.Errorf("failed to write file %s: %s", n.PasswordConfigPath, err)
136+
}
137+
// We need this in case the user ssh to the vm as root
138+
if err := os.WriteFile(n.RootPasswordConfigPath, []byte(passStr), 0700); err != nil {
139+
return fmt.Errorf("failed to write file %s: %s", n.RootPasswordConfigPath, err)
140+
}
141+
142+
if _, err := os.Stat(n.BarmanCronFile); os.IsNotExist(err) {
143+
barmanCronFileContent := `* * * * * /usr/bin/barman cron
144+
`
145+
if err := os.WriteFile(n.BarmanCronFile, []byte(barmanCronFileContent), 0644); err != nil {
146+
return fmt.Errorf("failed write %s: %s", n.BarmanCronFile, err)
147+
}
148+
149+
log.Println(n.BarmanCronFile + " created successfully.")
150+
}
151+
152+
if _, err := os.Stat(n.LogFile); os.IsNotExist(err) {
153+
file, err := os.Create(n.LogFile)
154+
if err != nil {
155+
return fmt.Errorf("failed to touch %s: %s", n.LogFile, err)
156+
}
157+
defer func() { _ = file.Close() }()
158+
159+
log.Println(n.LogFile + " created successfully.")
160+
}
161+
162+
if os.Getenv("UNIT_TESTING") == "" {
163+
crontabCommand := exec.Command("/usr/bin/crontab", n.BarmanCronFile)
164+
if _, err := crontabCommand.Output(); err != nil {
165+
return fmt.Errorf("failed set crontab: %s", err)
166+
}
167+
168+
log.Println("Crontab updated")
169+
170+
serviceCmd := exec.Command("/usr/sbin/service", "--version")
171+
if err := serviceCmd.Run(); err != nil {
172+
log.Println("service command not found, skipping initializing cron service")
173+
} else {
174+
serviceCronStartCommand := exec.Command("service", "cron", "start")
175+
if _, err := serviceCronStartCommand.Output(); err != nil {
176+
return fmt.Errorf("failed starting cron service: %s", err)
177+
}
178+
log.Println("Started cron service")
179+
}
180+
181+
switchWalCommand := exec.Command("barman", "switch-wal", "--archive", "--force", "pg")
182+
if _, err := switchWalCommand.Output(); err != nil {
183+
log.Println(fmt.Errorf("failed switching WAL: %s", err))
184+
log.Println("try running `barman switch-wal --archive --force pg` or wait for the next WAL")
185+
} else {
186+
log.Println("successfully switched WAL files to start barman")
187+
}
188+
189+
cronCommand := exec.Command("barman", "cron")
190+
if _, err := cronCommand.Output(); err != nil {
191+
log.Println(fmt.Errorf("failed running barman cron: %s", err))
192+
log.Println("try running `cronCommand` or wait for the next run")
193+
} else {
194+
log.Println("successfully ran `barman cron`")
195+
}
196+
}
197+
198+
return nil
199+
}
200+
201+
func (n *Node) deleteGlobalBarmanFile() error {
202+
if _, err := os.Stat(n.GlobalBarmanConfigFile); os.IsNotExist(err) {
203+
return nil
204+
}
205+
206+
if err := os.Remove(n.GlobalBarmanConfigFile); err != nil {
207+
return err
208+
}
209+
210+
log.Println(n.GlobalBarmanConfigFile + " deleted successfully")
211+
return nil
212+
}

0 commit comments

Comments
 (0)