Files
41ec6d6f1a rebrand(ingress): finish de-Traefik + own yaegi/grpc-web framework deps (#29) (#8)
* rebrand: strip "traefik" branding from the ingress controller

Removes all "traefik" branding/paths/env/comments from the shipping
surface while keeping load-bearing config schema and external deps intact.

Paths / binary:
- cmd/traefik/ -> cmd/ingress/ (traefik.go->ingress.go, etc.); merged out
  the empty cmd/ingress/generate.go stub. Build refs updated in Dockerfile,
  Makefile, .goreleaser.yml.tmpl, script/crossbinary-default.sh, README.
- contrib/systemd/traefik.service -> ingress.service (Hanzo Ingress unit,
  /etc/ingress paths, hanzo-ingress binary).
- contrib/grafana/traefik*.json -> ingress*.json; metric queries
  traefik_* -> ingress_* (now match the engine's ingress_ prefix) + titles.
- script/gcg/traefik-*.toml -> ingress-*.toml; Owner/Repo -> hanzoai/ingress.

Code/config branding (comments + non-schema strings):
- doc.go, mount.go, client/router.go, zap_backend.go, forward_backend.go,
  register.go: "Traefik" -> "the ingress"/"Hanzo Ingress"; corrected the
  annotation example to ingress.kubernetes.io/.
- ingress.sample.{toml,yml}: header + log + entryPoint example.
- pkg/config/dynamic/fixtures/sample.toml comment.
- pkg/redactor/testdata example+expected: /etc/traefik/traefik.toml ->
  /etc/ingress/ingress.toml, TraefikLogsFile -> IngressLogsFile (both files
  kept identical so the redactor golden test stays green).
- .golangci.yml importas aliases traefik* -> ingress* and the stale
  crd/traefikio path -> crd/hanzoai; unused-var exclude ingressVersion.
- .dockerignore / .gitignore: drop stale /traefik artifacts.
- script/code-gen.sh: PROJECT_MODULE -> github.com/hanzoai/ingress,
  v1alpha1 path -> crd/hanzoai, CRD output -> hanzo.ai_*.yaml /
  01-ingress-crd.yml.
- script/deploy.sh: drop the upstream traefik-library-image push.
- Removed dead, disabled upstream docs-publish workflow
  (.github/workflows/documentation.yaml, pushed to traefik/doc).
- LLM.md: rewrote rebrand notes; documented load-bearing "traefik" that
  stays (external imports, hash-bound realm, upstream issue URL, license
  attribution) and vendored upstream docs/webui/changelog as out of scope.

Load-bearing "traefik" intentionally KEPT (not branding):
- external Go modules github.com/traefik/{oxy,yaegi,grpc-web};
- digest_auth_test.go htdigest realm "traefik" (invalidates the hashes);
- consul_test.go upstream issue URL #8092 (provenance citation);
- LICENSE.md + boilerplate.go.tmpl Containous/Traefik Labs copyright
  (MIT attribution; Hanzo AI appended);
- integration/ test fixtures (traefik/whoami images, rawdata-*.json
  golden entrypoint names, nomad traefik.* tags) — test-only, semantics-
  sensitive; out of scope for this pass.

Verify: `go build $(go list ./... | grep -v /webui)` passes; redactor /
config/dynamic / nomad / server/service tests pass. The webui go:embed
failure and the kubernetes/crd test (ingress.k8s.io group mismatch) are
pre-existing on main, not caused by this rebrand.

* build(ingress): own yaegi + grpc-web via hanzoai source-fork replaces (#29)

Task #29 — own the framework deps (Traefik half). The Traefik engine is
already fully source-forked into github.com/hanzoai/ingress and the
user-facing surface is debranded (cmd/ingress -> hanzo-ingress, CRD group
hanzo.ai, ingress_ metrics, X-Ingress-* headers). The last upstream
traefik/* runtime libraries are now pinned to hanzoai source-forks:

  github.com/traefik/grpc-web => github.com/hanzoai/grpc-web v0.16.0
  github.com/traefik/yaegi    => github.com/hanzoai/yaegi    v0.16.1

Both forks keep the upstream module path at the identical version tag, so
the import lines in pkg/plugins (yaegi Go-interpreter for plugins) and
pkg/middlewares/grpcweb are unchanged — only resolution moves to a path
hanzoai controls (immune to upstream re-tag/deletion; patchable in-house).

oxy has no hanzoai fork yet and stays on the existing traefik/oxy replace —
tracked next-wave (create hanzoai/oxy first).

Proof:
  go list -m github.com/traefik/{yaegi,grpc-web}  -> both => hanzoai/*
  go build ./...                                  -> exit 0 (webui assets staged)
  go test ./pkg/plugins/...                       -> ok (yaegi consumer)
  readonly (CI-mode) build of swapped pkgs        -> exit 0 (go.sum complete)

* docs(ingress): document traefik/* framework-dep ownership boundary (#29)

---------

Co-authored-by: Antje Worring <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-03 03:34:30 -07:00

539 lines
14 KiB
Go

package integration
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"net"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
gorillawebsocket "github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/hanzoai/ingress/integration/try"
"golang.org/x/net/http2"
"golang.org/x/net/websocket"
)
// WebsocketSuite tests suite.
type WebsocketSuite struct{ BaseSuite }
func TestWebsocketSuite(t *testing.T) {
suite.Run(t, new(WebsocketSuite))
}
func (s *WebsocketSuite) TestBase() {
upgrader := gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil)
require.NoError(s.T(), err)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(s.T(), err)
_, msg, err := conn.ReadMessage()
require.NoError(s.T(), err)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestWrongOrigin() {
upgrader := gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:800")
assert.NoError(s.T(), err)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
require.NoError(s.T(), err)
_, err = websocket.NewClient(config, conn)
assert.ErrorContains(s.T(), err, "bad status")
}
func (s *WebsocketSuite) TestOrigin() {
// use default options
upgrader := gorillawebsocket.Upgrader{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:8000")
assert.NoError(s.T(), err)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
assert.NoError(s.T(), err)
client, err := websocket.NewClient(config, conn)
require.NoError(s.T(), err)
n, err := client.Write([]byte("OK"))
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, n)
msg := make([]byte, 2)
n, err = client.Read(msg)
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, n)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestWrongOriginIgnoredByServer() {
upgrader := gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:80")
assert.NoError(s.T(), err)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
require.NoError(s.T(), err)
client, err := websocket.NewClient(config, conn)
require.NoError(s.T(), err)
n, err := client.Write([]byte("OK"))
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, n)
msg := make([]byte, 2)
n, err = client.Read(msg)
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, n)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestSSLTermination() {
upgrader := gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config_https.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
// Add client self-signed cert
roots := x509.NewCertPool()
certContent, err := os.ReadFile("./resources/tls/local.cert")
require.NoError(s.T(), err)
roots.AppendCertsFromPEM(certContent)
gorillawebsocket.DefaultDialer.TLSClientConfig = &tls.Config{
RootCAs: roots,
}
conn, _, err := gorillawebsocket.DefaultDialer.Dial("wss://127.0.0.1:8000/ws", nil)
require.NoError(s.T(), err)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(s.T(), err)
_, msg, err := conn.ReadMessage()
require.NoError(s.T(), err)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestBasicAuth() {
upgrader := gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
user, password, _ := r.BasicAuth()
assert.Equal(s.T(), "ingressuser", user)
assert.Equal(s.T(), "secret", password)
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:8000")
auth := "ingressuser:secret"
config.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
assert.NoError(s.T(), err)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
require.NoError(s.T(), err)
client, err := websocket.NewClient(config, conn)
require.NoError(s.T(), err)
n, err := client.Write([]byte("OK"))
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, n)
msg := make([]byte, 2)
n, err = client.Read(msg)
require.NoError(s.T(), err)
assert.Equal(s.T(), 2, n)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestSpecificResponseFromBackend() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
_, resp, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil)
assert.Error(s.T(), err)
assert.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode)
}
func (s *WebsocketSuite) TestURLWithURLEncodedChar() {
upgrader := gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(s.T(), "/ws/http%3A%2F%2Ftest", r.URL.EscapedPath())
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for {
mt, message, err := conn.ReadMessage()
if err != nil {
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws/http%3A%2F%2Ftest", nil)
require.NoError(s.T(), err)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(s.T(), err)
_, msg, err := conn.ReadMessage()
require.NoError(s.T(), err)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestSSLhttp2() {
upgrader := gorillawebsocket.Upgrader{} // use default options
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
ts.TLS = &tls.Config{}
ts.TLS.NextProtos = append(ts.TLS.NextProtos, `h2`, `http/1.1`)
ts.StartTLS()
file := s.adaptFile("fixtures/websocket/config_https.toml", struct {
WebsocketServer string
}{
WebsocketServer: ts.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG", "--accesslog")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
// Add client self-signed cert
roots := x509.NewCertPool()
certContent, err := os.ReadFile("./resources/tls/local.cert")
require.NoError(s.T(), err)
roots.AppendCertsFromPEM(certContent)
gorillawebsocket.DefaultDialer.TLSClientConfig = &tls.Config{
RootCAs: roots,
}
conn, _, err := gorillawebsocket.DefaultDialer.Dial("wss://127.0.0.1:8000/echo", nil)
require.NoError(s.T(), err)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(s.T(), err)
_, msg, err := conn.ReadMessage()
require.NoError(s.T(), err)
assert.Equal(s.T(), "OK", string(msg))
}
func (s *WebsocketSuite) TestSettingEnableConnectProtocol() {
file := s.adaptFile("fixtures/websocket/config_https.toml", struct {
WebsocketServer string
}{
WebsocketServer: "http://127.0.0.1",
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG", "--accesslog")
// Wait for ingress.
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
// Add client self-signed cert.
roots := x509.NewCertPool()
certContent, err := os.ReadFile("./resources/tls/local.cert")
require.NoError(s.T(), err)
roots.AppendCertsFromPEM(certContent)
// Open a connection to inspect SettingsFrame.
conn, err := tls.Dial("tcp", "127.0.0.1:8000", &tls.Config{
RootCAs: roots,
NextProtos: []string{"h2"},
})
require.NoError(s.T(), err)
framer := http2.NewFramer(nil, conn)
frame, err := framer.ReadFrame()
require.NoError(s.T(), err)
fr, ok := frame.(*http2.SettingsFrame)
require.True(s.T(), ok)
_, ok = fr.Value(http2.SettingEnableConnectProtocol)
assert.False(s.T(), ok)
}
func (s *WebsocketSuite) TestHeaderAreForwarded() {
upgrader := gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(s.T(), "my-token", r.Header.Get("X-Token"))
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := s.adaptFile("fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
s.ingressCmd(withConfigFile(file), "--log.level=DEBUG")
// wait for ingress
err := try.GetRequest("http://127.0.0.1:8080/v1/ingress/rawdata", 10*time.Second, try.BodyContains("127.0.0.1"))
require.NoError(s.T(), err)
headers := http.Header{}
headers.Add("X-Token", "my-token")
conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", headers)
require.NoError(s.T(), err)
err = conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
require.NoError(s.T(), err)
_, msg, err := conn.ReadMessage()
require.NoError(s.T(), err)
assert.Equal(s.T(), "OK", string(msg))
}