Skip to content

Commit d3ae996

Browse files
FEATURE: add configure command (#841)
Add 'configure' command - If run after the "build" command, this is equivalent to today's 'bootstrap' command. Note that unlike build command, a docker run+commit pattern needs to be used here as this requires a running database + mounted volumes.
1 parent 0808c17 commit d3ae996

File tree

10 files changed

+365
-7
lines changed

10 files changed

+365
-7
lines changed

launcher_go/v2/cli_build.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"github.com/discourse/discourse_docker/launcher_go/v2/config"
77
"github.com/discourse/discourse_docker/launcher_go/v2/docker"
8+
"github.com/discourse/discourse_docker/launcher_go/v2/utils"
9+
"github.com/google/uuid"
810
"os"
911
"strings"
1012
)
@@ -53,6 +55,29 @@ func (r *DockerBuildCmd) Run(cli *Cli, ctx *context.Context) error {
5355
return nil
5456
}
5557

58+
type DockerConfigureCmd struct {
59+
Tag string `default:"latest" help:"Resulting image tag."`
60+
Config string `arg:"" name:"config" help:"config" predictor:"config"`
61+
}
62+
63+
func (r *DockerConfigureCmd) Run(cli *Cli, ctx *context.Context) error {
64+
config, err := config.LoadConfig(cli.ConfDir, r.Config, true, cli.TemplatesDir)
65+
if err != nil {
66+
return errors.New("YAML syntax error. Please check your containers/*.yml config files.")
67+
}
68+
69+
containerId := "discourse-build-" + uuid.NewString()
70+
pups := docker.DockerPupsRunner{
71+
Config: config,
72+
PupsArgs: "--tags=db,precompile",
73+
SavedImageName: utils.BaseImageName + r.Config + ":" + r.Tag,
74+
ExtraEnv: []string{"SKIP_EMBER_CLI_COMPILE=1"},
75+
Ctx: ctx,
76+
ContainerId: containerId,
77+
}
78+
return pups.Run()
79+
}
80+
5681
type CleanCmd struct {
5782
Config string `arg:"" name:"config" help:"config to clean" predictor:"config"`
5883
}

launcher_go/v2/cli_build_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,50 @@ var _ = Describe("Build", func() {
5757
Expect(buf.String()).ToNot(ContainSubstring("SKIP_EMBER_CLI_COMPILE=1"))
5858
}
5959

60+
var checkConfigureCmd = func(cmd exec.Cmd) {
61+
Expect(cmd.String()).To(ContainSubstring("docker run"))
62+
Expect(cmd.String()).To(ContainSubstring("--env DISCOURSE_DEVELOPER_EMAILS"))
63+
Expect(cmd.String()).To(ContainSubstring("--env SKIP_EMBER_CLI_COMPILE=1"))
64+
// we commit, we need the container to stick around after it is stopped.
65+
Expect(cmd.String()).ToNot(ContainSubstring("--rm"))
66+
67+
// we don't expose ports on configure command
68+
Expect(cmd.String()).ToNot(ContainSubstring("-p 80"))
69+
Expect(cmd.Env).To(ContainElement("DISCOURSE_DB_PASSWORD=SOME_SECRET"))
70+
buf := new(strings.Builder)
71+
io.Copy(buf, cmd.Stdin)
72+
// docker run's stdin is a pups config
73+
Expect(buf.String()).To(ContainSubstring("path: /etc/service/nginx/run"))
74+
}
75+
76+
// commit on configure
77+
var checkConfigureCommit = func(cmd exec.Cmd) {
78+
Expect(cmd.String()).To(ContainSubstring("docker commit"))
79+
Expect(cmd.String()).To(ContainSubstring("--change CMD [\"/sbin/boot\"]"))
80+
Expect(cmd.String()).To(ContainSubstring("discourse-build"))
81+
Expect(cmd.String()).To(ContainSubstring("local_discourse/test"))
82+
Expect(cmd.Env).ToNot(ContainElement("DISCOURSE_DB_PASSWORD=SOME_SECRET"))
83+
}
84+
85+
// configure also cleans up
86+
var checkConfigureClean = func(cmd exec.Cmd) {
87+
Expect(cmd.String()).To(ContainSubstring("docker rm -f discourse-build-"))
88+
}
89+
6090
It("Should run docker build with correct arguments", func() {
6191
runner := ddocker.DockerBuildCmd{Config: "test"}
6292
runner.Run(cli, &ctx)
6393
Expect(len(RanCmds)).To(Equal(1))
6494
checkBuildCmd(RanCmds[0])
6595
})
96+
97+
It("Should run docker run followed by docker commit and rm container when configuring", func() {
98+
runner := ddocker.DockerConfigureCmd{Config: "test"}
99+
runner.Run(cli, &ctx)
100+
Expect(len(RanCmds)).To(Equal(3))
101+
checkConfigureCmd(RanCmds[0])
102+
checkConfigureCommit(RanCmds[1])
103+
checkConfigureClean(RanCmds[2])
104+
})
66105
})
67106
})

launcher_go/v2/config/config.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func (config *Config) Dockerfile(pupsArgs string, bakeEnv bool) string {
143143
builder.WriteString("RUN " +
144144
"cat /temp-config.yaml | /usr/local/bin/pups " + pupsArgs + " --stdin " +
145145
"&& rm /temp-config.yaml\n")
146-
builder.WriteString("CMD [\"" + config.bootCommand() + "\"]")
146+
builder.WriteString("CMD [\"" + config.BootCommand() + "\"]")
147147
return builder.String()
148148
}
149149

@@ -155,7 +155,7 @@ func (config *Config) WriteYamlConfig(dir string) error {
155155
return nil
156156
}
157157

158-
func (config *Config) bootCommand() string {
158+
func (config *Config) BootCommand() string {
159159
if len(config.Boot_Command) > 0 {
160160
return config.Boot_Command
161161
} else if config.No_Boot_Command {
@@ -177,6 +177,10 @@ func (config *Config) EnvArray(includeKnownSecrets bool) []string {
177177
return envs
178178
}
179179

180+
func (config *Config) DockerArgs() []string {
181+
return strings.Fields(config.Docker_Args)
182+
}
183+
180184
func (config *Config) dockerfileEnvs() string {
181185
builder := []string{}
182186
for k, _ := range config.Env {
@@ -207,3 +211,21 @@ func (config *Config) dockerfileExpose() string {
207211
slices.Sort(builder)
208212
return strings.Join(builder, "\n")
209213
}
214+
215+
func (config *Config) RunImage() string {
216+
if len(config.Run_Image) > 0 {
217+
return config.Run_Image
218+
}
219+
return utils.BaseImageName + config.Name
220+
}
221+
222+
func (config *Config) DockerHostname(defaultHostname string) string {
223+
_, exists := config.Env["DOCKER_USE_HOSTNAME"]
224+
re := regexp.MustCompile(`[^a-zA-Z-]`)
225+
hostname := defaultHostname
226+
if exists {
227+
hostname = config.Env["DISCOURSE_HOSTNAME"]
228+
}
229+
hostname = string(re.ReplaceAll([]byte(hostname), []byte("-"))[:])
230+
return hostname
231+
}

launcher_go/v2/config/config_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,19 @@ var _ = Describe("Config", func() {
4343
Expect(dockerfile).To(ContainSubstring("RUN cat /temp-config.yaml"))
4444
Expect(dockerfile).To(ContainSubstring("EXPOSE 80"))
4545
})
46+
47+
Context("hostname tests", func() {
48+
It("replaces hostname", func() {
49+
config := config.Config{Env: map[string]string{"DOCKER_USE_HOSTNAME": "true", "DISCOURSE_HOSTNAME": "asdfASDF"}}
50+
Expect(config.DockerHostname("")).To(Equal("asdfASDF"))
51+
})
52+
It("replaces hostname", func() {
53+
config := config.Config{Env: map[string]string{"DOCKER_USE_HOSTNAME": "true", "DISCOURSE_HOSTNAME": "asdf!@#$%^&*()ASDF"}}
54+
Expect(config.DockerHostname("")).To(Equal("asdf----------ASDF"))
55+
})
56+
It("replaces a default hostnamehostname", func() {
57+
config := config.Config{}
58+
Expect(config.DockerHostname("asdf!@#")).To(Equal("asdf---"))
59+
})
60+
})
4661
})

launcher_go/v2/docker/commands.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ package docker
22

33
import (
44
"context"
5+
"fmt"
6+
"github.com/Wing924/shellwords"
57
"github.com/discourse/discourse_docker/launcher_go/v2/config"
68
"github.com/discourse/discourse_docker/launcher_go/v2/utils"
79
"golang.org/x/sys/unix"
810
"io"
911
"os"
1012
"os/exec"
13+
"runtime"
14+
"strings"
1115
"syscall"
16+
"time"
1217
)
1318

1419
type DockerBuilder struct {
@@ -52,3 +57,197 @@ func (r *DockerBuilder) Run() error {
5257
}
5358
return nil
5459
}
60+
61+
type DockerRunner struct {
62+
Config *config.Config
63+
Ctx *context.Context
64+
ExtraEnv []string
65+
ExtraFlags []string
66+
Rm bool
67+
ContainerId string
68+
CustomImage string
69+
Cmd []string
70+
Stdin io.Reader
71+
SkipPorts bool
72+
DryRun bool
73+
Restart bool
74+
Detatch bool
75+
Hostname string
76+
}
77+
78+
func (r *DockerRunner) Run() error {
79+
cmd := exec.CommandContext(*r.Ctx, utils.DockerPath, "run")
80+
81+
// Detatch signifies we do not want to supervise
82+
if !r.Detatch {
83+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
84+
cmd.Cancel = func() error {
85+
if runtime.GOOS == "darwin" {
86+
runCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
87+
stopCmd := exec.CommandContext(runCtx, utils.DockerPath, "stop", r.ContainerId)
88+
utils.CmdRunner(stopCmd).Run()
89+
cancel()
90+
}
91+
return unix.Kill(-cmd.Process.Pid, unix.SIGINT)
92+
}
93+
}
94+
cmd.Env = r.Config.EnvArray(true)
95+
96+
if r.DryRun {
97+
// multi-line env doesn't work super great from CLI, but we can print out the rest.
98+
for k, v := range r.Config.Env {
99+
if !strings.Contains(v, "\n") {
100+
cmd.Args = append(cmd.Args, "--env")
101+
cmd.Args = append(cmd.Args, k+"="+shellwords.Escape(v))
102+
}
103+
}
104+
} else {
105+
for k, _ := range r.Config.Env {
106+
cmd.Args = append(cmd.Args, "--env")
107+
cmd.Args = append(cmd.Args, k)
108+
}
109+
}
110+
111+
// Order is important here, we add extra env after config's env to override anything set in env.
112+
for _, e := range r.ExtraEnv {
113+
cmd.Args = append(cmd.Args, "--env")
114+
cmd.Args = append(cmd.Args, e)
115+
}
116+
for k, v := range r.Config.Labels {
117+
cmd.Args = append(cmd.Args, "--label")
118+
cmd.Args = append(cmd.Args, k+"="+v)
119+
}
120+
if !r.SkipPorts {
121+
for _, v := range r.Config.Expose {
122+
if strings.Contains(v, ":") {
123+
cmd.Args = append(cmd.Args, "-p")
124+
cmd.Args = append(cmd.Args, v)
125+
} else {
126+
cmd.Args = append(cmd.Args, "--expose")
127+
cmd.Args = append(cmd.Args, v)
128+
}
129+
}
130+
}
131+
for _, v := range r.Config.Volumes {
132+
cmd.Args = append(cmd.Args, "-v")
133+
cmd.Args = append(cmd.Args, v.Volume.Host+":"+v.Volume.Guest)
134+
}
135+
for _, v := range r.Config.Links {
136+
cmd.Args = append(cmd.Args, "--link")
137+
cmd.Args = append(cmd.Args, v.Link.Name+":"+v.Link.Alias)
138+
}
139+
cmd.Args = append(cmd.Args, "--shm-size=512m")
140+
if r.Rm {
141+
cmd.Args = append(cmd.Args, "--rm")
142+
}
143+
if r.Restart {
144+
cmd.Args = append(cmd.Args, "--restart=always")
145+
} else {
146+
cmd.Args = append(cmd.Args, "--restart=no")
147+
}
148+
if r.Detatch {
149+
cmd.Args = append(cmd.Args, "-d")
150+
}
151+
cmd.Args = append(cmd.Args, "-i")
152+
153+
// Docker args override settings above
154+
for _, f := range r.Config.DockerArgs() {
155+
cmd.Args = append(cmd.Args, f)
156+
}
157+
for _, f := range r.ExtraFlags {
158+
cmd.Args = append(cmd.Args, f)
159+
}
160+
cmd.Args = append(cmd.Args, "-h")
161+
cmd.Args = append(cmd.Args, r.Hostname)
162+
cmd.Args = append(cmd.Args, "--name")
163+
cmd.Args = append(cmd.Args, r.ContainerId)
164+
if len(r.CustomImage) > 0 {
165+
cmd.Args = append(cmd.Args, r.CustomImage)
166+
} else {
167+
cmd.Args = append(cmd.Args, r.Config.RunImage())
168+
}
169+
170+
for _, c := range r.Cmd {
171+
cmd.Args = append(cmd.Args, c)
172+
}
173+
174+
if !r.Detatch {
175+
cmd.Stdout = os.Stdout
176+
cmd.Stderr = os.Stderr
177+
cmd.Stdin = r.Stdin
178+
}
179+
runner := utils.CmdRunner(cmd)
180+
if r.DryRun {
181+
fmt.Println(cmd)
182+
} else {
183+
if err := runner.Run(); err != nil {
184+
return err
185+
}
186+
}
187+
return nil
188+
}
189+
190+
type DockerPupsRunner struct {
191+
Config *config.Config
192+
PupsArgs string
193+
SavedImageName string
194+
ExtraEnv []string
195+
Ctx *context.Context
196+
ContainerId string
197+
}
198+
199+
func (r *DockerPupsRunner) Run() error {
200+
rm := false
201+
// remove : in case docker tag is blank, and use default latest tag
202+
r.SavedImageName = strings.TrimRight(r.SavedImageName, ":")
203+
if r.SavedImageName == "" {
204+
rm = true
205+
}
206+
defer func(rm bool) {
207+
if !rm {
208+
time.Sleep(utils.CommitWait)
209+
runCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
210+
cmd := exec.CommandContext(runCtx, utils.DockerPath, "rm", "-f", r.ContainerId)
211+
utils.CmdRunner(cmd).Run()
212+
cancel()
213+
}
214+
}(rm)
215+
commands := []string{"/bin/bash",
216+
"-c",
217+
"/usr/local/bin/pups --stdin " + r.PupsArgs}
218+
219+
runner := DockerRunner{Config: r.Config,
220+
Ctx: r.Ctx,
221+
ExtraEnv: r.ExtraEnv,
222+
Rm: rm,
223+
ContainerId: r.ContainerId,
224+
Cmd: commands,
225+
Stdin: strings.NewReader(r.Config.Yaml()),
226+
SkipPorts: true, //pups runs don't need to expose ports
227+
}
228+
229+
if err := runner.Run(); err != nil {
230+
return err
231+
}
232+
233+
if len(r.SavedImageName) > 0 {
234+
time.Sleep(utils.CommitWait)
235+
cmd := exec.Command("docker",
236+
"commit",
237+
"--change",
238+
"LABEL org.opencontainers.image.created=\""+time.Now().Format(time.RFC3339)+"\"",
239+
"--change",
240+
"CMD [\""+r.Config.BootCommand()+"\"]",
241+
r.ContainerId,
242+
r.SavedImageName,
243+
)
244+
cmd.Stdout = os.Stdout
245+
cmd.Stderr = os.Stderr
246+
247+
fmt.Fprintln(utils.Out, cmd)
248+
if err := utils.CmdRunner(cmd).Run(); err != nil {
249+
return err
250+
}
251+
}
252+
return nil
253+
}

0 commit comments

Comments
 (0)