4 Commits

Author SHA1 Message Date
82de580879 feat(ekko): add WithTrustOptions for CDN IP trust configuration
Allow callers to append additional echo.TrustOption values to the
default IP extraction configuration. This enables trusting CDN IP
ranges (e.g. Fastly) when extracting client IPs from X-Forwarded-For.
2026-03-08 18:31:44 -07:00
92b202037a fix(ekko): store service name and add fallback for Server header
The name parameter passed to ekko.New() was never stored on the struct,
causing the HTTP Server header to be malformed (e.g. "/vdev-snapshot+hash"
instead of "warmform/vdev-snapshot+hash").

Store the name in the struct literal and add a fallback that derives the
name from debug.ReadBuildInfo() if an empty string is passed.
2026-03-08 17:41:41 -07:00
af7683da9a fix(version): don't add "v" prefix to non-semver VERSION strings
When VERSION is set to a non-tag value like "main" (from goreleaser or
ldflags), the init() function unconditionally prepended "v", producing
"vmain". Now only add the "v" prefix when doing so produces a valid
semver string, leaving branch names and other non-semver values as-is.
2026-03-08 14:02:45 -07:00
3c801842e4 build(ci): support both Woodpecker and Drone in run-goreleaser
Use CI_COMMIT_TAG with DRONE_TAG fallback so the script
works in both CI systems during migration.
2026-03-07 16:21:20 -08:00
5 changed files with 55 additions and 19 deletions

View File

@@ -34,6 +34,8 @@ import (
"fmt"
"net"
"net/http"
"runtime/debug"
"strings"
"time"
"github.com/labstack/echo-contrib/echoprometheus"
@@ -70,6 +72,7 @@ import (
// - WithGzipConfig(): Custom gzip compression settings
func New(name string, options ...func(*Ekko)) (*Ekko, error) {
ek := &Ekko{
name: name,
writeTimeout: 60 * time.Second,
readHeaderTimeout: 30 * time.Second,
}
@@ -77,6 +80,20 @@ func New(name string, options ...func(*Ekko)) (*Ekko, error) {
for _, o := range options {
o(ek)
}
if ek.name == "" {
if bi, ok := debug.ReadBuildInfo(); ok && bi.Main.Path != "" {
if idx := strings.LastIndex(bi.Main.Path, "/"); idx >= 0 {
ek.name = bi.Main.Path[idx+1:]
} else {
ek.name = bi.Main.Path
}
}
if ek.name == "" {
ek.name = "ekko-app"
}
}
return ek, nil
}
@@ -146,6 +163,7 @@ func (ek *Ekko) setup(ctx context.Context) (*echo.Echo, error) {
echo.TrustLinkLocal(false),
echo.TrustPrivateNet(true),
}
trustOptions = append(trustOptions, ek.extraTrustOptions...)
e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...)
if ek.otelmiddleware == nil {

View File

@@ -13,13 +13,14 @@ import (
// It encapsulates server configuration, middleware options, and lifecycle management
// for NTP Pool web services. Use New() with functional options to configure.
type Ekko struct {
name string
prom prometheus.Registerer
port int
routeFn func(e *echo.Echo) error
logFilters []slogecho.Filter
otelmiddleware echo.MiddlewareFunc
gzipConfig *middleware.GzipConfig
name string
prom prometheus.Registerer
port int
routeFn func(e *echo.Echo) error
logFilters []slogecho.Filter
otelmiddleware echo.MiddlewareFunc
gzipConfig *middleware.GzipConfig
extraTrustOptions []echo.TrustOption
writeTimeout time.Duration
readHeaderTimeout time.Duration
@@ -92,6 +93,16 @@ func WithReadHeaderTimeout(t time.Duration) func(*Ekko) {
}
}
// WithTrustOptions appends additional trust options to the default IP extraction
// configuration. These options are applied after the built-in trust settings
// (loopback trusted, link-local untrusted, private networks trusted) when
// extracting client IPs from the X-Forwarded-For header.
func WithTrustOptions(opts ...echo.TrustOption) func(*Ekko) {
return func(ek *Ekko) {
ek.extraTrustOptions = append(ek.extraTrustOptions, opts...)
}
}
// WithGzipConfig provides custom gzip compression configuration.
// By default, gzip compression is enabled with standard settings.
// Use this option to customize compression level, skip patterns, or disable compression.

View File

@@ -2,7 +2,7 @@
set -euo pipefail
go install github.com/goreleaser/goreleaser/v2@v2.12.3
go install github.com/goreleaser/goreleaser/v2@v2.14.1
if [ ! -z "${harbor_username:-}" ]; then
DOCKER_FILE=~/.docker/config.json
@@ -13,11 +13,11 @@ if [ ! -z "${harbor_username:-}" ]; then
fi
fi
DRONE_TAG=${DRONE_TAG-""}
CI_TAG=${CI_COMMIT_TAG:-${DRONE_TAG:-""}}
is_snapshot=""
if [ -z "$DRONE_TAG" ]; then
if [ -z "$CI_TAG" ]; then
is_snapshot="--snapshot"
fi

View File

@@ -90,10 +90,13 @@ func init() {
VERSION = "dev-snapshot"
} else {
if !strings.HasPrefix(VERSION, "v") {
VERSION = "v" + VERSION
vVersion := "v" + VERSION
if semver.IsValid(vVersion) {
VERSION = vVersion
}
}
if !semver.IsValid(VERSION) {
slog.Default().Warn("invalid version number", "version", VERSION)
if !semver.IsValid(VERSION) && VERSION != "dev-snapshot" {
slog.Default().Info("non-semver version", "version", VERSION)
}
}
if bi, ok := debug.ReadBuildInfo(); ok {

View File

@@ -7,6 +7,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"golang.org/x/mod/semver"
)
func TestCheckVersion(t *testing.T) {
@@ -48,9 +49,12 @@ func TestVersionInfo(t *testing.T) {
t.Error("VersionInfo().Version should not be empty")
}
// Version should start with "v" or be "dev-snapshot"
// Version should start with "v", be "dev-snapshot", or be a non-semver string (e.g. branch name)
if !strings.HasPrefix(info.Version, "v") && info.Version != "dev-snapshot" {
t.Errorf("Version should start with 'v' or be 'dev-snapshot', got: %s", info.Version)
// Non-semver versions like branch names ("main") are acceptable
if semver.IsValid("v" + info.Version) {
t.Errorf("Semver-like version should start with 'v', got: %s", info.Version)
}
}
// GitRevShort should be <= 7 characters if set
@@ -398,10 +402,10 @@ func TestParseBuildTimeConsistency(t *testing.T) {
func BenchmarkParseBuildTime(b *testing.B) {
inputs := []string{
"1672531200", // Unix epoch
"2023-01-01T00:00:00Z", // RFC3339
"invalid-timestamp", // Invalid
"", // Empty
"1672531200", // Unix epoch
"2023-01-01T00:00:00Z", // RFC3339
"invalid-timestamp", // Invalid
"", // Empty
}
for _, input := range inputs {