diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f09e0ce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +.github +LICENSE.md +README.md +SECURITY.md +Makefile +supercronic diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ab6f9b..7b02085 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: - name: install golang uses: actions/setup-go@v5 with: - go-version: 1.23.1 + go-version: 1.23.2 - name: checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f393d77..cd21438 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: branches: [master] pull_request: branches: [master] + workflow_dispatch: jobs: build: @@ -12,28 +13,31 @@ jobs: strategy: matrix: - go-version: [1.23.1] + go-version: [1.23.2] steps: - - name: install golang - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.go-version }} - - - name: install bats - run: | - git clone https://github.com/bats-core/bats-core.git --branch v1.11.0 --depth 1 "${HOME}/bats" - echo "${HOME}/bats/bin" >> $GITHUB_PATH - - - name: install govulncheck - run: | - go install golang.org/x/vuln/cmd/govulncheck@latest - - - name: checkout code - uses: actions/checkout@v4 - - - name: run tests - run: make test - - - name: run vuln check - run: make vulncheck + - name: install golang + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Setup Bats and bats libs + id: setup-bats + uses: bats-core/bats-action@3.0.0 + + - name: install govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: checkout code + uses: actions/checkout@v4 + + - name: run tests + run: make test + env: + BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }} + - name: run vuln check + run: make vulncheck diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c8fdae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1 + +# Create a stage for building the application. +ARG GO_VERSION=1.23.2 +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build +WORKDIR /src + +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,source=go.sum,target=go.sum \ + --mount=type=bind,source=go.mod,target=go.mod \ + go mod download -x + +# This is the architecture you're building for, which is passed in by the builder. +# Placing it here allows the previous steps to be cached across architectures. +ARG TARGETARCH +ARG VERSION="" + +RUN --mount=type=cache,target=/go/pkg/mod/ \ + --mount=type=bind,target=. \ + CGO_ENABLED=0 GOARCH=$TARGETARCH \ + go build -ldflags "-X main.Version=${VERSION}" \ + -o /bin/supercronic . + +################################################################################ +FROM alpine:latest AS final + +RUN --mount=type=cache,target=/var/cache/apk \ + apk --update add \ + ca-certificates \ + tzdata \ + && \ + update-ca-certificates + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --system \ + --no-create-home \ + --uid "${UID}" \ + supercronic +USER supercronic + +COPY --from=build /bin/supercronic /bin/ + +ENTRYPOINT [ "/bin/supercronic" ] diff --git a/Makefile b/Makefile index 7df77ee..8ff9b2c 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,13 @@ deps: go mod vendor .PHONY: build -build: $(GOFILES) +build: go build -ldflags "-X main.Version=${VERSION}" +.PHONY: docker-build +docker-build: + docker build -t supercronic:${VERSION} --build-arg VERSION=${VERSION} . + .PHONY: unit unit: go test -v -race $$(go list ./... | grep -v /vendor/) @@ -17,7 +21,7 @@ unit: .PHONY: integration integration: VERSION=v1337 -integration: build +integration: build docker-build bats integration .PHONY: test diff --git a/go.mod b/go.mod index 93ea420..2bcea62 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/aptible/supercronic -go 1.23.0 +go 1.23.2 require ( github.com/evalphobia/logrus_sentry v0.8.2 github.com/fsnotify/fsnotify v1.7.0 - github.com/prometheus/client_golang v1.20.2 + github.com/prometheus/client_golang v1.20.5 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 ) @@ -16,16 +16,16 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/getsentry/raven-go v0.2.0 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/kr/text v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.57.0 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - golang.org/x/sys v0.24.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + golang.org/x/sys v0.26.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cb5ebe5..e18707f 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JY github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -30,16 +30,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= -github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY= -github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -47,10 +47,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/integration/test.bats b/integration/test.bats index 8eb2060..f4c0695 100755 --- a/integration/test.bats +++ b/integration/test.bats @@ -7,9 +7,15 @@ function run_supercronic() { "${BATS_TEST_DIRNAME}/../supercronic" ${SUPERCRONIC_ARGS:-} "$crontab" 2>&1 } -setup () { +setup() { WORK_DIR="$(mktemp -d)" export WORK_DIR + + export BATS_LIB_PATH=${BATS_LIB_PATH:-"/usr/lib"} + bats_load_library bats-assert + bats_load_library bats-support + # mock version + export VERSION=v1337 } teardown() { @@ -18,7 +24,7 @@ teardown() { wait_for() { for i in $(seq 0 50); do - if "$@" > /dev/null 2>&1; then + if "$@" >/dev/null 2>&1; then return 0 fi sleep 0.1 @@ -28,8 +34,8 @@ wait_for() { } @test "it prints the version" { - run "${BATS_TEST_DIRNAME}/../supercronic" -version - [[ "$output" =~ ^v1337$ ]] + run "${BATS_TEST_DIRNAME}/../supercronic" -version + assert_output $VERSION } @test "it starts" { @@ -108,33 +114,31 @@ wait_for() { } @test "it run as pid 1 and reap zombie process" { - out="${WORK_DIR}/zombie-crontab-out" + run timeout 5s docker run \ + -v "${BATS_TEST_DIRNAME}/zombie.crontab":/test.crontab \ + --rm supercronic:${VERSION} /test.crontab - # run in new process namespace - sudo timeout 10s unshare --fork --pid --mount-proc \ - ${BATS_TEST_DIRNAME}/../supercronic "${BATS_TEST_DIRNAME}/zombie.crontab" >"$out" 2>&1 & - local pid=$! - sleep 3 + assert_equal $status 124 # timeout exit code - kill -TERM ${pid} # todo: use other method to detect zombie cleanup - wait_for grep "reaper cleanup: pid=" "$out" + assert_line --partial 'reaping dead processes' + assert_line --partial "reaper cleanup: pid=" } - @test "it run as pid 1 and normal crontab no error" { - out="${WORK_DIR}/normal-crontab-out" - # sleep 30 seconds occur found bug # FIXME: other way to detect - sudo timeout 30s unshare --fork --pid --mount-proc \ - "${BATS_TEST_DIRNAME}/../supercronic" "${BATS_TEST_DIRNAME}/normal.crontab" >"$out" 2>&1 & # https://github.com/aptible/supercronic/issues/171 - local pid=$! - local foundErr - - sleep 29.5 - kill -TERM ${pid} - grep "waitid: no child processes" "$out" && foundErr=1 - [[ $foundErr != 1 ]] + run timeout 30s docker run \ + -v "${BATS_TEST_DIRNAME}/normal.crontab":/normal.crontab \ + --rm supercronic:${VERSION} /normal.crontab + + assert_equal $status 124 # timeout exit code + + refute_line --partial 'waitid: no child processes' + assert_line --partial 'reaping dead processes' + assert_line --partial 'msg=1' # normal output + refute_line --partial 'failed' + # https://github.com/aptible/supercronic/issues/177 + refute_line --partial 'no such file or directory' } diff --git a/main.go b/main.go index 6b4c508..4c06d03 100644 --- a/main.go +++ b/main.go @@ -123,7 +123,7 @@ func main() { forkExec() return } - + logrus.Warn("process reaping disabled, not pid 1") } crontabFileName := flag.Args()[0] @@ -184,7 +184,7 @@ func main() { defer func() { if err := promServerShutdownClosure(); err != nil { - logrus.Fatalf("prometheus http shutdown failed: %s", err.Error()) + logrus.Errorf("prometheus http shutdown failed: %s", err.Error()) } }() } diff --git a/prometheus_metrics/prommetrics.go b/prometheus_metrics/prommetrics.go index c225895..bf51e01 100644 --- a/prometheus_metrics/prommetrics.go +++ b/prometheus_metrics/prommetrics.go @@ -2,6 +2,7 @@ package prometheus_metrics import ( "context" + "errors" "fmt" "net" "net/http" @@ -55,7 +56,7 @@ func NewPrometheusMetrics() PrometheusMetrics { pm.CronsSuccessCounter = *prometheus.NewCounterVec( prometheus.CounterOpts{ Name: genMetricName("successful_executions"), - Help: "count of successul cron executions", + Help: "count of successful cron executions", }, cronLabels, ) @@ -140,17 +141,23 @@ func InitHTTPServer(listenAddr string, shutdownContext context.Context) (func() http.Handle("/metrics", promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(` + _, err := w.Write([]byte(` Supercronic

Supercronic

Metrics

`)) + if err != nil { + logrus.Warnf("failed to write response on '/': %s", err.Error()) + } }) http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(`OK`)) + _, err := w.Write([]byte(`OK`)) + if err != nil { + logrus.Warnf("failed to write response on '/health': %s", err.Error()) + } }) shutdownClosure := func() error { @@ -164,7 +171,9 @@ func InitHTTPServer(listenAddr string, shutdownContext context.Context) (func() go func() { if err := promSrv.Serve(listener); err != nil { - logrus.Fatalf("prometheus http serve failed: %s", err.Error()) + if !errors.Is(err, http.ErrServerClosed) { + logrus.Fatalf("prometheus http serve failed: %s", err.Error()) + } } }() diff --git a/reaper.go b/reaper.go index a271bbd..17445e6 100644 --- a/reaper.go +++ b/reaper.go @@ -16,6 +16,11 @@ func forkExec() { logrus.Fatalf("Failed to get current working directory: %s", err.Error()) return } + exe, err := os.Executable() + if err != nil { + logrus.Fatalf("Failed to get executable %s", err.Error()) + return + } pattrs := &syscall.ProcAttr{ Dir: pwd, @@ -28,7 +33,7 @@ func forkExec() { } args := make([]string, 0, len(os.Args)+1) // disable reaping for supercronic, avoid no sense warning - args = append(args, os.Args[0], "-no-reap") + args = append(args, exe, "-no-reap") args = append(args, os.Args[1:]...) pid, err := syscall.ForkExec(args[0], args, pattrs)