Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@ LABEL org.opencontainers.image.title="Pebblify" \
org.opencontainers.image.vendor="Dockermint" \
org.opencontainers.image.created="${CREATED}"

RUN apk add --no-cache ca-certificates tzdata \
RUN apk add --no-cache ca-certificates tzdata curl \
&& adduser -D -H -u 10000 -s /sbin/nologin appuser

COPY --from=builder /build/pebblify /usr/local/bin/pebblify

EXPOSE 8086 9090

HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8086/healthz/live || exit 1

USER 10000:10000

ENTRYPOINT ["pebblify"]
ENTRYPOINT ["pebblify"]
203 changes: 199 additions & 4 deletions cmd/pebblify/main.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package main

import (
"context"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"time"

"github.com/Dockermint/Pebblify/internal/completion"
"github.com/Dockermint/Pebblify/internal/fsutil"
"github.com/Dockermint/Pebblify/internal/health"
"github.com/Dockermint/Pebblify/internal/migration"
"github.com/Dockermint/Pebblify/internal/prom"
"github.com/Dockermint/Pebblify/internal/state"
"github.com/Dockermint/Pebblify/internal/verify"
)
Expand Down Expand Up @@ -38,6 +44,8 @@ func main() {
recoverCmd(os.Args[2:])
case "verify":
verifyCmd(os.Args[2:])
case "completion":
completionCmd(os.Args[2:])
case "-h", "--help", "help":
usage()
default:
Expand All @@ -64,6 +72,7 @@ Commands:
level-to-pebble Convert a Tendermint/CometBFT data/ directory from LevelDB to PebbleDB
recover Resume a previously interrupted conversion
verify Verify that converted data matches the source
completion Generate or install shell completion scripts
version Show version information

Options for level-to-pebble:
Expand All @@ -85,6 +94,14 @@ Options for verify:
--stop-on-error Stop at first mismatch
-v, --verbose Show each key being verified

Health probes (opt-in):
--health Enable the HTTP health probe server
--health-port P Port for the health server (default: 8086)

Prometheus metrics (opt-in):
--metrics Enable the Prometheus metrics server
--metrics-port P Port for the metrics server (default: 9090)

Global flags:
-h, --help Show this help
-V, --version Show version and exit
Expand All @@ -99,9 +116,94 @@ Examples:
# Verify the converted data
pebblify verify ~/.gaia/data ./output/data

# Convert with health probes enabled
pebblify level-to-pebble --health --health-port 8086 ~/.gaia/data ./output

# Generate bash completion script
pebblify completion bash

# Install zsh completion
pebblify completion install zsh

`, Version)
}

type healthFlags struct {
enabled bool
port int
}

func addHealthFlags(fs *flag.FlagSet) *healthFlags {
hf := &healthFlags{}
fs.BoolVar(&hf.enabled, "health", false, "enable HTTP health probe server")
fs.IntVar(&hf.port, "health-port", 8086, "port for the health server")
return hf
}

type metricsFlags struct {
enabled bool
port int
}

func addMetricsFlags(fs *flag.FlagSet) *metricsFlags {
mf := &metricsFlags{}
fs.BoolVar(&mf.enabled, "metrics", false, "enable Prometheus metrics server")
fs.IntVar(&mf.port, "metrics-port", 9090, "port for the metrics server")
return mf
}

func startMetricsServer(mf *metricsFlags) *prom.Server {
if !mf.enabled {
return nil
}

srv := prom.NewServer(mf.port)

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "metrics server error: %v\n", err)
}
}()

return srv
}

func stopMetricsServer(srv *prom.Server) {
if srv == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}

func startHealthServer(hf *healthFlags) (*health.Server, *health.ProbeState) {
probeState := health.NewProbeState(30 * time.Second)

if !hf.enabled {
return nil, probeState
}

srv := health.NewServer(hf.port, probeState)

go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "health server error: %v\n", err)
}
}()

return srv, probeState
}

func stopHealthServer(srv *health.Server) {
if srv == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}

func levelToPebbleCmd(args []string) {
fs := flag.NewFlagSet("level-to-pebble", flag.ExitOnError)
force := fs.Bool("force", false, "overwrite existing temporary state")
Expand All @@ -112,6 +214,8 @@ func levelToPebbleCmd(args []string) {
tmpDir := fs.String("tmp-dir", "", "directory where .pebblify-tmp will be created (default: system temp)")
verbose := fs.Bool("verbose", false, "enable verbose output")
fs.BoolVar(verbose, "v", false, "alias for --verbose")
hf := addHealthFlags(fs)
mf := addMetricsFlags(fs)

if err := fs.Parse(args); err != nil {
os.Exit(1)
Expand All @@ -125,6 +229,12 @@ func levelToPebbleCmd(args []string) {
os.Exit(1)
}

srv, probeState := startHealthServer(hf)
defer stopHealthServer(srv)

metricsSrv := startMetricsServer(mf)
defer stopMetricsServer(metricsSrv)

src := rest[0]
out := rest[1]

Expand Down Expand Up @@ -158,13 +268,21 @@ func levelToPebbleCmd(args []string) {
}
defer unlock()

probeState.SetStarted()
probeState.SetReady()

ticker := health.NewPingTicker(probeState, 5*time.Second)
defer ticker.Stop()

cfg := &migration.RunConfig{
Workers: *workers,
BatchMemory: *batchMemory,
Verbose: *verbose,
Workers: *workers,
BatchMemory: *batchMemory,
Verbose: *verbose,
MetricsEnabled: mf.enabled,
}

if err := migration.RunLevelToPebble(src, out, cfg, tmpRoot); err != nil {
probeState.SetNotReady()
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
Expand All @@ -178,6 +296,8 @@ func recoverCmd(args []string) {
tmpDir := fs.String("tmp-dir", "", "directory containing .pebblify-tmp (must match conversion)")
verbose := fs.Bool("verbose", false, "enable verbose output")
fs.BoolVar(verbose, "v", false, "alias for --verbose")
hf := addHealthFlags(fs)
mf := addMetricsFlags(fs)

if err := fs.Parse(args); err != nil {
os.Exit(1)
Expand All @@ -189,6 +309,12 @@ func recoverCmd(args []string) {
os.Exit(1)
}

srv, probeState := startHealthServer(hf)
defer stopHealthServer(srv)

metricsSrv := startMetricsServer(mf)
defer stopMetricsServer(metricsSrv)

baseTmpDir := os.TempDir()
if *tmpDir != "" {
baseTmpDir = *tmpDir
Expand All @@ -210,7 +336,14 @@ func recoverCmd(args []string) {
}
defer unlock()

if err := migration.RunRecover(*workers, *batchMemory, tmpRoot, *verbose); err != nil {
probeState.SetStarted()
probeState.SetReady()

ticker := health.NewPingTicker(probeState, 5*time.Second)
defer ticker.Stop()

if err := migration.RunRecover(*workers, *batchMemory, tmpRoot, *verbose, mf.enabled); err != nil {
probeState.SetNotReady()
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
Expand Down Expand Up @@ -249,3 +382,65 @@ func verifyCmd(args []string) {
os.Exit(1)
}
}

func completionCmd(args []string) {
if len(args) == 0 {
completionUsage()
os.Exit(1)
}

switch args[0] {
case "bash":
fmt.Print(completion.GenerateBash())
case "zsh":
fmt.Print(completion.GenerateZsh())
case "install":
completionInstallCmd(args[1:])
default:
fmt.Fprintf(os.Stderr, "unknown shell: %s\n\n", args[0])
completionUsage()
os.Exit(1)
}
}

func completionInstallCmd(args []string) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "missing shell argument\n\n")
completionUsage()
os.Exit(1)
}

var (
dest string
err error
hint string
)

switch args[0] {
case "bash":
dest, err = completion.InstallBash()
hint = fmt.Sprintf("source %s", dest)
case "zsh":
dest, err = completion.InstallZsh()
hint = "autoload -Uz compinit && compinit"
default:
fmt.Fprintf(os.Stderr, "unknown shell: %s\n\n", args[0])
completionUsage()
os.Exit(1)
}

if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}

fmt.Fprintf(os.Stderr, "completion installed to %s\n", dest)
fmt.Fprintf(os.Stderr, "reload with: %s\n", hint)
}

func completionUsage() {
fmt.Fprintf(os.Stderr, `Usage:
pebblify completion <bash|zsh> Print completion script to stdout
pebblify completion install <bash|zsh> Install completion script
`)
}
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
pebblify:
build:
context: .
args:
VERSION: dev
REVISION: local
ports:
- "8086:8086"
- "9090:9090"
volumes:
- ./testdata:/data
entrypoint: ["pebblify", "level-to-pebble", "--health", "--health-port", "8086", "--metrics", "--metrics-port", "9090"]
command: ["/data/source", "/data/output"]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8086/healthz/live"]
interval: 10s
timeout: 3s
start_period: 5s
retries: 3
23 changes: 11 additions & 12 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,33 @@ go 1.25.4

require (
github.com/cockroachdb/pebble v1.1.5
github.com/prometheus/client_golang v1.21.1
github.com/syndtr/goleveldb v1.0.0
)

require (
github.com/DataDog/zstd v1.4.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cockroachdb/errors v1.11.3 // indirect
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/getsentry/sentry-go v0.27.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
)
Loading
Loading