Skip to content

Commit d87acd4

Browse files
FEATURE: add runtime features (#849)
adds commands for: start, run, stop, cleanup, destroy, logs, enter, restart, rebuild -- carrying over existing run commands from launcher1. Rebuild will also do its best to minimize downtime with the following steps: * Detect if Discourse is running as a single container or external DB * Detect if db:migrate is configured to run on container boot * Build initial container (keeping existing one online) * Exit running containers if it's a single container (otherwise keeps existing online) * Run migrations * Defer migrations if db:migrate is configured to run on container boot * Run migrations with SKIP_POST_DEPLOYMENT_MIGRATIONS=1 if it's a 2 container setup * Otherwise, run all migrations * Destroy the old container (finally stopping the current, if it's still up here) * Start the new container * Run post-deploy migrations * Run migrations with SKIP_POST_DEPLOYMENT_MIGRATIONS=0 if it's a 2 container setup
1 parent de17bef commit d87acd4

File tree

5 files changed

+618
-0
lines changed

5 files changed

+618
-0
lines changed

launcher_go/v2/cli_build.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type DockerConfigureCmd struct {
6464

6565
func (r *DockerConfigureCmd) Run(cli *Cli, ctx *context.Context) error {
6666
config, err := config.LoadConfig(cli.ConfDir, r.Config, true, cli.TemplatesDir)
67+
6768
if err != nil {
6869
return errors.New("YAML syntax error. Please check your containers/*.yml config files.")
6970
}

launcher_go/v2/cli_runtime.go

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"errors"
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"runtime"
11+
"strings"
12+
"syscall"
13+
"time"
14+
15+
"github.com/discourse/discourse_docker/launcher_go/v2/config"
16+
"github.com/discourse/discourse_docker/launcher_go/v2/docker"
17+
"github.com/discourse/discourse_docker/launcher_go/v2/utils"
18+
19+
"golang.org/x/sys/unix"
20+
)
21+
22+
/*
23+
* start
24+
* run
25+
* stop
26+
* cleanup
27+
* destroy
28+
* logs
29+
* enter
30+
* rebuild
31+
* restart
32+
*/
33+
34+
type StartCmd struct {
35+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
36+
DryRun bool `name:"dry-run" short:"n" help:"Do not start, print docker start command and exit."`
37+
DockerArgs string `name:"docker-args" help:"Extra arguments to pass when running docker."`
38+
RunImage string `name:"run-image" help:"Start with a custom image."`
39+
Supervised bool `name:"supervised" env:"SUPERVISED" help:"Attach the running container on start."`
40+
41+
extraEnv []string
42+
}
43+
44+
func (r *StartCmd) Run(cli *Cli, ctx *context.Context) error {
45+
//start stopped container first if exists
46+
running, _ := docker.ContainerRunning(r.Config)
47+
48+
if running && !r.DryRun {
49+
fmt.Fprintln(utils.Out, "Nothing to do, your container has already started!")
50+
return nil
51+
}
52+
53+
exists, _ := docker.ContainerExists(r.Config)
54+
55+
if exists && !r.DryRun {
56+
fmt.Fprintln(utils.Out, "starting up existing container")
57+
cmd := exec.CommandContext(*ctx, utils.DockerPath, "start", r.Config)
58+
59+
if r.Supervised {
60+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
61+
62+
cmd.Cancel = func() error {
63+
if runtime.GOOS == "darwin" {
64+
runCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
65+
stopCmd := exec.CommandContext(runCtx, utils.DockerPath, "stop", r.Config)
66+
utils.CmdRunner(stopCmd).Run()
67+
cancel()
68+
}
69+
return unix.Kill(-cmd.Process.Pid, unix.SIGINT)
70+
}
71+
72+
cmd.Args = append(cmd.Args, "--attach")
73+
cmd.Stdin = os.Stdin
74+
cmd.Stdout = os.Stdout
75+
cmd.Stderr = os.Stderr
76+
}
77+
78+
fmt.Fprintln(utils.Out, cmd)
79+
80+
if err := utils.CmdRunner(cmd).Run(); err != nil {
81+
return err
82+
}
83+
84+
return nil
85+
}
86+
87+
config, err := config.LoadConfig(cli.ConfDir, r.Config, true, cli.TemplatesDir)
88+
89+
if err != nil {
90+
return errors.New("YAML syntax error. Please check your containers/*.yml config files.")
91+
}
92+
93+
defaultHostname, _ := os.Hostname()
94+
defaultHostname = defaultHostname + "-" + r.Config
95+
hostname := config.DockerHostname(defaultHostname)
96+
97+
restart := true
98+
detatch := true
99+
100+
if r.Supervised {
101+
restart = false
102+
detatch = false
103+
}
104+
105+
extraFlags := strings.Fields(r.DockerArgs)
106+
bootCmd := config.BootCommand()
107+
108+
runner := docker.DockerRunner{
109+
Config: config,
110+
Ctx: ctx,
111+
ContainerId: r.Config,
112+
DryRun: r.DryRun,
113+
CustomImage: r.RunImage,
114+
Restart: restart,
115+
Detatch: detatch,
116+
ExtraFlags: extraFlags,
117+
ExtraEnv: r.extraEnv,
118+
Hostname: hostname,
119+
Cmd: []string{bootCmd},
120+
}
121+
122+
fmt.Fprintln(utils.Out, "starting new container...")
123+
return runner.Run()
124+
}
125+
126+
type RunCmd struct {
127+
RunImage string `name:"run-image" help:"Override the image used for running the container."`
128+
DockerArgs string `name:"docker-args" help:"Extra arguments to pass when running docker"`
129+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
130+
Cmd []string `arg:"" help:"command to run" passthrough:""`
131+
}
132+
133+
func (r *RunCmd) Run(cli *Cli, ctx *context.Context) error {
134+
config, err := config.LoadConfig(cli.ConfDir, r.Config, true, cli.TemplatesDir)
135+
if err != nil {
136+
return errors.New("YAML syntax error. Please check your containers/*.yml config files.")
137+
}
138+
extraFlags := strings.Fields(r.DockerArgs)
139+
runner := docker.DockerRunner{
140+
Config: config,
141+
Ctx: ctx,
142+
CustomImage: r.RunImage,
143+
SkipPorts: true,
144+
Rm: true,
145+
Cmd: r.Cmd,
146+
ExtraFlags: extraFlags,
147+
}
148+
return runner.Run()
149+
return nil
150+
}
151+
152+
type StopCmd struct {
153+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
154+
}
155+
156+
func (r *StopCmd) Run(cli *Cli, ctx *context.Context) error {
157+
exists, _ := docker.ContainerExists(r.Config)
158+
if !exists {
159+
fmt.Fprintln(utils.Out, r.Config+" was not found")
160+
return nil
161+
}
162+
cmd := exec.CommandContext(*ctx, utils.DockerPath, "stop", "--time", "600", r.Config)
163+
164+
fmt.Fprintln(utils.Out, cmd)
165+
if err := utils.CmdRunner(cmd).Run(); err != nil {
166+
return err
167+
}
168+
return nil
169+
}
170+
171+
type RestartCmd struct {
172+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
173+
DockerArgs string `name:"docker-args" help:"Extra arguments to pass when running docker."`
174+
RunImage string `name:"run-image" help:"Override the image used for running the container."`
175+
}
176+
177+
func (r *RestartCmd) Run(cli *Cli, ctx *context.Context) error {
178+
start := StartCmd{Config: r.Config, DockerArgs: r.DockerArgs, RunImage: r.RunImage}
179+
stop := StopCmd{Config: r.Config}
180+
181+
if err := stop.Run(cli, ctx); err != nil {
182+
return err
183+
}
184+
185+
if err := start.Run(cli, ctx); err != nil {
186+
return err
187+
}
188+
189+
return nil
190+
}
191+
192+
type DestroyCmd struct {
193+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
194+
}
195+
196+
func (r *DestroyCmd) Run(cli *Cli, ctx *context.Context) error {
197+
exists, _ := docker.ContainerExists(r.Config)
198+
199+
if !exists {
200+
fmt.Fprintln(utils.Out, r.Config+" was not found")
201+
return nil
202+
}
203+
204+
cmd := exec.CommandContext(*ctx, utils.DockerPath, "stop", "--time", "600", r.Config)
205+
fmt.Fprintln(utils.Out, cmd)
206+
207+
if err := utils.CmdRunner(cmd).Run(); err != nil {
208+
return err
209+
}
210+
211+
cmd = exec.CommandContext(*ctx, utils.DockerPath, "rm", r.Config)
212+
fmt.Fprintln(utils.Out, cmd)
213+
214+
if err := utils.CmdRunner(cmd).Run(); err != nil {
215+
return err
216+
}
217+
218+
return nil
219+
}
220+
221+
type EnterCmd struct {
222+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
223+
}
224+
225+
func (r *EnterCmd) Run(cli *Cli, ctx *context.Context) error {
226+
cmd := exec.CommandContext(*ctx, utils.DockerPath, "exec", "-it", r.Config, "/bin/bash", "--login")
227+
cmd.Stdin = os.Stdin
228+
cmd.Stdout = os.Stdout
229+
cmd.Stderr = os.Stderr
230+
231+
if err := utils.CmdRunner(cmd).Run(); err != nil {
232+
return err
233+
}
234+
235+
return nil
236+
}
237+
238+
type LogsCmd struct {
239+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
240+
}
241+
242+
func (r *LogsCmd) Run(cli *Cli, ctx *context.Context) error {
243+
cmd := exec.CommandContext(*ctx, utils.DockerPath, "logs", r.Config)
244+
output, err := utils.CmdRunner(cmd).Output()
245+
246+
if err != nil {
247+
return err
248+
}
249+
250+
fmt.Fprintln(utils.Out, string(output[:]))
251+
return nil
252+
}
253+
254+
type RebuildCmd struct {
255+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
256+
FullBuild bool `name:"full-build" help:"Run a full build image even when migrate on boot and precompile on boot are present in the config. Saves a fully built image with environment baked in. Without this flag, if MIGRATE_ON_BOOT is set in config it will defer migration until container start, and if PRECOMPILE_ON_BOOT is set in the config, it will defer configure step until container start."`
257+
Clean bool `help:"also runs clean"`
258+
}
259+
260+
func (r *RebuildCmd) Run(cli *Cli, ctx *context.Context) error {
261+
config, err := config.LoadConfig(cli.ConfDir, r.Config, true, cli.TemplatesDir)
262+
263+
if err != nil {
264+
return errors.New("YAML syntax error. Please check your containers/*.yml config files.")
265+
}
266+
267+
// if we're not in an all-in-one setup, we can run migrations while the app is running
268+
externalDb := config.Env["DISCOURSE_DB_SOCKET"] == "" && config.Env["DISCOURSE_DB_HOST"] != ""
269+
270+
build := DockerBuildCmd{Config: r.Config}
271+
configure := DockerConfigureCmd{Config: r.Config}
272+
stop := StopCmd{Config: r.Config}
273+
destroy := DestroyCmd{Config: r.Config}
274+
clean := CleanupCmd{}
275+
extraEnv := []string{}
276+
277+
if err := build.Run(cli, ctx); err != nil {
278+
return err
279+
}
280+
281+
if !externalDb {
282+
if err := stop.Run(cli, ctx); err != nil {
283+
return err
284+
}
285+
}
286+
287+
_, migrateOnBoot := config.Env["MIGRATE_ON_BOOT"]
288+
289+
if !migrateOnBoot || r.FullBuild {
290+
migrate := DockerMigrateCmd{Config: r.Config}
291+
292+
if externalDb {
293+
// defer post deploy migrations until after reboot
294+
migrate.SkipPostDeploymentMigrations = true
295+
}
296+
297+
if err := migrate.Run(cli, ctx); err != nil {
298+
return err
299+
}
300+
301+
extraEnv = append(extraEnv, "MIGRATE_ON_BOOT=0")
302+
}
303+
304+
_, precompileOnBoot := config.Env["PRECOMPILE_ON_BOOT"]
305+
306+
if !precompileOnBoot || r.FullBuild {
307+
if err := configure.Run(cli, ctx); err != nil {
308+
return err
309+
}
310+
311+
extraEnv = append(extraEnv, "PRECOMPILE_ON_BOOT=0")
312+
}
313+
314+
if err := destroy.Run(cli, ctx); err != nil {
315+
return err
316+
}
317+
318+
start := StartCmd{Config: r.Config, extraEnv: extraEnv}
319+
320+
if err := start.Run(cli, ctx); err != nil {
321+
return err
322+
}
323+
324+
// run post deploy migrations since we've rebooted
325+
if externalDb {
326+
migrate := DockerMigrateCmd{Config: r.Config}
327+
if err := migrate.Run(cli, ctx); err != nil {
328+
return err
329+
}
330+
}
331+
332+
if r.Clean {
333+
if err := clean.Run(cli, ctx); err != nil {
334+
return err
335+
}
336+
}
337+
338+
return nil
339+
}
340+
341+
type CleanupCmd struct{}
342+
343+
func (r *CleanupCmd) Run(cli *Cli, ctx *context.Context) error {
344+
cmd := exec.CommandContext(*ctx, utils.DockerPath, "container", "prune", "--filter", "until=1h")
345+
346+
if err := utils.CmdRunner(cmd).Run(); err != nil {
347+
return err
348+
}
349+
350+
cmd = exec.CommandContext(*ctx, utils.DockerPath, "image", "prune", "--all", "--filter", "until=1h")
351+
352+
if err := utils.CmdRunner(cmd).Run(); err != nil {
353+
return err
354+
}
355+
356+
_, err := os.Stat("/var/discourse/shared/standalone/postgres_data_old")
357+
358+
if !os.IsNotExist(err) {
359+
fmt.Fprintln(utils.Out, "Old PostgreSQL backup data cluster detected")
360+
fmt.Fprintln(utils.Out, "Would you like to remove it? (y/N)")
361+
scanner := bufio.NewScanner(os.Stdin)
362+
scanner.Scan()
363+
reply := scanner.Text()
364+
if reply == "y" || reply == "Y" {
365+
fmt.Fprintln(utils.Out, "removing old PostgreSQL data cluster at /var/discourse/shared/standalone/postgres_data_old...")
366+
os.RemoveAll("/var/discourse/shared/standalone/postgres_data_old")
367+
} else {
368+
return errors.New("Cancelled")
369+
}
370+
}
371+
372+
return nil
373+
}

0 commit comments

Comments
 (0)