mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
ci: canonical docker-build (#2)
* feat: add /v1/o11y/* rewrite middleware for standard API path convention Add URL rewrite middleware that intercepts /v1/o11y/* requests and rewrites them to /api/* internally, allowing external clients to use the /<version>/<service>/<path> convention while keeping all existing internal route registrations unchanged. * chore: sync latest * ci: migrate to canonical hanzoai/.github/docker-build.yml reusable
This commit is contained in:
@@ -1,84 +1,25 @@
|
||||
name: Build & Push to GHCR
|
||||
|
||||
name: Docker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, test, dev]
|
||||
tags: [v*]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: hanzoai/o11y
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
jobs:
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Build frontend
|
||||
working-directory: frontend
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run build
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/build/
|
||||
|
||||
build-and-push:
|
||||
needs: build-frontend
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/build/
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26'
|
||||
- name: Build Go binary
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always)
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
LDFLAGS="-X github.com/hanzoai/o11y/pkg/version.version=$VERSION"
|
||||
LDFLAGS="$LDFLAGS -X github.com/hanzoai/o11y/pkg/version.hash=$HASH"
|
||||
LDFLAGS="$LDFLAGS -X github.com/hanzoai/o11y/pkg/version.time=$TIMESTAMP"
|
||||
LDFLAGS="$LDFLAGS -X github.com/hanzoai/o11y/pkg/version.branch=$BRANCH"
|
||||
mkdir -p target/linux-amd64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o target/linux-amd64/o11y ./cmd/server/
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=sha,prefix=
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: cmd/server/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
OS=linux
|
||||
TARGETARCH=amd64
|
||||
docker:
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ghcr.io/hanzoai/o11y
|
||||
pre-build-command: |
|
||||
cd frontend && corepack enable && pnpm install --frozen-lockfile && pnpm run build
|
||||
cd ..
|
||||
VERSION=$(git describe --tags --always)
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
LDFLAGS="-X github.com/hanzoai/o11y/pkg/version.version=$VERSION -X github.com/hanzoai/o11y/pkg/version.hash=$HASH -X github.com/hanzoai/o11y/pkg/version.time=$TIMESTAMP -X github.com/hanzoai/o11y/pkg/version.branch=$BRANCH"
|
||||
mkdir -p target/linux-amd64
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$LDFLAGS" -o target/linux-amd64/o11y ./cmd/server/
|
||||
secrets: inherit
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Workflow Sanity
|
||||
on:
|
||||
pull_request:
|
||||
paths: ['.github/workflows/**']
|
||||
jobs:
|
||||
sanity:
|
||||
uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
|
||||
@@ -0,0 +1,7 @@
|
||||
# o11y — AI Assistant Context
|
||||
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<a href="https://o11y.hanzo.ai" target="_blank">
|
||||
<img alt="Hanzo O11y" src="https://github.com/user-attachments/assets/ef9a33f7-12d7-4c94-8908-0a02b22f0c18" width="100" height="100">
|
||||
</a>
|
||||
<br>Hanzo O11y
|
||||
@@ -204,6 +204,8 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rewrite rewrites request URL paths that match a given prefix to a target
|
||||
// prefix. This allows exposing routes under a new prefix (e.g. /v1/o11y/*)
|
||||
// while keeping all internal route registrations unchanged (/api/*).
|
||||
type Rewrite struct {
|
||||
from string
|
||||
to string
|
||||
}
|
||||
|
||||
// NewRewrite creates a middleware that rewrites paths starting with `from` to
|
||||
// start with `to` instead. Both prefixes should include trailing context if
|
||||
// needed (e.g. "/v1/o11y" rewrites to "/api").
|
||||
func NewRewrite(from, to string) *Rewrite {
|
||||
return &Rewrite{from: from, to: to}
|
||||
}
|
||||
|
||||
func (rw *Rewrite) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, rw.from) {
|
||||
r.URL.Path = rw.to + strings.TrimPrefix(r.URL.Path, rw.from)
|
||||
if r.URL.RawPath != "" {
|
||||
r.URL.RawPath = rw.to + strings.TrimPrefix(r.URL.RawPath, rw.from)
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
from string
|
||||
to string
|
||||
path string
|
||||
wantPath string
|
||||
}{
|
||||
{
|
||||
name: "rewrites matching prefix",
|
||||
from: "/v1/o11y",
|
||||
to: "/api",
|
||||
path: "/v1/o11y/v1/dashboards",
|
||||
wantPath: "/api/v1/dashboards",
|
||||
},
|
||||
{
|
||||
name: "passes through non-matching path",
|
||||
from: "/v1/o11y",
|
||||
to: "/api",
|
||||
path: "/api/v1/dashboards",
|
||||
wantPath: "/api/v1/dashboards",
|
||||
},
|
||||
{
|
||||
name: "rewrites exact prefix",
|
||||
from: "/v1/o11y",
|
||||
to: "/api",
|
||||
path: "/v1/o11y",
|
||||
wantPath: "/api",
|
||||
},
|
||||
{
|
||||
name: "no rewrite for partial match",
|
||||
from: "/v1/o11y",
|
||||
to: "/api",
|
||||
path: "/v1/o11yxyz",
|
||||
wantPath: "/api" + "xyz",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var gotPath string
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
})
|
||||
rw := NewRewrite(tt.from, tt.to)
|
||||
handler := rw.Wrap(inner)
|
||||
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
|
||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
if gotPath != tt.wantPath {
|
||||
t.Errorf("got path %q, want %q", gotPath, tt.wantPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewrite_RawPath(t *testing.T) {
|
||||
var gotPath, gotRawPath string
|
||||
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
gotRawPath = r.URL.RawPath
|
||||
})
|
||||
rw := NewRewrite("/v1/o11y", "/api")
|
||||
handler := rw.Wrap(inner)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/o11y/v1/query%20range", nil)
|
||||
// httptest.NewRequest sets both Path and RawPath for encoded URLs
|
||||
req.URL.Path = "/v1/o11y/v1/query range"
|
||||
req.URL.RawPath = "/v1/o11y/v1/query%20range"
|
||||
|
||||
handler.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
if gotPath != "/api/v1/query range" {
|
||||
t.Errorf("got path %q, want %q", gotPath, "/api/v1/query range")
|
||||
}
|
||||
if gotRawPath != "/api/v1/query%20range" {
|
||||
t.Errorf("got raw path %q, want %q", gotRawPath, "/api/v1/query%20range")
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,10 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
||||
func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, error) {
|
||||
r := NewRouter()
|
||||
|
||||
// Rewrite /v1/o11y/* to /api/* so external clients use the
|
||||
// /<version>/<service>/<path> convention while internal routes stay unchanged.
|
||||
r.Use(middleware.NewRewrite("/v1/o11y", "/api").Wrap)
|
||||
|
||||
r.Use(otelmux.Middleware(
|
||||
"apiserver",
|
||||
otelmux.WithMeterProvider(s.o11y.Instrumentation.MeterProvider()),
|
||||
|
||||
Reference in New Issue
Block a user