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
DOCKER_USER=
DOCKER_PASSWORD=
DOCKER_REPO=thecodingmachine
# generate documentation.
doc:
@@ -27,7 +28,7 @@ tests:
# build Docker image.
image:
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.
gotenberg:

View File

@@ -1,4 +1,4 @@
FROM debian:9.5-slim
FROM debian:9-slim
# |--------------------------------------------------------------------------
# | Common libraries
@@ -91,3 +91,16 @@ RUN apt-get install -y \
fonts-unfonts-core
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
```
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).
## 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.
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
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`.
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:
> [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`.
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
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`.
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
# Copy our source code.
COPY . .
COPY internal ./internal
COPY cmd ./cmd
COPY go.sum go.sum
COPY go.mod go.mod
# Build our binary.
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/
ENV PM2_HOME=/gotenberg/.pm2
USER gotenberg
WORKDIR /gotenberg
EXPOSE 3000

View File

@@ -25,6 +25,7 @@ const (
disableGoogleChromeEnvVar = "DISABLE_GOOGLE_CHROME"
disableUnoconvEnvVar = "DISABLE_UNOCONV"
disableHealthcheckLoggingEnvVar = "DISABLE_HEALTHCHECK_LOGGING"
debugProcessStartup = "DEBUG_PROCESS_STARTUP"
)
func mustParseEnvVar() *api.Options {
@@ -49,37 +50,31 @@ func mustParseEnvVar() *api.Options {
}
opts.DefaultListenPort = v
}
if v, ok := os.LookupEnv(disableGoogleChromeEnvVar); ok {
if v != "1" && v != "0" {
notify.ErrPrint(fmt.Errorf("%s: wrong value: want \"0\" or \"1\" got %v", disableGoogleChromeEnvVar, v))
os.Exit(1)
//checkBoolEnv is a convenience function for reading an env var with a bool value where `1` is true and `0` is false
checkBoolEnv := func(name string) bool {
if v, ok := os.LookupEnv(name); ok {
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"
}
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"
return false
}
opts.EnableChromeEndpoints = !checkBoolEnv(disableGoogleChromeEnvVar)
opts.EnableUnoconvEndpoints = !checkBoolEnv(disableUnoconvEnvVar)
opts.EnableHealthcheckLogging = !checkBoolEnv(disableHealthcheckLoggingEnvVar)
opts.DebugProcessStartup = checkBoolEnv(debugProcessStartup)
return opts
}
func mustStartProcesses(opts *api.Options) []pm2.Process {
var processes []pm2.Process
if opts.EnableChromeEndpoints {
processes = append(processes, pm2.NewChrome())
processes = append(processes, pm2.NewChrome(opts.DebugProcessStartup))
}
if opts.EnableUnoconvEndpoints {
processes = append(processes, pm2.NewUnoconv())
processes = append(processes, pm2.NewUnoconv(opts.DebugProcessStartup))
}
for _, p := range processes {
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>
<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>
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
</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>
</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
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>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>
<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>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">
<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>
<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 class="Page" id="html">

View File

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

View File

@@ -7,15 +7,19 @@ import (
"github.com/mafredri/cdp/devtool"
)
const (
warmupTime = 10 * time.Second
)
type chrome struct {
manager *processManager
}
// NewChrome retruns a Google Chrome
// headless process.
func NewChrome() Process {
func NewChrome(debug bool) Process {
return &chrome{
manager: &processManager{},
manager: &processManager{verbose: debug},
}
}
@@ -58,12 +62,20 @@ func (p *chrome) viable() bool {
// check if Google Chrome is correctly running.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := devtool.New("http://localhost:9222").Version(ctx)
return err == nil
p.manager.notifyf(`%v: checking Chrome liveness via debug version endpoint
'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() {
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.

View File

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

View File

@@ -1,8 +1,14 @@
package pm2
import (
"bufio"
"fmt"
"io"
"os/exec"
"strings"
"time"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
)
const (
@@ -25,6 +31,7 @@ type Process interface {
type processManager struct {
heuristicState int32
verbose bool
}
func (m *processManager) start(p Process) error {
@@ -76,8 +83,43 @@ func (m *processManager) pm2(p Process, cmdName string) error {
"pm2",
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 {
return fmt.Errorf("%s %s with PM2: %v", cmdName, p.Fullname(), err)
}
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
// process.
func NewUnoconv() Process {
func NewUnoconv(debug bool) Process {
return &unoconv{
manager: &processManager{},
manager: &processManager{verbose: debug},
}
}

View File

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