Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .github/workflows/release_body.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# Adding in more supported CPU architectures
# Stability and bug fixes

This can now be used on arm64 chips. This enables use on apple silicon and other arm64 chips.
This release resolves a comprehensive set of bugs identified in a static analysis and manual code review. All critical, high, medium, and low severity issues have been addressed.

## Critical fixes
- Fixed infinite recursion in `rotateWriter.Close()` — was calling itself instead of `w.fp.Close()`, crashing on every graceful shutdown
- Fixed nil map panic in `FileLogManager` — `filetracker` map was never initialized, making file logging completely non-functional
- Fixed nil map access in `FileLogManager.RegisterConfig()` — same root cause as above
- Fixed data race in signal replicator — `listen()` iterated over `signalChannels` without holding the read lock
- Fixed potential deadlock in signal replicator — sends to process channels are now non-blocking (`select` with `default`)
- Fixed multiple timer callbacks on repeated signals — `time.AfterFunc` is now guarded with `sync.Once`

## High severity fixes
- Fixed STDERR logging — was logging the full unsplit buffer for every line instead of the individual line
- Removed always-false nil guards on struct fields — `&field == nil` is never true; removed four dead code blocks and the unused `createLoggingConfig` helper
- Fixed watchdog goroutine never being started in `newRW()` — file rotation was silently not working
- Fixed panic on empty write in `BytePipe.Write()` — added length guard before accessing the last byte
- Fixed implicit nil error return in `BytePipe.Write()` — now returns explicit `nil` on success

## Medium severity fixes
- Fixed inverted logic in `Process.running()` — was returning `p.exited` (true when stopped) instead of `!p.exited`
- Fixed nil file handle after rename failure in `rotate()` — error is now logged and execution falls through to create a fresh file rather than returning with `w.fp = nil`
- Removed unused global variable `fileLogManager`
- Fixed goroutine leak in `LogManager.Submit()` — sending to a nil channel (missing map key) now guarded with an ok check
- Fixed `log.Fatalf` format string — was using string concatenation instead of a format argument (missing comma)
- Fixed data race on `p.exited` — assignment now wrapped in `Lock()`/`Unlock()` to match the `RLock()` in `running()`

## Low severity fixes
- Renamed misleading `rotateWriter.panic()` method to `logError()` — it never panicked
- Added safe ok-check for `*exec.ExitError` type assertion in `RunSecretProcess`
- Added comments in `Console.Submit()` explaining why write errors are intentionally not reported

## Tests
- Added regression test for `rotateWriter.Close()` to guard against infinite recursion
- Added regression test for `FileLogManager` nil map to guard against uninitialized map panics
- Added concurrent regression test for signal replicator to catch the data race under `-race`
2 changes: 1 addition & 1 deletion Dockerfile.test
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM gcr.io/distroless/base
FROM gcr.io/distroless/static-debian13

ADD ./passwd /etc/passwd

Expand Down
65 changes: 40 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,68 @@
# Launch

A simple runtime-agnostic process manager that eases Docker services startup and logging.
Consider it a half way house between Kubernetes and Docker.
A lightweight process manager designed to run as PID 1 in Docker containers.

Launch is expected to be process 1 in a container. It allows you to watch other processes in your containers and when they are all finished it will finish allowing containers to stop correctly.
Launch lets you run multiple processes in a single container with centralized logging, ordered startup hooks, and graceful signal propagation.

## What can Launch help with
## Features

Launch is designed to be a process manager with simple but powerful logging. It borrows some ideas from kubernetes without having to deploy a kubernetes stack.
- Run **multiple processes** in one container.
- **Secret collection** runs first and injects output as environment variables for all later processes.
- **Init processes** run sequentially after secrets — any failure aborts startup.
- **Main processes** run in parallel; if one exits, Launch sends SIGTERM to all remaining ones and waits for shutdown.
- Ship logs to **console, file, syslog, or /dev/null** independently per process.
- **Templated YAML config** — inject environment variables and use built-in helper functions.

* You can run multiple processes in a single container.
* You can ship logs from processes to different logging engines.
* You can run init processes that run before your main processes. This allows you to collect artifacts, secrets or just setup an environment.
* A single main process dying will bring down a container, gracefully shutting down the other applications.
## How it works

## Configuration

More details can be found in the [README Folder](./READMEs/)
```
Startup sequence:
1. Read and render config file (first pass — container env vars available)
2. Run secret processes → capture stdout JSON and inject as env vars
3. Re-render config file (second pass — secret env vars now available)
4. Run init processes sequentially — any non-zero exit aborts startup
5. Start main processes in parallel
6. Wait — when any main process exits, send SIGTERM to all others and shut down
```

## Processes

Launch has 3 processes types when running.
Launch supports three types of processes, each with a distinct role.

### Secret processes

These are used to collect secret data like username and passwords at startup.
Run before anything else. They collect credentials or other secrets and expose them as environment variables. Because full logging is not yet available at this stage, output goes to the console.

### Init processes

These are used to configure the state of the container or collect more resources that don't need to be exported to environment variables.
Run sequentially after secrets. Use these for setup tasks — running migrations, fetching config, preparing the filesystem. All init processes must succeed before main processes start.

### Main Processes
### Main processes

These are the long running processes in your containers.
The long-running processes in your container. They start in parallel and are expected to run for the lifetime of the container. If any one of them exits, Launch sends SIGTERM to all the others.

## Configuration

The configuration YAML file is the driving force behind Launch. The configuration file will tell Launch what processes to run with what arguments, where to send logs.
The configuration is driven by a YAML file. Run the following to generate an annotated example:

```bash
./launch -example-config
```

The configuration file has a templating feature that allows you to make the configuration dynamic.
Full documentation:

The configuration file has many sections that are documented in the
[README dedicated to configuration](./READMEs/ConfigurationFile.MD).
- [Configuration reference](./READMEs/ConfigurationFile.md) — all sections and options
- [Logging engines](./READMEs/Logging.md) — console, file, syslog, `/dev/null`
- [Secrets](./READMEs/Secrets.md) — how secret collection works

## Contributing

This project is still very much in active development. Expect changes and improvements.
This project is still in active development. There is a simple build script for integration testing.

There is a simple build script that does the current integration testing.
```bash
# Run tests only
./build_and_test.sh

Use `./build_and_test.sh` to run only the tests
Use `./build_and_test.sh` gobuild" to also rebuild the go project
# Rebuild the binary and run tests
./build_and_test.sh gobuild
```
14 changes: 7 additions & 7 deletions READMEs/ConfigrationFile.md → READMEs/ConfigurationFile.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ An example of the configuration file can be obtained by running:
./launch -example-config
```

Below is each section of the configuration with the relevant values and details. If you would like more information regarding the configuration all data is layed out in the [configfile folder](../configfile).
Below is each section of the configuration with the relevant values and details. If you would like more information regarding the configuration all data is laid out in the [configfile folder](../configfile).

## Understanding double rendering

Configuration files have templating built in, see later templating section. This allows for environment variables to be used in the configuration file.
However many of those environment variables will be collected during the secrets and parameters collection phase.

The configuration file is rendered twice when the Launch is started.
The first render will be done on start up, it will produce the configuration with the available environment variables as the container starts.
The Launch will then proceed to collect secrets and parameters. Once complete the configuration is rendered a second time.
The configuration file is rendered twice when Launch starts.
The first render runs on startup and produces the configuration with the environment variables available at that point.
Launch then proceeds to collect secrets and parameters. Once complete, the configuration is rendered a second time.
This allows the configuration to make use of parameters and secrets as part of the configuration. Previously blanked environment settings will now be filled in.

An example use case is using an override for hostname in the syslog logging configuration, or setting a environment variable.
Expand All @@ -32,7 +32,7 @@ Secrets are only collected once at startup.

## process_manager

`process_manager` configures the Launch process itself. It needs to know where to send it's logs and also if it needs to enable debug logging.
`process_manager` configures Launch itself. It needs to know where to send its logs and whether to enable debug logging.

Example:

Expand All @@ -54,7 +54,7 @@ process_manager:

## processes

`processes` tells the Launch what start and where to send the logs. There is 3 sections here: secret_processes, init_processes and main_processes
`processes` tells Launch what to start and where to send the logs. There are 3 sections: `secret_processes`, `init_processes`, and `main_processes`.

Example:

Expand Down Expand Up @@ -142,7 +142,7 @@ default_logger_config:
```
## Logging

Logging is used to tell the Launch to send logs to a logging engine eg. syslog, console, etc...
Logging is used to tell Launch where to send logs — e.g. syslog, console, etc.

Below is the logging configuration. However this is a slightly different one to the rest.

Expand Down
16 changes: 8 additions & 8 deletions READMEs/Logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ A more detailed description of the loggers that are available is documented belo

## Startup logging

The Launch needs to start logging before it is actually capable of logging fully. This is because some loggers require authentication that is expected to be collected by the secrets.
To overcome this the Launch will log to the console until after the secrets have been collected.
Launch needs to start logging before it is fully configured to do so. This is because some loggers require credentials that are expected to be collected by the secrets phase.
To overcome this, Launch logs to the console until after secrets have been collected.
At which point it will read the configuration file and use the specified loggers.

## Default logging

Default values for logging can be set. These values will be used if there is not value present for a process or they will be merged. This allows most the configuration to be setup here, and only specifics to be set at the process level.
Default values for logging can be set. These values are used when no value is present for a process, or they are merged with per-process values. This allows most of the configuration to be set here, with only the specifics overridden at the process level.

Processes will still need to select a logging engine.

Expand All @@ -32,11 +32,11 @@ File logging is only really useful in development environments. In most producti

## Syslog

Syslog is a pretty standard linux way of sending logs. These logs are sent as lines and multiline logs are unfortunetly split.
Syslog is a standard Linux way of sending logs. Messages are sent line-by-line; multiline logs are unfortunately split across multiple entries.

The logger allows you to override the name that you see in syslog. By default it will use the process name. If you set the `program_name` key under the syslog logging configuration it will use that in its place.
The logger allows you to override the name that you see in syslog. By default it uses the process name. If you set the `program_name` key under the syslog logging configuration, that value is used instead.

Due to the logs being presented to Launch via stdout we are not able to know if the log is critical, warning or informational. This information might be available in the text, however the Launch will simply forward on the message with out inspecting its contents by default.
Because logs are presented to Launch via stdout, it cannot determine whether a log line is critical, a warning, or informational. By default, Launch forwards the message without inspecting its contents.

If you set `extract_log_level: true` the logger will attempt to detect the level from your message. There are limitations here and your messages need to be structured correctly.

Expand Down Expand Up @@ -70,6 +70,6 @@ You can combine this with the configuration file templating to make names that r

The normal rules for host names will apply. No special chars, no space, etc...

For easier tracking of the instances themselves you can append the containers hostname to either the process or to the hostname. If you do this you need to include any _ or - as it will simple tack on the hostname.
The can be set at the default configuration OR the process configuration.
For easier tracking you can append the container hostname to either the process tag or the syslog hostname. If you do this you need to include any `_` or `-` separator yourself, as Launch will simply append the hostname directly.
This can be set at the default configuration level or the process configuration level.
Use `append_container_name_to_tag` and `append_container_name_to_hostname` to control these features.
12 changes: 6 additions & 6 deletions READMEs/Secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ The idea is that you can create your own binary to use to collect secrets and ha
Because Launch is the parent process, child processes CAN NOT update the environment of the parent.
Therefore your process can not expose environment variables for later processes to see.
```
To over come this, Launch will read a key value pair in JSON as the stdout of your process and expose those as environment variables for you.
To overcome this, Launch reads the stdout of your process as a JSON key/value object and exposes those as environment variables.

Output expected:

```json
{"key":"value", "key2":"value2"}
```

It is also possible that you process writes to files that other processes can collect. It is expected that no output to STDOUT is given in this case.
It is also possible that your process writes to files that other processes can read. In this case no output to stdout is expected.

With this being said here are the requirements for Secret Processes:
Requirements for secret processes:

1. They must complete successfully to continue execution.
1. They are executed sequentially as shown in the configuration file.
1. output must be valid JSON and the only output on STDOUT.
1. They must exit successfully (exit code 0) to allow execution to continue.
1. They are executed sequentially in the order listed in the configuration file.
1. If they produce stdout, it must be a single valid JSON object and nothing else.

Secrets might not always need to be collected. Consider if you are using in a Dev environment.
Make use of the `skip` field to stop a process from running.
Expand Down
9 changes: 5 additions & 4 deletions build_and_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ if [[ $1 = "gobuild" ]]; then
echo "Building the go project!"
curdir=$(pwd)
echo "Building launch"
CGO_ENABLED=0 go build -a -installsuffix cgo -o launch .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o launch .
cd testbin
echo "Building testbin"
CGO_ENABLED=0 go build -a -installsuffix cgo -o testbin .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o testbin .
cd $curdir
echo "Finished building"
else
echo "Skipping go build"
fi

docker build -t morfien101/launch-test:latest -f Dockerfile.test .
docker build -t morfien101/launch-test:debug -f Dockerfile.debug .
# It's worth noting that distroless only supports linux, so we need to make sure we build for linux/amd64.
docker build --no-cache --platform linux/amd64 -t morfien101/launch-test:latest -f Dockerfile.test .
docker build --no-cache --platform linux/amd64 -t morfien101/launch-test:debug -f Dockerfile.debug .

echo "#########################"
echo "## Running full config ##"
Expand Down
5 changes: 4 additions & 1 deletion bytepipe/bytepipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ func New() *BytePipe {
}

func (bp *BytePipe) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
if p[len(p)-1] != '\n' {
p = append(p, '\n')
}

bp.Ready <- string(p)
return len(p), err
return len(p), nil
}

func (bp *BytePipe) Close() {
Expand Down
16 changes: 0 additions & 16 deletions configfile/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@ func (cf *Config) setDefaultSecretTimeout() {
// NOTE: setDefaultLoggerConfig should be called first
//
func (cf *Config) setDefaultProcessLogger() {
createLoggingConfig := func(proc *Process) {
proc.LoggerConfig = LoggingConfig{}
}
setName := func(proc *Process) {
proc.LoggerConfig.ProcessName = proc.Name
}
Expand All @@ -77,10 +74,6 @@ func (cf *Config) setDefaultProcessLogger() {
}
f := func(procList []*Process) {
for _, proc := range procList {
if &proc.LoggerConfig == nil {
// Create a logging config
createLoggingConfig(proc)
}
if proc.LoggerConfig.ProcessName == "" {
setName(proc)
}
Expand All @@ -95,21 +88,12 @@ func (cf *Config) setDefaultProcessLogger() {
}

func (cf *Config) setDefaultProcessManager() {
if &cf.ProcessManager == nil {
cf.ProcessManager = defaultProcessManager
}
if &cf.ProcessManager.LoggerConfig == nil {
cf.ProcessManager.LoggerConfig = defaultProcessManager.LoggerConfig
}
if len(cf.ProcessManager.LoggerConfig.Engine) == 0 {
cf.ProcessManager.LoggerConfig.Engine = defaultProcessManager.LoggerConfig.Engine
}

// Set defaults for logging engines under process manager context
if cf.ProcessManager.LoggerConfig.Engine == "syslog" {
if &cf.ProcessManager.LoggerConfig.Syslog == nil {
cf.ProcessManager.LoggerConfig.Syslog = defaultProcessManagerSyslog
}
if cf.ProcessManager.LoggerConfig.Syslog.ProgramName == "" {
cf.ProcessManager.LoggerConfig.Syslog.ProgramName = defaultProcessManagerSyslog.ProgramName
}
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ require (
github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae
github.com/silverstagtech/gotracer v0.2.0
github.com/silverstagtech/srslog v0.2.1
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.2.8
)

require (
github.com/spf13/afero v1.9.4 // indirect
golang.org/x/text v0.3.4 // indirect
golang.org/x/text v0.3.8 // indirect
)
Loading