Merge pull request #88 from tbflw/feature/run_non_root_and_log_startup

Feature/run non root and log startup
This commit is contained in:
Julien Neuhart
2019-06-21 11:18:26 +02:00
committed by GitHub
15 changed files with 150 additions and 44 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
scripts
test

1
.gitignore vendored
View File

@@ -0,0 +1 @@
.idea

View File

@@ -2,6 +2,7 @@ GOLANG_VERSION=1.12
VERSION=snapshot VERSION=snapshot
DOCKER_USER= DOCKER_USER=
DOCKER_PASSWORD= DOCKER_PASSWORD=
DOCKER_REPO=thecodingmachine
# generate documentation. # generate documentation.
doc: doc:
@@ -27,7 +28,7 @@ tests:
# build Docker image. # build Docker image.
image: image:
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile . docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile . docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t $(DOCKER_REPO)/gotenberg:$(VERSION) -f build/package/Dockerfile .
# start the API using previously built Docker image. # start the API using previously built Docker image.
gotenberg: gotenberg:

View File

@@ -1,4 +1,4 @@
FROM debian:9.5-slim FROM debian:9-slim
# |-------------------------------------------------------------------------- # |--------------------------------------------------------------------------
# | Common libraries # | Common libraries
@@ -91,3 +91,16 @@ RUN apt-get install -y \
fonts-unfonts-core fonts-unfonts-core
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
# |--------------------------------------------------------------------------
# | Default user
# |--------------------------------------------------------------------------
# |
# | TODO find a correct description for why we're doing this.
# |
RUN groupadd --gid 1001 gotenberg \
&& useradd --uid 1001 --gid gotenberg --shell /bin/bash --no-create-home gotenberg \
&& mkdir /gotenberg \
&& chown gotenberg: /gotenberg

View File

@@ -10,6 +10,8 @@ You may start it with:
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5 $ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:5
``` ```
All processes in the docker container runs as a dedicated non-root user called `gotenberg` with user id `1001` from the working dir `/gotenberg`.
> The API will be available at [http://localhost:3000](http://localhost:3000). > The API will be available at [http://localhost:3000](http://localhost:3000).
## Docker Compose ## Docker Compose
@@ -38,5 +40,12 @@ Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoc
> The more resources are granted, the quicker will be the conversions. > The more resources are granted, the quicker will be the conversions.
Also, in the deployment spec of the pod, specify the uid `1001` of the user `gotenberg`:
```
securityContext:
privileged: false
runAsUser: 1001
```
In the following examples, we will assume your In the following examples, we will assume your
Gotenberg API is available at [http://localhost:3000](http://localhost:3000). Gotenberg API is available at [http://localhost:3000](http://localhost:3000).

View File

@@ -8,7 +8,7 @@ You may customize the API behaviour thanks to environment variables.
In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`. In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME`.
It takes the strings `"0"` or `"1"` as value. It takes the strings `"0"` or `"1"` as value where `1` means `true`
> If Google Chrome is disabled, the following conversions will **not** be available anymore: > If Google Chrome is disabled, the following conversions will **not** be available anymore:
> [HTML](#html), [URL](#url) and [Markdown](#markdown) > [HTML](#html), [URL](#url) and [Markdown](#markdown)
@@ -37,12 +37,19 @@ By default, the API will add a log entry when the [healthcheck endpoint](#ping)
You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable `DISABLE_HEALTHCHECK_LOGGING`. You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable `DISABLE_HEALTHCHECK_LOGGING`.
This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values. This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values, where `1` is enabled.
## Default listen port ## Default listen port
By default, the API will listen on port `3000`. For most use cases this is perfectly fine, but at times there may be cases where you need to change this due to port conflicts. By default, the API will listen on port `3000`. For most use cases this is perfectly fine, but at times there may be cases where you need to change this due to port conflicts.
You may customize this port location with the environment variable `DEFAULT_LISTEN_PORT`. You may customize this port location with the environment variable `DEFAULT_LISTEN_PORT`.
This environment variable accepts any string that can be turned into a port number (e.g., the string `"0"` up to the string `"65535"`). This environment variable accepts any string that can be turned into a port number (e.g., the string `"0"` up to the string `"65535"`).
## Debug logging of process startup
By default, stdout and stderr messages from the started processes are disabled.
You may enable some debug logging from starting the process by setting the environment variable `DEBUG_PROCESS_STARTUP`.
This environment variable operates in the same manner as the `DISABLE_GOOGLE_CHROME` and `DISABLE_UNOCONV` variables operate in that it accepts the strings `"0"` or `"1"` as values, where `1` means `true`.

View File

@@ -19,7 +19,11 @@ ENV GOOS=linux \
WORKDIR /gotenberg WORKDIR /gotenberg
# Copy our source code. # Copy our source code.
COPY . . COPY internal ./internal
COPY cmd ./cmd
COPY go.sum go.sum
COPY go.mod go.mod
# Build our binary. # Build our binary.
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
@@ -37,6 +41,8 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/ COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
ENV PM2_HOME=/gotenberg/.pm2
USER gotenberg
WORKDIR /gotenberg WORKDIR /gotenberg
EXPOSE 3000 EXPOSE 3000

View File

@@ -25,6 +25,7 @@ const (
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME" disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
disableUnoconvEnvVar = "DISABLE_UNOCONV" disableUnoconvEnvVar = "DISABLE_UNOCONV"
disableHealthcheckLoggingEnvVar = "DISABLE_HEALTHCHECK_LOGGING" disableHealthcheckLoggingEnvVar = "DISABLE_HEALTHCHECK_LOGGING"
debugProcessStartup = "DEBUG_PROCESS_STARTUP"
) )
func mustParseEnvVar() *api.Options { func mustParseEnvVar() *api.Options {
@@ -49,37 +50,31 @@ func mustParseEnvVar() *api.Options {
} }
opts.DefaultListenPort = v opts.DefaultListenPort = v
} }
if v, ok := os.LookupEnv(disableGoogleChromeEnvVar); ok { //checkBoolEnv is a convenience function for reading an env var with a bool value where `1` is true and `0` is false
if v != "1" && v != "0" { checkBoolEnv := func(name string) bool {
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableGoogleChromeEnvVar, v)) if v, ok := os.LookupEnv(name); ok {
os.Exit(1) if v != "1" && v != "0" {
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", name, v))
os.Exit(1)
}
return v == "1"
} }
opts.EnableChromeEndpoints = v != "1" return false
}
if v, ok := os.LookupEnv(disableUnoconvEnvVar); ok {
if v != "1" && v != "0" {
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableUnoconvEnvVar, v))
os.Exit(1)
}
opts.EnableUnoconvEndpoints = v != "1"
}
if v, ok := os.LookupEnv(disableHealthcheckLoggingEnvVar); ok {
if v != "1" && v != "0" {
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableHealthcheckLoggingEnvVar, v))
os.Exit(1)
}
opts.EnableHealthcheckLogging = v != "1"
} }
opts.EnableChromeEndpoints = !checkBoolEnv(disableGoogleChromeEnvVar)
opts.EnableUnoconvEndpoints = !checkBoolEnv(disableUnoconvEnvVar)
opts.EnableHealthcheckLogging = !checkBoolEnv(disableHealthcheckLoggingEnvVar)
opts.DebugProcessStartup = checkBoolEnv(debugProcessStartup)
return opts return opts
} }
func mustStartProcesses(opts *api.Options) []pm2.Process { func mustStartProcesses(opts *api.Options) []pm2.Process {
var processes []pm2.Process var processes []pm2.Process
if opts.EnableChromeEndpoints { if opts.EnableChromeEndpoints {
processes = append(processes, pm2.NewChrome()) processes = append(processes, pm2.NewChrome(opts.DebugProcessStartup))
} }
if opts.EnableUnoconvEndpoints { if opts.EnableUnoconvEndpoints {
processes = append(processes, pm2.NewUnoconv()) processes = append(processes, pm2.NewUnoconv(opts.DebugProcessStartup))
} }
for _, p := range processes { for _, p := range processes {
notify.Printf("starting %s with PM2...", p.Fullname()) notify.Printf("starting %s with PM2...", p.Fullname())

View File

@@ -139,6 +139,8 @@
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:5 <pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:5
</pre> </pre>
<p>All processes in the docker container runs as a dedicated non-root user called <code>gotenberg</code> with user id <code>1001</code> from the working dir <code>/gotenberg</code>.</p>
<blockquote> <blockquote>
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p> <p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
</blockquote> </blockquote>
@@ -176,6 +178,13 @@ Otherwise the API will not be able to launch Google Chrome and LibreOffice (unoc
<p>The more resources are granted, the quicker will be the conversions.</p> <p>The more resources are granted, the quicker will be the conversions.</p>
</blockquote> </blockquote>
<p>Also, in the deployment spec of the pod, specify the uid <code>1001</code> of the user <code>gotenberg</code>:</p>
<pre class="chroma"> securityContext:
privileged: false
runAsUser: 1001
</pre>
<p>In the following examples, we will assume your <p>In the following examples, we will assume your
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p> Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
@@ -222,7 +231,7 @@ Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3
<p>In order to save some resources, the Gotenberg image accepts the environment variable <code>DISABLE_GOOGLE_CHROME</code>.</p> <p>In order to save some resources, the Gotenberg image accepts the environment variable <code>DISABLE_GOOGLE_CHROME</code>.</p>
<p>It takes the strings <code>&#34;0&#34;</code> or <code>&#34;1&#34;</code> as value.</p> <p>It takes the strings <code>&#34;0&#34;</code> or <code>&#34;1&#34;</code> as value where <code>1</code> means <code>true</code></p>
<blockquote> <blockquote>
<p>If Google Chrome is disabled, the following conversions will <strong>not</strong> be available anymore: <p>If Google Chrome is disabled, the following conversions will <strong>not</strong> be available anymore:
@@ -263,7 +272,7 @@ See the <a href="#timeout">timeout section</a>.</p>
<p>You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable <code>DISABLE_HEALTHCHECK_LOGGING</code>.</p> <p>You may turn off this logging so as to avoid unnecessary entries in your logs with the environment variable <code>DISABLE_HEALTHCHECK_LOGGING</code>.</p>
<p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>&#34;0&#34;</code> or <code>&#34;1&#34;</code> as values.</p> <p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>&#34;0&#34;</code> or <code>&#34;1&#34;</code> as values, where <code>1</code> is enabled.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_listen_port" href="#environment_variables.default_listen_port"> <h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.default_listen_port" href="#environment_variables.default_listen_port">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
@@ -275,6 +284,16 @@ See the <a href="#timeout">timeout section</a>.</p>
<p>This environment variable accepts any string that can be turned into a port number (e.g., the string <code>&#34;0&#34;</code> up to the string <code>&#34;65535&#34;</code>).</p> <p>This environment variable accepts any string that can be turned into a port number (e.g., the string <code>&#34;0&#34;</code> up to the string <code>&#34;65535&#34;</code>).</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="environment_variables.debug_logging_of_process_startup" href="#environment_variables.debug_logging_of_process_startup">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Debug logging of process startup</h2>
<p>By default, stdout and stderr messages from the started processes are disabled.</p>
<p>You may enable some debug logging from starting the process by setting the environment variable <code>DEBUG_PROCESS_STARTUP</code>.</p>
<p>This environment variable operates in the same manner as the <code>DISABLE_GOOGLE_CHROME</code> and <code>DISABLE_UNOCONV</code> variables operate in that it accepts the strings <code>&#34;0&#34;</code> or <code>&#34;1&#34;</code> as values, where <code>1</code> means <code>true</code>.</p>
</div> </div>
<div class="Page" id="html"> <div class="Page" id="html">

View File

@@ -1,8 +1,6 @@
package api package api
import ( import "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4"
)
const pingEndpoint = "/ping" const pingEndpoint = "/ping"
@@ -14,6 +12,7 @@ type Options struct {
EnableChromeEndpoints bool EnableChromeEndpoints bool
EnableUnoconvEndpoints bool EnableUnoconvEndpoints bool
EnableHealthcheckLogging bool EnableHealthcheckLogging bool
DebugProcessStartup bool
} }
// DefaultOptions returns default options. // DefaultOptions returns default options.

View File

@@ -7,15 +7,19 @@ import (
"github.com/mafredri/cdp/devtool" "github.com/mafredri/cdp/devtool"
) )
const (
warmupTime = 10 * time.Second
)
type chrome struct { type chrome struct {
manager *processManager manager *processManager
} }
// NewChrome retruns a Google Chrome // NewChrome retruns a Google Chrome
// headless process. // headless process.
func NewChrome() Process { func NewChrome(debug bool) Process {
return &chrome{ return &chrome{
manager: &processManager{}, manager: &processManager{verbose: debug},
} }
} }
@@ -58,12 +62,20 @@ func (p *chrome) viable() bool {
// check if Google Chrome is correctly running. // check if Google Chrome is correctly running.
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
_, err := devtool.New("http://localhost:9222").Version(ctx) p.manager.notifyf(`%v: checking Chrome liveness via debug version endpoint
return err == nil 'http://localhost:9222/json/version'`, p.name())
v, err := devtool.New("http://localhost:9222").Version(ctx)
if err != nil {
p.manager.notifyf("%s: %s version endpoint returned error: %v", p.name(), p.Fullname(), err)
return false
}
p.manager.notifyf("%s: %s returned version info: %+v", p.name(), p.Fullname(), *v)
return true
} }
func (p *chrome) warmup() { func (p *chrome) warmup() {
time.Sleep(5 * time.Second) p.manager.notifyf("%s: allowing %s %v to startup", p.name(), p.Fullname(), warmupTime)
time.Sleep(warmupTime)
} }
// Compile-time checks to ensure type implements desired interfaces. // Compile-time checks to ensure type implements desired interfaces.

View File

@@ -7,13 +7,13 @@ import (
) )
func TestChromeStart(t *testing.T) { func TestChromeStart(t *testing.T) {
p := NewChrome() p := NewChrome(false)
err := p.Start() err := p.Start()
require.Nil(t, err) require.Nil(t, err)
} }
func TestChromeShutdown(t *testing.T) { func TestChromeShutdown(t *testing.T) {
p := NewChrome() p := NewChrome(false)
err := p.Shutdown() err := p.Shutdown()
require.Nil(t, err) require.Nil(t, err)
} }

View File

@@ -1,8 +1,14 @@
package pm2 package pm2
import ( import (
"bufio"
"fmt" "fmt"
"io"
"os/exec" "os/exec"
"strings"
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
) )
const ( const (
@@ -25,6 +31,7 @@ type Process interface {
type processManager struct { type processManager struct {
heuristicState int32 heuristicState int32
verbose bool
} }
func (m *processManager) start(p Process) error { func (m *processManager) start(p Process) error {
@@ -76,8 +83,43 @@ func (m *processManager) pm2(p Process, cmdName string) error {
"pm2", "pm2",
cmdArgs..., cmdArgs...,
) )
m.notifyf("executing command '%v'", strings.Join(cmd.Args, " "))
if m.verbose {
processStdErr, err := cmd.StderrPipe()
if err != nil {
return fmt.Errorf("failed getting stderr from '%s': %v", p.Fullname(), err)
}
processStdOut, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed getting stdout from '%s': %v", p.Fullname(), err)
}
readFromPipe := func(name string, reader io.ReadCloser) {
r := bufio.NewReader(reader)
defer reader.Close() // nolint: errcheck
for {
line, _, err := r.ReadLine()
if err != nil {
if err != io.EOF {
m.notifyf("error reading from %v for process '%v'", name, p.Fullname())
}
break
}
if len(line) != 0 {
m.notifyf("%v %v: %s", p.name(), name, string(line))
}
}
}
go readFromPipe("stdout", processStdOut)
go readFromPipe("stderr", processStdErr)
}
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err) return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
} }
return nil return nil
} }
func (m *processManager) notifyf(format string, args ...interface{}) {
if m.verbose {
notify.Printf(fmt.Sprintf("%v: %s", time.Now().Format(time.RFC3339), format), args...)
}
}

View File

@@ -6,9 +6,9 @@ type unoconv struct {
// NewUnoconv retruns a unoconv listener // NewUnoconv retruns a unoconv listener
// process. // process.
func NewUnoconv() Process { func NewUnoconv(debug bool) Process {
return &unoconv{ return &unoconv{
manager: &processManager{}, manager: &processManager{verbose: debug},
} }
} }

View File

@@ -7,13 +7,13 @@ import (
) )
func TestUnoconvStart(t *testing.T) { func TestUnoconvStart(t *testing.T) {
p := NewUnoconv() p := NewUnoconv(false)
err := p.Start() err := p.Start()
require.Nil(t, err) require.Nil(t, err)
} }
func TestUnoconvShutdown(t *testing.T) { func TestUnoconvShutdown(t *testing.T) {
p := NewUnoconv() p := NewUnoconv(false)
err := p.Shutdown() err := p.Shutdown()
require.Nil(t, err) require.Nil(t, err)
} }