Skip to content

Commit

Permalink
supervisor: lightweight process manager for the storagenode
Browse files Browse the repository at this point in the history
This updates and manages the storagenode process

Change-Id: If8704e91028d7fc86d947244e6e8a21d33c4ab7a
  • Loading branch information
profclems committed Oct 24, 2024
1 parent a187f98 commit ccfaf6f
Show file tree
Hide file tree
Showing 14 changed files with 787 additions and 167 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
/.vs

# Jetbrains
.idea/
.idea/

/test
26 changes: 15 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
ARG DOCKER_PLATFORM
ARG DOCKER_ARCH
ARG GO_DOCKER_PLATFORM

FROM --platform=${GO_DOCKER_PLATFORM:-linux/amd64} golang:1.23-alpine AS builder
ARG CGO_ENABLED=0
WORKDIR /app
COPY go.mod go.sum ./
COPY ./supervisor ./supervisor
COPY ./cmd/supervisor ./cmd/supervisor
RUN mkdir -p /app/bin
ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target="/root/.cache/go-build" go build -o ./bin/supervisor ./cmd/supervisor

FROM --platform=${DOCKER_PLATFORM:-linux/amd64} ${DOCKER_ARCH:-amd64}/debian:bookworm-slim
ARG GOARCH
ARG VERSION_SERVER_URL
ARG SUPERVISOR_SERVER
ENV GOARCH=${GOARCH:-amd64} \
VERSION_SERVER_URL=${VERSION_SERVER_URL:-https://version.storj.io} \
SUPERVISOR_SERVER=${SUPERVISOR_SERVER:-unix}

RUN apt-get update
RUN apt-get install -y --no-install-recommends ca-certificates supervisor unzip wget
RUN apt-get install -y --no-install-recommends ca-certificates
RUN update-ca-certificates

RUN mkdir -p /var/log/supervisor /app

COPY docker/ /

# set permissions to allow non-root access
RUN chmod -R a+rw /etc/supervisor /var/log/supervisor /app
# remove the default supervisord.conf
RUN rm -rf /etc/supervisord.conf
# create a symlink to custom supervisord config file at the default location
RUN ln -s /etc/supervisor/supervisord.conf /etc/supervisord.conf
RUN mkdir -p /app/bin
COPY --from=builder /app/bin/supervisor /app/bin/supervisor

EXPOSE 28967
EXPOSE 14002
Expand All @@ -37,4 +41,4 @@ ENV ADDRESS="" \
SETUP="false" \
AUTO_UPDATE="true" \
LOG_LEVEL="" \
BINARY_STORE_DIR="/app/config/bin"
BINARY_STORE_DIR="/app/config/bin" \
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ images: amd64-image arm64-image arm32-image ## Build storagenode Docker images
amd64-image: ## Build storagenode Docker image for amd64
${DOCKER_BUILDX} --load --pull=true -t storjlabs/storagenode:${TAG}${CUSTOMTAG}-amd64 \
--platform=linux/amd64 \
--build-arg=GOARCH=amd64 \
--build-arg=GO_DOCKER_PLATFORM=linux/amd64 \
-f Dockerfile .

.PHONY: arm32-image
arm32-image: ## Build storagenode Docker image for arm32v5
${DOCKER_BUILDX} --load --pull=true -t storjlabs/storagenode:${TAG}${CUSTOMTAG}-arm32v5 \
--platform=linux/arm/v5 \
--build-arg=GOARCH=arm --build-arg=DOCKER_ARCH=arm32v5 --build-arg=DOCKER_PLATFORM=linux/arm/v5 \
--build-arg=GO_DOCKER_PLATFORM=linux/arm/v6 --build-arg=DOCKER_ARCH=arm32v5 --build-arg=DOCKER_PLATFORM=linux/arm/v5 \
-f Dockerfile .

.PHONY: arm64-image
arm64-image: ## Build storagenode Docker image for arm64v8
${DOCKER_BUILDX} --load --pull=true -t storjlabs/storagenode:${TAG}${CUSTOMTAG}-arm64v8 \
--platform=linux/arm64/v8 \
--build-arg=GOARCH=arm64 --build-arg=DOCKER_ARCH=arm64v8 --build-arg=DOCKER_PLATFORM=linux/arm64 \
--build-arg=GO_DOCKER_PLATFORM=linux/arm64/v8 --build-arg=DOCKER_ARCH=arm64v8 --build-arg=DOCKER_PLATFORM=linux/arm64 \
-f Dockerfile .

.PHONY: pull-images
Expand Down
161 changes: 161 additions & 0 deletions cmd/supervisor/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package main

import (
"context"
"log"
"log/slog"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"

"github.com/profclems/go-dotenv"
"github.com/spf13/cobra"
"github.com/zeebo/errs"

"storj.io/common/identity"
"storj.io/common/storj"
"storj.io/common/sync2"
"storj.io/common/version"
"storj.io/storagenode-docker/supervisor"
"storj.io/storj/private/version/checker"
)

type config struct {
Interval time.Duration `env:"STORJ_SUPERVISOR_UPDATE_CHECK_INTERVAL" default:"15m" description:"Interval in seconds to check for updates"`
CheckTimeout time.Duration `env:"STORJ_SUPERVISOR_UPDATE_CHECK_TIMEOUT" default:"1m" description:"Request timeout for checking for updates"`
BinaryLocation string `env:"STORJ_SUPERVISOR_BINARY_LOCATION" default:"/app/bin/storagenode" description:"Path to the storagenode binary"`
BinaryStoreDir string `env:"STORJ_SUPERVISOR_BINARY_STORE_DIR" default:"/app/config/bin" description:"Directory to store storagenode backup binaries"`
VersionServerAddress string `env:"STORJ_SUPERVISOR_VERSION_SERVER_ADDRESS" default:"https://version.storj.io" description:"URL of the version server"`

NodeID storj.NodeID `env:"STORJ_SUPERVISOR_NODE_ID" description:"Node ID. If not provided, it will be read from the identity file"`
IdentityDir string `env:"STORJ_SUPERVISOR_IDENTITY_DIR" default:"/app/identity" description:"Path to the identity directory. Required if node ID is not provided"`
}

func main() {
ctx := getContext()
slog.SetDefault(slog.With("service", "supervisor"))

rootCmd := &cobra.Command{
Use: "supervisor",
Short: "A process manager for the storagenode",
}

var cfg config
execCmd := &cobra.Command{
Use: "exec STORAGENODE_COMMAND",
Short: "Execute the storagenode binary with supervisor",
Example: `supervisor exec /path/to/storagenode run --config-dir=/path/to/config`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
env := dotenv.New()
err := env.Unmarshal(&cfg)
if err != nil {
return err
}

log.Println(cfg)
return execSupervisor(ctx, cfg, args)
},
DisableFlagParsing: true,
}

rootCmd.AddCommand(execCmd)

err := rootCmd.ExecuteContext(ctx)
if err != nil && !errs.Is(err, context.Canceled) {
slog.Info("error executing command", "error", err)
os.Exit(1)
}
}

func execSupervisor(ctx context.Context, cfg config, args []string) error {
if cfg.NodeID.IsZero() {
fullID, err := identity.Config{
CertPath: filepath.Join(cfg.IdentityDir, "identity.cert"),
KeyPath: filepath.Join(cfg.IdentityDir, "identity.key"),
}.Load()

if err != nil {
return err
}
cfg.NodeID = fullID.ID
}

process := supervisor.NewProcess(cfg.NodeID, cfg.BinaryLocation, cfg.BinaryStoreDir, args)

versionChecker := checker.New(checker.ClientConfig{
ServerAddress: cfg.VersionServerAddress,
RequestTimeout: cfg.CheckTimeout,
})

updater := supervisor.NewUpdater(versionChecker)

// check that storagenode binary exists
if _, err := os.Stat(cfg.BinaryLocation); err != nil {
// check store dir for backup binary
backupBinary := filepath.Join(cfg.BinaryStoreDir, "storagenode")
if _, err := os.Stat(backupBinary); err == nil {
// copy backup binary to binary location
if err := copyBinary(ctx, cfg.BinaryLocation, backupBinary); err != nil {
return err
}
} else {
log.Println("Binary does not exist, downloading new binary")
// binary does not exist, download it
_, err := updater.Update(ctx, process, version.SemVer{})
if err != nil {
return err
}
}
}

err := supervisor.New(updater, process, cfg.Interval).Run(ctx)
if err != nil {
slog.Info("Supervisor stopped", "error", err)
return err
}

return nil
}

func getContext() context.Context {
ctx, cancel := context.WithCancel(context.Background())
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-c
slog.Info("Got a signal from the OS:", "signal", sig)
signal.Stop(c)
cancel()
}()

return ctx
}

func copyBinary(ctx context.Context, dest, src string) error {
srcFile, err := os.Open(src)
if err != nil {
return errs.Wrap(err)
}
defer func() {
err = errs.Combine(err, srcFile.Close())
}()

destFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
return errs.Wrap(err)
}

defer func() {
err = errs.Combine(err, destFile.Close())
}()

_, err = sync2.Copy(ctx, destFile, srcFile)
if err != nil {
return errs.Wrap(err)
}

return nil
}
103 changes: 7 additions & 96 deletions docker/entrypoint
Original file line number Diff line number Diff line change
@@ -1,62 +1,7 @@
#!/bin/bash
set -euo pipefail

BINARY_DIR=/app/bin
BINARY_STORE_DIR=${BINARY_STORE_DIR:-/app/config/bin}

get_default_url() {
process=$1
version=$2
wget -O- "${VERSION_SERVER_URL}/processes/${process}/${version}/url?os=linux&arch=${GOARCH}"
}

copy_binary() {
binary=$1
mkdir -p "${BINARY_DIR}"
cp "${BINARY_STORE_DIR}/${binary}" "${BINARY_DIR}/${binary}"
chmod u+x "${BINARY_DIR}/${binary}"
}

get_binary() {
binary=$1
url=$2
wget -O "/tmp/${binary}.zip" "${url}"
mkdir -p "${BINARY_STORE_DIR}"
unzip -p "/tmp/${binary}.zip" > "${BINARY_STORE_DIR}/${binary}"
rm "/tmp/${binary}.zip"
}

should_update() {
binary=$1
copy_binary ${binary}
for version in minimum suggested; do
if ${BINARY_DIR}/storagenode-updater should-update ${binary} \
--binary-location "${BINARY_DIR}/${binary}" \
--identity-dir identity \
--version.server-address="${VERSION_SERVER_URL}" 2>/dev/null
then
echo "downloading ${binary}"
get_binary ${binary} "$(get_default_url ${binary} ${version})"
copy_binary ${binary}
else
break
fi
done
}

# install storagenode and storagenode-updater binaries
# during run of the container to not to release new docker image
# on each new version of the storagenode binary.
for binary in storagenode-updater storagenode; do
if [ ! -f "${BINARY_STORE_DIR}/${binary}" ]; then
echo "downloading ${binary}"
get_binary ${binary} "$(get_default_url ${binary} minimum)"
fi
should_update ${binary}
done

SUPERVISOR_SERVER="${SUPERVISOR_SERVER:-unix}"

RUN_PARAMS="${RUN_PARAMS:-} --config-dir config"
RUN_PARAMS="${RUN_PARAMS} --identity-dir identity"

Expand Down Expand Up @@ -92,47 +37,13 @@ if [ -n "${LOG_LEVEL:-}" ]; then
fi

if [ "${SETUP:-}" = "true" ]; then
echo "Running ${BINARY_DIR}/storagenode setup $SNO_RUN_PARAMS ${*}"
exec ${BINARY_DIR}/storagenode setup ${SNO_RUN_PARAMS} ${*}
echo "Running /app/bin/storagenode setup $SNO_RUN_PARAMS ${*}"
exec /app/bin/storagenode setup ${SNO_RUN_PARAMS} ${*}
else
sed -i \
"s#^command=/app/bin/storagenode-updater\$#command=${BINARY_DIR}/storagenode-updater run --binary-location ${BINARY_DIR}/storagenode ${RUN_PARAMS} #" \
/etc/supervisor/supervisord.conf

sed -i \
"s#^command=/app/bin/storagenode\$#command=${BINARY_DIR}/storagenode run ${SNO_RUN_PARAMS} ${*}#" \
/etc/supervisor/supervisord.conf

# remove explicit user flag when container is run as non-root
if [ $EUID != "0" ]; then
sed -i "s#^user=root##" /etc/supervisor/supervisord.conf
fi

#
case ${SUPERVISOR_SERVER} in
unix) # default
;;
public_port)
# replace unix_http_server section to inet_http_server
sed -i "s#^\[unix_http_server\]\$#\[inet_http_server\]#" /etc/supervisor/supervisord.conf
# replace unix socket file with tcp public port
sed -i "s#^file=/etc/supervisor/supervisor.sock\$#port=*:9001#" /etc/supervisor/supervisord.conf
# set server url to http server address
sed -i "s#^serverurl=unix:///etc/supervisor/supervisor.sock\$#serverurl=http://127.0.0.1:9001#" /etc/supervisor/supervisord.conf
;;
private_port)
# replace unix_http_server section to inet_http_server
sed -i "s#^\[unix_http_server\]\$#\[inet_http_server\]#" /etc/supervisor/supervisord.conf
# replace unix socket file with tcp private port .i.e. listens on only localhost
sed -i "s#^file=/etc/supervisor/supervisor.sock\$#port=127.0.0.1:9001#" /etc/supervisor/supervisord.conf
# set server url to http server address
sed -i "s#^serverurl=unix:///etc/supervisor/supervisor.sock\$#serverurl=http://127.0.0.1:9001#" /etc/supervisor/supervisord.conf
;;
*)
echo "Invalid value '${SUPERVISOR_SERVER}' for SUPERVISOR_SERVER. Expected 'unix', 'public_port' or 'private_port'"
exit 1
;;
esac
export STORJ_SUPERVISOR_BINARY_STORE_DIR="${BINARY_STORE_DIR}"
export STORJ_SUPERVISOR_VERSION_SERVER_ADDRESS="${VERSION_SERVER_URL}"
export STORJ_SUPERVISOR_BINARY_LOCATION="/app/bin/storagenode"

exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
echo "Running /app/bin/supervisor exec /app/bin/storagenode run $SNO_RUN_PARAMS ${*}"
exec /app/bin/supervisor exec /app/bin/storagenode run $SNO_RUN_PARAMS ${*}
fi
Loading

0 comments on commit ccfaf6f

Please sign in to comment.