From fdd8a4adc8feda2807af38f7ead37f032883fc46 Mon Sep 17 00:00:00 2001 From: Martin Koppehel Date: Sat, 11 Nov 2023 16:14:39 +0100 Subject: [PATCH] Initial commit --- .github/workflows/docker-image.yaml | 55 +++++++++++++++++ Dockerfile | 35 +++++++++++ config.json | 5 ++ go.mod | 11 ++++ go.sum | 11 ++++ main.go | 96 +++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 .github/workflows/docker-image.yaml create mode 100644 Dockerfile create mode 100644 config.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.github/workflows/docker-image.yaml b/.github/workflows/docker-image.yaml new file mode 100644 index 0000000..2e798d2 --- /dev/null +++ b/.github/workflows/docker-image.yaml @@ -0,0 +1,55 @@ +name: Build Docker Images + +on: + push: + tags: + - '**' + branches: + - 'main' + pull_request: + types: + - "opened" + - "reopened" + - "synchronize" +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + strategy: + matrix: + image: ["signal-http-bridge"] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.image }} + tags: | + type=semver,pattern={{version}} + type=edge + type=ref,event=pr + + - name: Build and push Docker Images + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: "TARGET=${{ matrix.image }}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc9556e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1 + +## +## Build stage. +## +FROM golang:1.21.3-alpine AS build +ENV GO111MODULE=on + +WORKDIR /app + +# Download dependencies +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download + +# Copy app +COPY . . + +# build the actual binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o entry + +## +## Deploy stage +## +FROM alpine:3.18 + +# install common deps +RUN apk add curl wget bash + +# copy the prebuilt file +WORKDIR / +COPY --from=build /app/entry /usr/bin/entry + +# set app as startup app +ENTRYPOINT ["/usr/bin/entry"] diff --git a/config.json b/config.json new file mode 100644 index 0000000..130e691 --- /dev/null +++ b/config.json @@ -0,0 +1,5 @@ +{ + "signals": { + "USR1": "echo 'hallo'" + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..280e965 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/deinstapel/signal-cmd-executor + +go 1.21.3 + +require ( + github.com/abiosoft/lineprefix v0.1.4 // indirect + github.com/fatih/color v1.12.0 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0322118 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/abiosoft/lineprefix v0.1.4 h1:fXu3jc+B2EaS98mTpEL5OH9EKv3scHRb7/gsvlqAD1A= +github.com/abiosoft/lineprefix v0.1.4/go.mod h1:Myq9hfXs8e2OmHFvajp3pHxxThZL645XK+BrEQNvNSs= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b6e225c --- /dev/null +++ b/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/abiosoft/lineprefix" +) + +type Config struct { + Signals map[string]string `json:"signals"` +} + +var signalMap = map[string]syscall.Signal{ + "USR1": syscall.SIGUSR1, + "USR2": syscall.SIGUSR2, + "INT": syscall.SIGINT, + "ABRT": syscall.SIGABRT, + "ALRM": syscall.SIGALRM, + "BUS": syscall.SIGBUS, + "CHLD": syscall.SIGCHLD, + "CLD": syscall.SIGCLD, + "CONT": syscall.SIGCONT, + "FPE": syscall.SIGFPE, + "HUP": syscall.SIGHUP, + "IO": syscall.SIGIO, + "IOT": syscall.SIGIOT, +} + +// handles a single signal +func handleSignal(ctx context.Context, signalName string, command string) { + c := make(chan os.Signal, 3) + signalNumber, ok := signalMap[signalName] + if !ok { + log.Fatalf("failed to find syscall for signal %v\n", signalName) + } + + signal.Notify(c, signalNumber) + + prefix := lineprefix.Prefix(fmt.Sprintf("[CMD SIG%v]", signalName)) + stdoutWrapper := lineprefix.New(prefix, lineprefix.Writer(os.Stdout)) + stderrWrapper := lineprefix.New(prefix, lineprefix.Writer(os.Stderr)) + +outer: + for { + select { + case <-c: + cmd := exec.Command("/bin/bash", "-c", command) + cmd.Stderr = stderrWrapper + cmd.Stdout = stdoutWrapper + if err := cmd.Run(); err != nil { + log.Printf("WARNING: program terminated with error: %v\n", err) + } + case <-ctx.Done(): + break outer + } + } + + signal.Stop(c) + close(c) +} + +// main entry point for the program, loads config file etc. +func main() { + configFile, ok := os.LookupEnv("CONFIG_FILE") + if !ok { + configFile = "/etc/config.json" + } + + configBytes, err := os.ReadFile(configFile) + if err != nil { + log.Fatalf("failed to read config file: %v\n", err) + } + + config := Config{} + if err := json.Unmarshal(configBytes, &config); err != nil { + log.Fatalf("failed to deserialize config file: %v\n", err) + } + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM) + defer cancel() + + for signal, command := range config.Signals { + go handleSignal(ctx, signal, command) + } + + <-ctx.Done() + + log.Printf("exiting") +}