diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a3a0fce8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +scripts +test \ No newline at end of file diff --git a/.gitignore b/.gitignore index e69de29b..723ef36f 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/Makefile b/Makefile index b259902b..30627d03 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/build/base/Dockerfile b/build/base/Dockerfile index 102f6873..24ab4661 100644 --- a/build/base/Dockerfile +++ b/build/base/Dockerfile @@ -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 + diff --git a/build/docs/content/01-install.md b/build/docs/content/01-install.md index 03585a66..0437115c 100644 --- a/build/docs/content/01-install.md +++ b/build/docs/content/01-install.md @@ -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). \ No newline at end of file diff --git a/build/docs/content/03-environment-variables.md b/build/docs/content/03-environment-variables.md index 3f081d56..685cf91c 100644 --- a/build/docs/content/03-environment-variables.md +++ b/build/docs/content/03-environment-variables.md @@ -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"`). \ No newline at end of file +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`. \ No newline at end of file diff --git a/build/package/Dockerfile b/build/package/Dockerfile index f9028a72..05fa13c0 100644 --- a/build/package/Dockerfile +++ b/build/package/Dockerfile @@ -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 " COPY --from=golang /gotenberg/gotenberg /usr/local/bin/ +ENV PM2_HOME=/gotenberg/.pm2 +USER gotenberg WORKDIR /gotenberg EXPOSE 3000 diff --git a/cmd/gotenberg/main.go b/cmd/gotenberg/main.go index 0d7d7cea..dae9192e 100644 --- a/cmd/gotenberg/main.go +++ b/cmd/gotenberg/main.go @@ -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()) diff --git a/docs/index.html b/docs/index.html index bcf2fd81..fa8df116 100755 --- a/docs/index.html +++ b/docs/index.html @@ -139,6 +139,8 @@
$ 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.

@@ -176,6 +178,13 @@ 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.

@@ -222,7 +231,7 @@ Gotenberg API is available at http://localhost:3

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: @@ -263,7 +272,7 @@ See the timeout section.

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.

timeout section.

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.

+
diff --git a/internal/app/api/api.go b/internal/app/api/api.go index 1edd6063..d00e01eb 100644 --- a/internal/app/api/api.go +++ b/internal/app/api/api.go @@ -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. diff --git a/internal/pkg/pm2/chrome.go b/internal/pkg/pm2/chrome.go index dfcdc1f9..105c76cc 100644 --- a/internal/pkg/pm2/chrome.go +++ b/internal/pkg/pm2/chrome.go @@ -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. diff --git a/internal/pkg/pm2/chrome_test.go b/internal/pkg/pm2/chrome_test.go index fa30c443..edb8b5af 100644 --- a/internal/pkg/pm2/chrome_test.go +++ b/internal/pkg/pm2/chrome_test.go @@ -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) } diff --git a/internal/pkg/pm2/pm2.go b/internal/pkg/pm2/pm2.go index c4cf937a..167b3e0d 100644 --- a/internal/pkg/pm2/pm2.go +++ b/internal/pkg/pm2/pm2.go @@ -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...) + } +} diff --git a/internal/pkg/pm2/unoconv.go b/internal/pkg/pm2/unoconv.go index f2881010..7c4fa13e 100644 --- a/internal/pkg/pm2/unoconv.go +++ b/internal/pkg/pm2/unoconv.go @@ -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}, } } diff --git a/internal/pkg/pm2/unoconv_test.go b/internal/pkg/pm2/unoconv_test.go index 8c74b244..6995109e 100644 --- a/internal/pkg/pm2/unoconv_test.go +++ b/internal/pkg/pm2/unoconv_test.go @@ -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) }