From db74b13ce2714d4b7761b581b3ae5ac6c93b1ecc Mon Sep 17 00:00:00 2001 From: stackops Date: Thu, 9 Apr 2026 14:05:23 +0300 Subject: [PATCH] test app with postgres and redis healthchecks --- .gitignore | 2 + Dockerfile | 12 +++++ go.mod | 14 +++++ go.sum | 24 +++++++++ main.go | 139 +++++++++++++++++++++++++++++++++++++++++++++++++ stackfile.toml | 22 ++++++++ 6 files changed, 213 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 stackfile.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bafe145 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.exe + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4789f15 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.25-alpine AS builder +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o /test-app . + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates +COPY --from=builder /test-app /usr/local/bin/test-app +EXPOSE 8080 +ENTRYPOINT ["test-app"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5823bf1 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module test-app + +go 1.25.6 + +require ( + github.com/lib/pq v1.12.3 + github.com/redis/go-redis/v9 v9.18.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + go.uber.org/atomic v1.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a72a9c7 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +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/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..22272a5 --- /dev/null +++ b/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "net/http" + "os" + "time" + + _ "github.com/lib/pq" + "github.com/redis/go-redis/v9" +) + +type result struct { + Name string + OK bool + Message string +} + +func env(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +func testPostgres() result { + dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + env("POSTGRES_HOST", "localhost"), + env("POSTGRES_PORT", "5432"), + env("POSTGRES_USER", "app"), + env("POSTGRES_PASSWORD", ""), + env("POSTGRES_DB", "app"), + ) + db, err := sql.Open("postgres", dsn) + if err != nil { + return result{"PostgreSQL", false, fmt.Sprintf("open: %v", err)} + } + defer db.Close() + db.SetConnMaxLifetime(5 * time.Second) + + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS healthcheck (id SERIAL PRIMARY KEY, checked_at TIMESTAMPTZ DEFAULT NOW(), msg TEXT)`) + if err != nil { + return result{"PostgreSQL", false, fmt.Sprintf("create table: %v", err)} + } + + _, err = db.Exec(`INSERT INTO healthcheck (msg) VALUES ($1)`, fmt.Sprintf("ping at %s", time.Now().Format(time.RFC3339))) + if err != nil { + return result{"PostgreSQL", false, fmt.Sprintf("insert: %v", err)} + } + + var count int + err = db.QueryRow(`SELECT COUNT(*) FROM healthcheck`).Scan(&count) + if err != nil { + return result{"PostgreSQL", false, fmt.Sprintf("count: %v", err)} + } + + return result{"PostgreSQL", true, fmt.Sprintf("OK — %d rows in healthcheck", count)} +} + +func testRedis() result { + addr := fmt.Sprintf("%s:%s", + env("REDIS_HOST", "localhost"), + env("REDIS_PORT", "6379"), + ) + rdb := redis.NewClient(&redis.Options{Addr: addr}) + defer rdb.Close() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := rdb.Set(ctx, "test-key", "hello from test-app", 0).Err() + if err != nil { + return result{"Redis", false, fmt.Sprintf("SET: %v", err)} + } + + val, err := rdb.Get(ctx, "test-key").Result() + if err != nil { + return result{"Redis", false, fmt.Sprintf("GET: %v", err)} + } + + return result{"Redis", true, fmt.Sprintf("OK — GET test-key = %q", val)} +} + +func handler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/healthz" { + w.WriteHeader(200) + fmt.Fprint(w, "ok") + return + } + + pg := testPostgres() + rd := testRedis() + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, ` + + + StackOps Test App + + + +

StackOps Test App

+

Dependency connectivity checks

+`) + for _, t := range []result{pg, rd} { + cls := "ok" + if !t.OK { + cls = "fail" + } + fmt.Fprintf(w, `
+
%s
+
%s
+
+`, cls, t.Name, t.Message) + } + fmt.Fprintf(w, `
Checked at %s
+ +`, time.Now().Format(time.RFC3339)) +} + +func main() { + http.HandleFunc("/", handler) + port := env("PORT", "8080") + log.Printf("test-app listening on :%s", port) + log.Fatal(http.ListenAndServe(":"+port, nil)) +} diff --git a/stackfile.toml b/stackfile.toml new file mode 100644 index 0000000..f04d7a4 --- /dev/null +++ b/stackfile.toml @@ -0,0 +1,22 @@ +[app] +name = "test-app" +image = "git.nodeup.ru/stackops/test-app:latest" +replicas = 1 +port = 8080 + +[ingress] +host = "test.app.nodeup.ru" +path = "/" +tls = true + +[health] +liveness = "/healthz" +readiness = "/healthz" + +[dependencies.postgres] +version = "15" +storage = "1Gi" + +[dependencies.redis] +version = "7" +storage = "1Gi"