Skip to content

Commit

Permalink
Initial commit. Works.
Browse files Browse the repository at this point in the history
  • Loading branch information
morfien101 committed Sep 10, 2019
0 parents commit 4a613a6
Show file tree
Hide file tree
Showing 40 changed files with 3,128 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/launch
/testbin/testbin
# Available publically and likely to fall out of date.
# https://help.papertrailapp.com/kb/configuration/encrypting-remote-syslog-with-tls-ssl/
papertrail_cert.crt

# while I sort these out
READMEs/
8 changes: 8 additions & 0 deletions Dockerfile.debug
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM ubuntu:latest

USER nobody

ADD ./launch* /
ADD ./testbin/testbin /testbin

ENTRYPOINT [ "/launch" ]
10 changes: 10 additions & 0 deletions Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM gcr.io/distroless/base

ADD ./passwd /etc/passwd

USER nobody

ADD ./launch* /
ADD ./testbin/testbin /testbin

ENTRYPOINT [ "/launch" ]
101 changes: 101 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Launch

A simple runtime-agnostic process manager that eases Docker services startup and logging.
Consider it a half way house between Kubernetes and Docker.

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.

## What can Launch help with

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.

* 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.

## TODO

features still in the back burner:

* [ ] delayed start on processes.
* [ ] health check processes - not sure how to do this yet
* [ ] restart failed main processes if configured
* [ ] rendering of configuration file trigger after init processes. To allow for secrets or configuration to be collected.

## Logging

Launch has the following statement in grained into its design which guides its actions on logging:

1. Development teams should not have to worry about where logs are going during creation of their projects.
1. A change to where logs end up should not result in projects having to refactor source code.
1. It is the responsibility of the developer to push all logging out to STDOUT and STDERR. Launch will collect and forward the logs onto the relevant logging engine defined by the configuration of the project.

Launch does however give the choice to the developer of where they would like the logs to end up. This does not contradict points 1 and 2 because the developer has the choice at run time rather than at development time.

Logging is important to all applications. However that importance does not trump the running of the service. Therefore all logging plugins will send logs in `Best Effort` mode.

Logging engines available:

1. Console
1. DevNull
1. File with rotation
1. Syslog

See
[Logging Documentation](./READMEs/Logging.MD)
for details on each logging engine.

The use of Launch's logging engines are optional. You could if you wanted to, setup filebeat in the container and read from files.
Use the console logger for any stray logs.
Another option is to use the file logger to feed the files that filebeat can watch.

## Processes

Launch has 2 processes types when running.

* Init processes
* Main Processes

### Init Processes

Initialization Processes are used to get the environment ready for the main processes to run.
Init processes are run sequentially in the order that they are defined in the configuration file.
These processes **MUST** finish successfully (exit code 0) for the next process to start. Only once all init processes are complete will the main processes start. If a single init process fails Launch will stop further processes from starting and terminate.

### Main Processes

Main processes are processes that need to run continually. They dictate the lifespan of the container. Multiple main processes can be run however currently all of then **MUST** be running in order for the container to be considered as healthy.

If a main process terminates for any reason then launch will send termination signals to all remaining main processes. It will give them a grace period to terminate after which it will forcefully terminate them. The grace period is configuration driven so you have can give the process the time it needs to wrap up any tasks. The default grace period is 30 seconds.

## Run time

Launch effectively acts as an init system. It does this to ease administration in container clusters by wrapping everything a application needs to run including any sidecar services for logging, metrics and data harvesting.

Launch is aware that it is not the first process and knows that it can be terminated by a controller of some kind. To this effect Launch will forward on selected signalling that it gets to the underlying processes.

Signals currently supported for forwarding:

* SIGTERM
* SIGINT

Launch is designed to work in a container no other signalling is expected.

## 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 and what tags to put on logs.

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

The configuration file has many sections that are documented in the
[README dedicated to configuration](./READMEs/ConfigurationFile.MD).

## Contributing

This project is still very much in active development. Expect changes and improvements.

There is a simple build script that does the current integration testing.

Use `./build_and_test.sh` to run only the tests
Use `./build_and_test.sh` gobuild" to also rebuild the go project
37 changes: 37 additions & 0 deletions build_and_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
# Use "./build_and_test.sh" to run only the tests
# Use "./build_and_test.sh gobuild" to also rebuild the go project

if [[ $1 = "gobuild" ]]; then
echo "Building the go project!"
curdir=$(pwd)
echo "Building launch"
CGO_ENABLED=0 go build -a -installsuffix cgo -o launch .
cd testbin
echo "Building testbin"
CGO_ENABLED=0 go build -a -installsuffix cgo -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 .

echo "#########################"
echo "## Running full config ##"
echo "#########################"
docker run \
-it \
-v $(pwd)/launch.yaml:/launch.yaml \
morfien101/launch-test:latest

#echo "############################"
#echo "## Running minimal config ##"
#echo "############################"
#
#docker run \
# -it \
# -v $(pwd)/launch_minimal.yaml:/launch.yaml \
# morfien101/launch-test:latest
120 changes: 120 additions & 0 deletions configfile/configfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package configfile

import (
"fmt"
"io/ioutil"

"github.com/morfien101/launch/configfile/templating"
"gopkg.in/yaml.v2"
)

// Config is a struct that represents the YAML file that we want to pass in.
type Config struct {
ProcessManager ProcessManager `yaml:"process_manager"`
Processes Processes `yaml:"processes"`
DefaultLoggerConfig DefaultLoggerDetails `yaml:"default_logger_config"`
}

// New will return a new config file if one can be read from the location
// specified. An error is also returned if something goes wrong.
func New(filePath string) (*Config, error) {
// Digest the config file
fileBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("Could not read config file. Error: %s", err)
}

decodedYaml, err := templating.GenerateTemplate(fileBytes)
if err != nil {
return nil, fmt.Errorf("Failed to decode template. Error: %s", err)
}
newConfig := &Config{}
if err := yaml.Unmarshal(decodedYaml, newConfig); err != nil {
return nil, fmt.Errorf("Failed to unmarshal yaml. Error: %s", err)
}

newConfig.setDefaultLoggerConfig()
newConfig.setDefaultProcessLogger()
newConfig.setDefaultProcessManager()
newConfig.setDefaultProcessTimeout()

return newConfig, nil
}

// setDefaultLoggerConfig will setup a default logging config if one doesn't already exist.
func (cf *Config) setDefaultLoggerConfig() {
if &cf.DefaultLoggerConfig == nil {
cf.DefaultLoggerConfig = DefaultLoggerDetails{
Config: defaultLoggingEngine,
}
}
if cf.DefaultLoggerConfig.Config.Engine == "" {
cf.DefaultLoggerConfig.Config.Engine = defaultLoggingEngine.Engine
}
}

// setDefaultProcessLogger will go through the processes and set the default logging if there is
// nothing set. The following rules will apply
// The logging engine will be the default engine
// The process logging name should the be name given to the process
//
// 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
}
setEngine := func(proc *Process) {
proc.LoggerConfig.Engine = cf.DefaultLoggerConfig.Config.Engine
}
f := func(procList []*Process) {
for _, proc := range procList {
if &proc.LoggerConfig == nil {
// Create a logging config
createLoggingConfig(proc)
}
if proc.LoggerConfig.ProcessName == "" {
setName(proc)
}
if proc.LoggerConfig.Engine == "" {
setEngine(proc)
}
}
}

f(cf.Processes.InitProcesses)
f(cf.Processes.MainProcesses)
}

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

func (cf *Config) setDefaultProcessTimeout() {
f := func(procs []*Process) {
for _, proc := range procs {
if proc.TermTimeout <= 0 {
proc.TermTimeout = defaultProcTimeout
}
}
}

f(cf.Processes.InitProcesses)
f(cf.Processes.MainProcesses)
}

func (cf Config) String() string {
output, _ := yaml.Marshal(cf)
return string(output)
}
Loading

0 comments on commit 4a613a6

Please sign in to comment.