diff --git a/.gitignore b/.gitignore index 22c9efc..e7e1bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,12 +17,12 @@ GEMINI.md build/ dist/ /altinity-mcp +/jwe-token-generator go-carpet* .mcp.json .gocache/ .gomodcache/ .tmp/ -resutls.xml coverage.out results.xml push-action diff --git a/cmd/altinity-mcp/main_test.go b/cmd/altinity-mcp/main_test.go index 59b852a..af20787 100644 --- a/cmd/altinity-mcp/main_test.go +++ b/cmd/altinity-mcp/main_test.go @@ -22,14 +22,12 @@ import ( "testing" "time" + "github.com/altinity/altinity-mcp/internal/testutil/embeddedch" "github.com/altinity/altinity-mcp/pkg/clickhouse" "github.com/altinity/altinity-mcp/pkg/config" "github.com/altinity/altinity-mcp/pkg/jwe_auth" altinitymcp "github.com/altinity/altinity-mcp/pkg/server" - "github.com/moby/moby/api/types/container" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" "github.com/urfave/cli/v3" ) @@ -427,77 +425,23 @@ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7d7Qj8fKjKjKjKjKjKjK }) } -// setupClickHouseContainerMain is a local helper for this package's tests +// setupClickHouseContainerMain boots a ClickHouse server as a host +// subprocess via embedded-clickhouse and seeds the default.test table this +// package's tests rely on. func setupClickHouseContainerMain(t *testing.T) *config.ClickHouseConfig { t.Helper() - ctx := context.Background() - - totalStart := time.Now() - - req := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp", "9000/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - "CLICKHOUSE_DB": "default", - "CLICKHOUSE_USER": "default", - "CLICKHOUSE_PASSWORD": "", - "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT": "1", - }, - WaitingFor: wait.ForHTTP("/").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(2 * time.Second), - } - containerStart := time.Now() - chContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: req, Started: true}) - containerElapsed := time.Since(containerStart) - require.NoError(t, err) - - t.Cleanup(func() { - cleanupStart := time.Now() - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = chContainer.Terminate(cleanupCtx) - t.Logf("[container/%s] cleanup took %s", req.Image, time.Since(cleanupStart)) - }) - - host, err := chContainer.Host(ctx) - require.NoError(t, err) - port, err := chContainer.MappedPort(ctx, "9000") - require.NoError(t, err) + cfg := embeddedch.Setup(t, embeddedch.WithTCPProtocol()) + cfg.Limit = 1000 - cfg := &config.ClickHouseConfig{ - Host: host, - Port: int(port.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.TCPProtocol, - ReadOnly: false, - MaxExecutionTime: 60, - Limit: 1000, - } - - // create base table - setupStart := time.Now() + ctx := context.Background() client, err := clickhouse.NewClient(ctx, *cfg) require.NoError(t, err) defer func() { _ = client.Close() }() _, _ = client.ExecuteQuery(ctx, "CREATE TABLE IF NOT EXISTS default.test (id UInt64, value String) ENGINE = Memory") _, _ = client.ExecuteQuery(ctx, "INSERT INTO default.test VALUES (1, 'one') ON CLUSTER default") - setupElapsed := time.Since(setupStart) - - t.Logf("[container/%s] start=%s setup=%s total=%s", req.Image, containerElapsed, setupElapsed, time.Since(totalStart)) return cfg } -// startContainerWithTiming wraps testcontainers.GenericContainer with timing logs. -func startContainerWithTiming(t *testing.T, ctx context.Context, req testcontainers.GenericContainerRequest) (testcontainers.Container, error) { - t.Helper() - start := time.Now() - container, err := testcontainers.GenericContainer(ctx, req) - t.Logf("[container/%s] start took %s", req.Image, time.Since(start)) - return container, err -} - // Health handler tests func TestHealthHandler_Additions(t *testing.T) { t.Parallel() @@ -728,48 +672,10 @@ func TestHealthHandler(t *testing.T) { t.Run("successful_clickhouse_connection_with_testcontainer", func(t *testing.T) { t.Parallel() - ctx := context.Background() - - // Start ClickHouse container - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - // Get the mapped port - mappedPort, err := clickhouseContainer.MappedPort(ctx, "8123") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) - + cfg := embeddedch.Setup(t) app := &application{ config: config.Config{ - ClickHouse: config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.HTTPProtocol, - }, + ClickHouse: *cfg, Server: config.ServerConfig{ JWE: config.JWEConfig{Enabled: false}, }, @@ -962,289 +868,75 @@ func TestTestConnection(t *testing.T) { t.Run("successful_connection_with_testcontainer", func(t *testing.T) { t.Parallel() ctx := context.Background() - - // Start ClickHouse container - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - // Get the mapped port - mappedPort, err := clickhouseContainer.MappedPort(ctx, "8123") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) - - cfg := config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.HTTPProtocol, - } - - // Test connection - err = testConnection(ctx, cfg) - require.NoError(t, err) + cfg := embeddedch.Setup(t) + require.NoError(t, testConnection(ctx, *cfg)) }) t.Run("connection_with_tcp_protocol", func(t *testing.T) { t.Parallel() ctx := context.Background() - - // Start ClickHouse container - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"9000/tcp", "8123/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - // Get the mapped port for TCP - mappedPort, err := clickhouseContainer.MappedPort(ctx, "9000") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) - - cfg := config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.TCPProtocol, - } - - // Test connection - err = testConnection(ctx, cfg) - require.NoError(t, err) + cfg := embeddedch.Setup(t, embeddedch.WithTCPProtocol()) + require.NoError(t, testConnection(ctx, *cfg)) }) t.Run("connection_with_tls", func(t *testing.T) { t.Parallel() ctx := context.Background() require.NoError(t, setupLogging("debug")) - // Generate self-signed certificate + cert, key, err := generateSelfSignedCert() require.NoError(t, err) - // Create HTTPS port config with OpenSSL server section - httpsConfig := ` + // Materialize cert + key on disk so the embedded CH server can read + // them; then point the server config at those host paths via a + // config.d drop-in. + certDir := t.TempDir() + certPath := filepath.Join(certDir, "server.crt") + keyPath := filepath.Join(certDir, "server.key") + require.NoError(t, os.WriteFile(certPath, cert, 0644)) + require.NoError(t, os.WriteFile(keyPath, key, 0600)) + + httpsConfig := fmt.Sprintf(` 8443 - /etc/clickhouse-server/server.crt - /etc/clickhouse-server/server.key + %s + %s none -` +`, certPath, keyPath) // https://github.com/ClickHouse/clickhouse-go/issues/1630 nonEmptyDefaultUserPassword := "non_empty" - // Materialize config to a TempDir and bind-mount read-only. - // testcontainers' File-stream-via-archive path uses PUT /containers/{id}/archive - // which is blocked in some sandboxes (e.g. our agent isolator). - tmpDir := t.TempDir() - writeTmp := func(name, content string) string { - p := tmpDir + "/" + name - require.NoError(t, os.WriteFile(p, []byte(content), 0644)) - return p - } - certFile := writeTmp("server.crt", string(cert)) - keyFile := writeTmp("server.key", string(key)) - httpsConfigFile := writeTmp("https_port.xml", httpsConfig) - // https://github.com/ClickHouse/clickhouse-go/issues/1630 - nonEmptyPasswordFile := writeTmp("non_empty_password.xml", nonEmptyDefaultUserPassword) - - // Start ClickHouse container with TLS enabled - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp", "8443/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - HostConfigModifier: func(hc *container.HostConfig) { - hc.Binds = append(hc.Binds, - certFile+":/etc/clickhouse-server/server.crt:ro", - keyFile+":/etc/clickhouse-server/server.key:ro", - httpsConfigFile+":/etc/clickhouse-server/config.d/https_port.xml:ro", - nonEmptyPasswordFile+":/etc/clickhouse-server/users.d/non_empty_password.xml:ro", - ) - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - mappedPort, err := clickhouseContainer.MappedPort(ctx, "8443") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) - - cfg := config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - // https://github.com/ClickHouse/clickhouse-go/issues/1630 - Password: "non_empty", - Protocol: config.HTTPProtocol, - TLS: config.TLSConfig{ - Enabled: true, - InsecureSkipVerify: true, - }, - } + chCfg := embeddedch.Setup(t, + embeddedch.WithConfigDropIn(httpsConfig), + embeddedch.WithConfigDropIn(nonEmptyDefaultUserPassword), + ) + chCfg.Port = 8443 + chCfg.Username = "default" + chCfg.Password = "non_empty" + chCfg.Protocol = config.HTTPProtocol + chCfg.TLS = config.TLSConfig{Enabled: true, InsecureSkipVerify: true} - // Test connection - err = testConnection(ctx, cfg) - require.NoError(t, err) + require.NoError(t, testConnection(ctx, *chCfg)) }) t.Run("connection_with_readonly_mode", func(t *testing.T) { t.Parallel() ctx := context.Background() - - // Start ClickHouse container - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - // Get the mapped port - mappedPort, err := clickhouseContainer.MappedPort(ctx, "8123") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) - - cfg := config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.HTTPProtocol, - ReadOnly: true, - } - - // Test connection - err = testConnection(ctx, cfg) - require.NoError(t, err) + cfg := embeddedch.Setup(t) + cfg.ReadOnly = true + require.NoError(t, testConnection(ctx, *cfg)) }) t.Run("connection_with_max_execution_time", func(t *testing.T) { t.Parallel() ctx := context.Background() - - // Start ClickHouse container - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - // Get the mapped port - mappedPort, err := clickhouseContainer.MappedPort(ctx, "8123") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) - - cfg := config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.HTTPProtocol, - MaxExecutionTime: 300, - } - - // Test connection - err = testConnection(ctx, cfg) - require.NoError(t, err) + cfg := embeddedch.Setup(t) + cfg.MaxExecutionTime = 300 + require.NoError(t, testConnection(ctx, *cfg)) }) } @@ -2760,46 +2452,10 @@ logging: func TestNewApplicationWithTestContainer(t *testing.T) { t.Parallel() ctx := context.Background() - - // Start ClickHouse container - containerReq := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - }, - WaitingFor: wait.ForHTTP("/ping").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(1 * time.Second), - } - - clickhouseContainer, err := startContainerWithTiming(t, ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: containerReq, - Started: true, - }) - if err != nil { - t.Fatal("Failed to start ClickHouse container, skipping test:", err) - } - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = clickhouseContainer.Terminate(cleanupCtx) - }) - - // Get the mapped port - mappedPort, err := clickhouseContainer.MappedPort(ctx, "8123") - require.NoError(t, err) - - host, err := clickhouseContainer.Host(ctx) - require.NoError(t, err) + chCfg := embeddedch.Setup(t) cfg := config.Config{ - ClickHouse: config.ClickHouseConfig{ - Host: host, - Port: int(mappedPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.HTTPProtocol, - }, + ClickHouse: *chCfg, Server: config.ServerConfig{ JWE: config.JWEConfig{ Enabled: false, diff --git a/cmd/altinity-mcp/oauth_server.go b/cmd/altinity-mcp/oauth_server.go index 3800a5b..41fe30e 100644 --- a/cmd/altinity-mcp/oauth_server.go +++ b/cmd/altinity-mcp/oauth_server.go @@ -601,7 +601,11 @@ func (a *application) fetchUserInfo(accessToken string) (*altinitymcp.OAuthClaim if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Error().Err(closeErr).Msgf("can't close %s response body", userInfoURL) + } + }() body, err := io.ReadAll(io.LimitReader(resp.Body, maxOAuthResponseBytes)) if err != nil { return nil, err @@ -903,7 +907,11 @@ func (a *application) handleOAuthCallback(w http.ResponseWriter, r *http.Request http.Error(w, "Failed to exchange upstream auth code", http.StatusBadGateway) return } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Error().Err(closeErr).Msgf("can't close %s response body", tokenURL) + } + }() body, err := io.ReadAll(io.LimitReader(resp.Body, maxOAuthResponseBytes)) if err != nil { http.Error(w, "Failed to read upstream token response", http.StatusBadGateway) @@ -1394,7 +1402,11 @@ func (a *application) handleOAuthTokenRefreshForward(w http.ResponseWriter, r *h writeOAuthTokenError(w, http.StatusBadGateway, "server_error", "upstream refresh failed") return } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + log.Error().Err(closeErr).Msgf("can't close %s response body", tokenURL) + } + }() body, err := io.ReadAll(io.LimitReader(resp.Body, maxOAuthResponseBytes)) if err != nil { writeOAuthTokenError(w, http.StatusBadGateway, "server_error", "failed to read upstream refresh response") diff --git a/docs/development_and_testing.md b/docs/development_and_testing.md index 20aee0e..44c8d7f 100644 --- a/docs/development_and_testing.md +++ b/docs/development_and_testing.md @@ -79,6 +79,109 @@ These tests run automatically as part of `go test ./...` (skipped with `-short`) For configuration background and provider-specific setup, see [oauth_authorization.md](./oauth_authorization.md). +## Embedded ClickHouse for Tests + +Tests under `internal/testutil/embeddedch` boot ClickHouse as a host subprocess instead of a container. Two flavors are supported: + +- **Stock** — upstream ClickHouse, downloaded automatically by `franchb/embedded-clickhouse`. +- **Antalya** — the Altinity Antalya binary, expected at `~/.cache/embedded-clickhouse/clickhouse-`. + +On Linux the Antalya binary is extracted once from the Antalya Docker image (`altinity/clickhouse-server:26.1.6.20001.altinityantalya`) on first use. +On macOS and other non-Linux hosts you must build it from source ahead of time — the Antalya Docker image only ships a Linux ELF, so it cannot run as a host subprocess on macOS. + +### Build the Antalya `clickhouse` binary on macOS + +The Antalya tree shares the upstream ClickHouse build system, so the standard macOS build flow applies. Steps below are tailored for `~/.cache/embedded-clickhouse/` placement expected by the tests in this repo. + +1. Install build prerequisites via Homebrew: + + ```bash + brew update + brew install ccache cmake ninja libtool gettext llvm lld binutils grep findutils nasm bash rust rustup + ``` + +2. Extract the Antalya image ref/tag straight from the Go source so the build stays in lockstep with what the tests expect. Run this from the root of the `altinity-mcp` checkout: + + ```bash + # AntalyaImageRef looks like "altinity/clickhouse-server:26.1.6.20001.altinityantalya" + ANTALYA_IMAGE_REF=$(grep -E '^\s*const AntalyaImageRef' internal/testutil/embeddedch/embeddedch.go | sed -E 's/.*"([^"]+)".*/\1/') + # 26.1.6.20001.altinityantalya + ANTALYA_IMAGE_TAG="v${ANTALYA_IMAGE_REF##*:}" + echo "image=$ANTALYA_IMAGE_REF tag=$ANTALYA_IMAGE_TAG" + ``` + + Keep this shell session open — `ANTALYA_IMAGE_REF` and `ANTALYA_IMAGE_TAG` are reused in steps 3 and 5. + +3. Clone the Altinity ClickHouse fork (Antalya branches/tags live here) with submodules and check out the tag matching `ANTALYA_IMAGE_TAG`. The clone path **must not contain whitespace**: + + ```bash + git clone https://github.com/Altinity/ClickHouse.git ~/src/github.com/altinity/ClickHouse + cd ~/src/github.com/altinity/ClickHouse + git fetch --tags + git checkout "$ANTALYA_IMAGE_TAG" + git submodule update --init --jobs 8 + ``` + + If you skipped `--recurse-submodules` during clone, the explicit `git submodule update --init` step is required — submodules are not checked out by default. + +4. Install the Rust nightly toolchain ClickHouse pins. The version is hardcoded in `contrib/corrosion-cmake/CMakeLists.txt` (`Rust_TOOLCHAIN`); if you skip this step `cmake` fails with `Cannot find nightly-YYYY-MM-DD Rust toolchain`. Extract it from the source so you always install exactly what the build expects: + + ```bash + cd ~/src/github.com/altinity/ClickHouse + # First time only: initialise rustup if you installed it via Homebrew. + # `brew install rustup` puts the binary on PATH but does not pick a default + # toolchain — running rustup-init -y is the supported one-shot setup. + command -v rustup-init >/dev/null && rustup-init -y --no-modify-path --default-toolchain stable + + RUST_TOOLCHAIN=$(grep -E 'set\(Rust_TOOLCHAIN' contrib/corrosion-cmake/CMakeLists.txt | sed -E 's/.*"([^"]+)".*/\1/') + echo "required toolchain: $RUST_TOOLCHAIN" + + rustup toolchain install "$RUST_TOOLCHAIN" + # Sanity check — should list the nightly alongside stable: + rustup toolchain list + ``` + + The build does not need this nightly to be the default toolchain; corrosion picks it up by name. If you ever bump the ClickHouse checkout to a newer Antalya tag and `cmake` complains about a different missing nightly, just rerun the snippet above — it always reads the value from source. + +5. Build with Homebrew's Clang/LLD (Apple's system Clang is **not** supported): + + ```bash + cd ~/src/github.com/altinity/ClickHouse + mkdir -p build + export PATH="$(brew --prefix llvm)/bin:$PATH" + cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_C_COMPILER="$(brew --prefix llvm)/bin/clang" \ + -DCMAKE_CXX_COMPILER="$(brew --prefix llvm)/bin/clang++" \ + -DCMAKE_AR="$(brew --prefix llvm)/bin/llvm-ar" \ + -DCMAKE_RANLIB="$(brew --prefix llvm)/bin/llvm-ranlib" + cmake --build build --target clickhouse + # Resulting binary: build/programs/clickhouse + ``` + + If linking fails with `ld: archive member '/' not a mach-o file in ...`, double-check that `-DCMAKE_AR=$(brew --prefix llvm)/bin/llvm-ar` is set (Apple's `ar` does not support GNU thin archives). + +6. Copy the built binary into the cache directory using the exact filename the tests look for. The filename suffix is `safeFileName(AntalyaImageRef)` — every char outside `[A-Za-z0-9._-]` becomes `_` — which we reproduce here from `$ANTALYA_IMAGE_REF`: + + ```bash + mkdir -p ~/.cache/embedded-clickhouse + ANTALYA_BIN_SUFFIX=$(printf '%s' "$ANTALYA_IMAGE_REF" | LC_ALL=C sed -E 's/[^A-Za-z0-9._-]/_/g') + DEST=~/.cache/embedded-clickhouse/clickhouse-${ANTALYA_BIN_SUFFIX} + cp ~/src/github.com/altinity/ClickHouse/build/programs/clickhouse "$DEST" + chmod +x "$DEST" + echo "installed: $DEST" + ``` + + If you're unsure what filename the loader expects, just run the Antalya tests once — the failure message prints the exact path it looked for. + +7. Re-run the embedded-CH tests: + + ```bash + go test ./pkg/server/... -run Antalya -count=1 -v + ``` + +If you want to refresh the macOS binary after `AntalyaImageRef` is bumped, re-run step 2 to refresh `ANTALYA_IMAGE_REF`/`ANTALYA_IMAGE_TAG`, then repeat steps 3 (`git pull && git checkout && git submodule update`), 4 (the pinned Rust nightly may have changed), 5, and 6. The cached binary is keyed by `AntalyaImageRef`, so bumping that constant invalidates the cache automatically. + ## Suggested Contributor Workflow For a typical code change: diff --git a/go.mod b/go.mod index ac42dcc..faa0377 100644 --- a/go.mod +++ b/go.mod @@ -5,77 +5,42 @@ go 1.26 require ( github.com/AfterShip/clickhouse-sql-parser v0.5.1 github.com/Altinity/clickhouse-go/v2 v2.45.1-0.20260424134931-fb5f38b1cac7 + github.com/franchb/embedded-clickhouse v0.4.0 github.com/go-jose/go-jose/v4 v4.1.4 - github.com/moby/moby/api v1.54.1 - github.com/modelcontextprotocol/go-sdk v1.5.0 + github.com/moby/moby/api v1.54.2 + github.com/modelcontextprotocol/go-sdk v1.6.0 github.com/rs/zerolog v1.35.1 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.42.0 github.com/urfave/cli/v3 v3.8.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - dario.cat/mergo v1.0.2 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/ClickHouse/ch-go v0.71.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.10.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.18.5 // indirect - github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect - github.com/magiconair/properties v1.8.10 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/moby/client v0.4.0 // indirect - github.com/moby/patternmatcher v0.6.1 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect - github.com/moby/sys/user v0.4.0 // indirect - github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/paulmach/orb v0.13.0 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.4 // indirect - github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.49.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sys v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index 0736b00..9290c89 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,24 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AfterShip/clickhouse-sql-parser v0.5.1 h1:Re4BZXx0v4zVKvtumqwFSVy6Xmaui6QT/OgYL6zM3aI= github.com/AfterShip/clickhouse-sql-parser v0.5.1/go.mod h1:Qi3qvPTfZb/aFwI5V4WFOahgjsLJa4MzVijIAfwOhDw= github.com/Altinity/clickhouse-go/v2 v2.45.1-0.20260424134931-fb5f38b1cac7 h1:/vBQtlgdQ2URiFPjIxHwF0YUnAEkwk87B/edeu/G36Q= github.com/Altinity/clickhouse-go/v2 v2.45.1-0.20260424134931-fb5f38b1cac7/go.mod h1:SqvoxopcNPeJWv3lSv3hpBYlwanQ24hWkorGjEWKmdg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ= +github.com/ClickHouse/clickhouse-go/v2 v2.44.0/go.mod h1:giJfUVlMkcfUEPVfRpt51zZaGEx9i17gCos8gBl392c= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= 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/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= -github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= -github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/franchb/embedded-clickhouse v0.4.0 h1:oOvS8DWqBuKTwFclBefZR4oq3l2BPVJfjXRVIf80ZEk= +github.com/franchb/embedded-clickhouse v0.4.0/go.mod h1:03D51zEeJ1QAOaReNR3SSlmb7hJQ91BK/KmOynOT288= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= @@ -53,65 +30,67 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= -github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= -github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= -github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= -github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= -github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= +github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY= +github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw= github.com/paulmach/orb v0.13.0/go.mod h1:6scRWINywA2Jf05dcjOfLfxrUIMECvTSG2MVbRLxu/k= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= @@ -122,67 +101,93 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= -github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= -github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= -pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/internal/testutil/embeddedch/embeddedch.go b/internal/testutil/embeddedch/embeddedch.go new file mode 100644 index 0000000..f301277 --- /dev/null +++ b/internal/testutil/embeddedch/embeddedch.go @@ -0,0 +1,323 @@ +// Package embeddedch boots a ClickHouse server as a host subprocess for tests +// via franchb/embedded-clickhouse. It replaces the testcontainers-based +// fixtures used across this repo, eliminating Docker/Ryuk/proxy plumbing for +// any test that doesn't require Antalya-specific server features. +// +// Stock ClickHouse 26.3 is the default; pass WithFlavor(FlavorAntalya) to run +// the Altinity Antalya binary. On Linux the binary is extracted once from the +// production Docker image into ~/.cache/embedded-clickhouse/. On non-Linux +// hosts (macOS, ...) the binary must be present at that location ahead of +// time — see docs/development_and_testing.md for build/install steps. +package embeddedch + +import ( + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "sync" + "syscall" + "testing" + "time" + + "github.com/altinity/altinity-mcp/pkg/config" + embeddedclickhouse "github.com/franchb/embedded-clickhouse" + "github.com/stretchr/testify/require" +) + +// AntalyaImageRef is the Altinity Antalya image used as the source for the +// extracted Antalya ClickHouse binary on Linux. Bump in lockstep with +// production deployments. +const AntalyaImageRef = "altinity/clickhouse-server:26.1.11.20001.altinityantalya" + +// Flavor selects which ClickHouse binary embedded-clickhouse runs. +type Flavor int + +const ( + // FlavorStock uses the upstream ClickHouse binary that + // embedded-clickhouse fetches from GitHub releases. + FlavorStock Flavor = iota + // FlavorAntalya runs the Altinity Antalya clickhouse binary cached at + // embeddedClickHouseCacheDir() (typically ~/.cache/embedded-clickhouse/). + // On Linux the binary is extracted once from AntalyaImageRef on first use. + // On non-Linux hosts the binary must be built locally and dropped into the + // cache dir — see docs/development_and_testing.md for instructions. + FlavorAntalya +) + +// Options customizes the ClickHouse fixture spun up by Setup. +type Options struct { + Flavor Flavor + ConfigDropIns []string + // UsersXML, if non-empty, is written to /users.xml beside the + // generated config.xml. Use it when a config.d drop-in references + // `users.xml` (Antalya's + // token_processors / user_directories does this) — the file would + // otherwise not exist and ClickHouse fails startup with CANNOT_LOAD_CONFIG. + UsersXML string + // Protocol controls which port (HTTP or TCP) is reflected in the + // returned ClickHouseConfig. Defaults to HTTP. + Protocol config.ClickHouseProtocol + StartTimeout time.Duration +} + +// Option mutates Options. +type Option func(*Options) + +// WithFlavor selects the ClickHouse flavor. +func WithFlavor(f Flavor) Option { return func(o *Options) { o.Flavor = f } } + +// WithConfigDropIn adds an XML drop-in written to /config.d/N.xml +// before Start. ClickHouse auto-merges these into its main config; this is +// the no-Docker equivalent of testcontainers' Files: []ContainerFile. +func WithConfigDropIn(xml string) Option { + return func(o *Options) { o.ConfigDropIns = append(o.ConfigDropIns, xml) } +} + +// WithTCPProtocol returns the native protocol port instead of HTTP. +func WithTCPProtocol() Option { + return func(o *Options) { o.Protocol = config.TCPProtocol } +} + +// WithStartTimeout overrides the default 60s start timeout. +func WithStartTimeout(d time.Duration) Option { + return func(o *Options) { o.StartTimeout = d } +} + +// WithUsersXML writes the given XML to /users.xml beside config.xml. +// Pair with config.d drop-ins that declare . +func WithUsersXML(xml string) Option { + return func(o *Options) { o.UsersXML = xml } +} + +// Setup boots a ClickHouse server as a host subprocess and returns a +// ClickHouseConfig pointing at it. The server is shut down via t.Cleanup. +// +// In short test mode (-short) the test is skipped to avoid the binary +// download cost on quick local iterations. +func Setup(t *testing.T, opts ...Option) *config.ClickHouseConfig { + t.Helper() + if testing.Short() { + t.Skip("skipping embedded ClickHouse in short mode") + } + + o := Options{ + Protocol: config.HTTPProtocol, + StartTimeout: 60 * time.Second, + } + for _, fn := range opts { + fn(&o) + } + + cfgBuilder := embeddedclickhouse.DefaultConfig(). + Version(embeddedclickhouse.V26_3). + StartTimeout(o.StartTimeout) + + if o.Flavor == FlavorAntalya { + bin := EnsureAntalyaBinary(t) + cfgBuilder = cfgBuilder.BinaryPath(bin) + } else { + // embedded-clickhouse v0.4.0's cache layer locks within a process but + // not across processes. `go test ./...` runs each package as a + // separate binary, so multiple processes may race to download and + // extract the same archive into ~/.cache/embedded-clickhouse, + // corrupting the .tmp file with "write binary: unexpected EOF" or + // "rename temp file: no such file or directory". Hold an OS-level + // flock around the first start until the archive lands, then release. + release, err := acquireFileLock(t, embeddedClickHouseCacheLockPath()) + require.NoError(t, err) + defer release() + } + + if len(o.ConfigDropIns) > 0 || o.UsersXML != "" { + dataDir := t.TempDir() + if len(o.ConfigDropIns) > 0 { + configDDir := filepath.Join(dataDir, "config.d") + require.NoError(t, os.MkdirAll(configDDir, 0o755)) + for i, xml := range o.ConfigDropIns { + path := filepath.Join(configDDir, "drop-in-"+strconv.Itoa(i)+".xml") + require.NoError(t, os.WriteFile(path, []byte(xml), 0o644)) + } + } + if o.UsersXML != "" { + require.NoError(t, os.WriteFile(filepath.Join(dataDir, "users.xml"), []byte(o.UsersXML), 0o644)) + } + cfgBuilder = cfgBuilder.DataPath(dataDir) + } + + ch := embeddedclickhouse.NewServer(cfgBuilder) + require.NoError(t, ch.Start(), "embedded-clickhouse start failed") + t.Cleanup(func() { _ = ch.Stop() }) + + addr := ch.HTTPAddr() + if o.Protocol == config.TCPProtocol { + addr = ch.TCPAddr() + } + host, port, err := net.SplitHostPort(addr) + require.NoError(t, err) + portInt, err := strconv.Atoi(port) + require.NoError(t, err) + + return &config.ClickHouseConfig{ + Host: host, + Port: portInt, + Database: "default", + Username: "default", + Password: "", + Protocol: o.Protocol, + ReadOnly: false, + MaxExecutionTime: 60, + } +} + +// antalyaBinaryCache memoizes the extracted binary path for the lifetime +// of the test process so we only extract once per `go test` invocation. +var ( + antalyaBinaryOnce sync.Once + antalyaBinaryPath string + antalyaBinaryErr error +) + +// EnsureAntalyaBinary returns the path to a cached Antalya clickhouse binary +// at embeddedClickHouseCacheDir(). +// +// On Linux the binary is extracted once from AntalyaImageRef via Docker. +// On non-Linux hosts the binary must already be present at the expected path +// (Antalya does not publish macOS/Windows Docker images, and a Linux ELF +// cannot run as a host subprocess on macOS). When missing on non-Linux, this +// fails with a message pointing at docs/development_and_testing.md, which +// covers building clickhouse from source on macOS. +func EnsureAntalyaBinary(t *testing.T) string { + t.Helper() + antalyaBinaryOnce.Do(func() { + antalyaBinaryPath, antalyaBinaryErr = ensureAntalyaBinary() + }) + require.NoError(t, antalyaBinaryErr, "failed to provide Antalya clickhouse binary") + require.NotEmpty(t, antalyaBinaryPath, "Antalya binary path is empty") + return antalyaBinaryPath +} + +// AntalyaBinaryPath is the on-disk location where EnsureAntalyaBinary looks +// for (and on Linux, caches) the Antalya clickhouse binary. +func AntalyaBinaryPath() string { + return filepath.Join(embeddedClickHouseCacheDir(), "clickhouse-"+safeFileName(AntalyaImageRef)) +} + +func ensureAntalyaBinary() (string, error) { + binPath := AntalyaBinaryPath() + if st, err := os.Stat(binPath); err == nil && st.Mode().IsRegular() && st.Size() > 0 { + return binPath, nil + } + if runtime.GOOS != "linux" { + return "", fmt.Errorf( + "Antalya clickhouse binary not found at %s.\n"+ + "On %s/%s the binary cannot be extracted from the Antalya Docker image (Linux ELF only).\n"+ + "Build clickhouse from source and copy the resulting binary into %s — see docs/development_and_testing.md.", + binPath, runtime.GOOS, runtime.GOARCH, embeddedClickHouseCacheDir(), + ) + } + return extractAntalyaBinaryFromDocker(binPath) +} + +// extractAntalyaBinaryFromDocker pulls AntalyaImageRef and copies +// /usr/bin/clickhouse out of a non-running container into binPath. Subsequent +// callers reuse the cached file when the underlying tag hasn't changed. +func extractAntalyaBinaryFromDocker(binPath string) (string, error) { + if err := os.MkdirAll(filepath.Dir(binPath), 0o755); err != nil { + return "", err + } + + if out, err := exec.Command("docker", "pull", AntalyaImageRef).CombinedOutput(); err != nil { + return "", &extractErr{stage: "docker pull", out: string(out), err: err} + } + + cname := "antalya-extract-" + strconv.FormatInt(time.Now().UnixNano(), 36) + if out, err := exec.Command("docker", "create", "--name", cname, AntalyaImageRef).CombinedOutput(); err != nil { + return "", &extractErr{stage: "docker create", out: string(out), err: err} + } + defer func() { + _ = exec.Command("docker", "rm", "-f", cname).Run() + }() + + if out, err := exec.Command("docker", "cp", cname+":/usr/bin/clickhouse", binPath).CombinedOutput(); err != nil { + return "", &extractErr{stage: "docker cp", out: string(out), err: err} + } + if err := os.Chmod(binPath, 0o755); err != nil { + return "", err + } + return binPath, nil +} + +type extractErr struct { + stage string + out string + err error +} + +func (e *extractErr) Error() string { + if e.out != "" { + return e.stage + ": " + e.err.Error() + "\n" + e.out + } + return e.stage + ": " + e.err.Error() +} + +// embeddedClickHouseCacheDir is the on-disk cache shared between the Antalya +// extraction path and the cross-process lock used by the stock-CH download +// path. Always ~/.cache/embedded-clickhouse/ — we deliberately avoid +// os.UserCacheDir() because it resolves to ~/Library/Caches on macOS, which +// would split the cache between OSes and surprise developers who built the +// Antalya binary by hand following docs/development_and_testing.md. Falls +// back to /tmp only if $HOME cannot be determined. +func embeddedClickHouseCacheDir() string { + home, err := os.UserHomeDir() + if err != nil || home == "" { + home = "/tmp" + } + cacheDir := filepath.Join(home, ".cache", "embedded-clickhouse") + _ = os.MkdirAll(cacheDir, 0o755) + return cacheDir +} + +// embeddedClickHouseCacheLockPath returns the path to a process-private lock +// file used to serialize concurrent first-time stock-CH binary extractions +// when go test runs packages in parallel. +func embeddedClickHouseCacheLockPath() string { + return filepath.Join(embeddedClickHouseCacheDir(), ".altinity-mcp-extract.lock") +} + +// acquireFileLock takes an exclusive flock on the given path, returning a +// release function the caller must defer. Blocks until the lock is granted. +// The lock file is created if it doesn't exist; we never delete it (multiple +// concurrent processes need a stable inode to flock on). +func acquireFileLock(t *testing.T, path string) (func(), error) { + t.Helper() + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + return nil, err + } + if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + _ = f.Close() + return nil, err + } + return func() { + _ = syscall.Flock(int(f.Fd()), syscall.LOCK_UN) + _ = f.Close() + }, nil +} + +func safeFileName(s string) string { + out := make([]byte, 0, len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '.', c == '-', c == '_': + out = append(out, c) + default: + out = append(out, '_') + } + } + return string(out) +} diff --git a/pkg/clickhouse/client_test.go b/pkg/clickhouse/client_test.go index 7a64ae3..771d4d9 100644 --- a/pkg/clickhouse/client_test.go +++ b/pkg/clickhouse/client_test.go @@ -5,68 +5,17 @@ import ( "testing" "time" + "github.com/altinity/altinity-mcp/internal/testutil/embeddedch" "github.com/altinity/altinity-mcp/pkg/config" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" ) -// setupClickHouseContainer sets up a ClickHouse container for testing. -func setupClickHouseContainer(t *testing.T) *config.ClickHouseConfig { +// setupEmbeddedClickHouse boots a ClickHouse server as a host subprocess via +// embedded-clickhouse and returns a TCP-protocol config. This replaces the +// previous testcontainers-based fixture. +func setupEmbeddedClickHouse(t *testing.T) *config.ClickHouseConfig { t.Helper() - ctx := context.Background() - - totalStart := time.Now() - - req := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp", "9000/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - "CLICKHOUSE_DB": "default", - "CLICKHOUSE_USER": "default", - "CLICKHOUSE_PASSWORD": "", - "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT": "1", - }, - WaitingFor: wait.ForHTTP("/").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(2 * time.Second), - } - containerStart := time.Now() - chContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - containerElapsed := time.Since(containerStart) - require.NoError(t, err) - - t.Cleanup(func() { - cleanupStart := time.Now() - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - if err := chContainer.Terminate(cleanupCtx); err != nil { - t.Logf("Warning: failed to terminate container: %v", err) - } - t.Logf("[container/%s] cleanup took %s", req.Image, time.Since(cleanupStart)) - }) - - host, err := chContainer.Host(ctx) - require.NoError(t, err) - - port, err := chContainer.MappedPort(ctx, "9000") - require.NoError(t, err) - - t.Logf("[container/%s] start=%s total=%s", req.Image, containerElapsed, time.Since(totalStart)) - - return &config.ClickHouseConfig{ - Host: host, - Port: int(port.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.TCPProtocol, - ReadOnly: false, - MaxExecutionTime: 60, - Limit: 0, - } + return embeddedch.Setup(t, embeddedch.WithTCPProtocol()) } // TestNewClient tests client creation @@ -109,7 +58,7 @@ func TestNewClient(t *testing.T) { // TestClientOperations tests client operations with real ClickHouse func TestClientOperations(t *testing.T) { t.Parallel() - cfg := setupClickHouseContainer(t) + cfg := setupEmbeddedClickHouse(t) ctx := context.Background() client, err := NewClient(ctx, *cfg) @@ -173,7 +122,7 @@ func TestClientErrorPaths(t *testing.T) { t.Run("describe_table_not_exists", func(t *testing.T) { t.Parallel() - cfg := setupClickHouseContainer(t) + cfg := setupEmbeddedClickHouse(t) ctx := context.Background() client, err := NewClient(ctx, *cfg) require.NoError(t, err) @@ -185,7 +134,7 @@ func TestClientErrorPaths(t *testing.T) { t.Run("non_select_error", func(t *testing.T) { t.Parallel() - cfg := setupClickHouseContainer(t) + cfg := setupEmbeddedClickHouse(t) ctx := context.Background() client, err := NewClient(ctx, *cfg) require.NoError(t, err) diff --git a/pkg/clickhouse/cluster_secret_test.go b/pkg/clickhouse/cluster_secret_test.go index d769f16..599d233 100644 --- a/pkg/clickhouse/cluster_secret_test.go +++ b/pkg/clickhouse/cluster_secret_test.go @@ -2,14 +2,11 @@ package clickhouse import ( "context" - "fmt" "testing" - "time" + "github.com/altinity/altinity-mcp/internal/testutil/embeddedch" "github.com/altinity/altinity-mcp/pkg/config" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" ) const ( @@ -56,57 +53,18 @@ const ( ` ) -// setupClusterSecretClickHouse launches a ClickHouse container whose -// `remote_servers` list declares a cluster with a shared interserver secret, -// plus two password-less users (alice, bob) that the driver can impersonate. -// -// Config files are written by a shell wrapper before ClickHouse starts. We -// cannot use testcontainers' file-copy API here — the isolator sandbox -// blocks the `PUT /containers//archive` Docker endpoint. +// setupClusterSecretClickHouse boots ClickHouse via embedded-clickhouse with +// `remote_servers` declaring a cluster + shared interserver secret, plus two +// password-less users (alice, bob) the driver impersonates over the cluster +// channel. Both XML pieces are merged via config.d drop-ins. func setupClusterSecretClickHouse(t *testing.T) (host string, tcpPort int) { t.Helper() - ctx := context.Background() - - script := fmt.Sprintf(`set -e -cat > /etc/clickhouse-server/config.d/cluster.xml <<'EOF' -%sEOF -cat > /etc/clickhouse-server/users.d/cluster_users.xml <<'EOF' -%sEOF -exec /entrypoint.sh -`, clusterConfigXML, clusterUsersXML) - - req := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp", "9000/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_DB": "default", - "CLICKHOUSE_USER": "default", - "CLICKHOUSE_PASSWORD": "", - "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT": "1", - }, - Entrypoint: []string{"/bin/sh", "-c", script}, - WaitingFor: wait.ForHTTP("/").WithPort("8123/tcp"). - WithStartupTimeout(60 * time.Second). - WithPollInterval(2 * time.Second), - } - c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - require.NoError(t, err) - t.Cleanup(func() { - cleanCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - if termErr := c.Terminate(cleanCtx); termErr != nil { - t.Logf("cluster fixture: terminate failed: %v", termErr) - } - }) - - h, err := c.Host(ctx) - require.NoError(t, err) - p, err := c.MappedPort(ctx, "9000") - require.NoError(t, err) - return h, int(p.Num()) + cfg := embeddedch.Setup(t, + embeddedch.WithTCPProtocol(), + embeddedch.WithConfigDropIn(clusterConfigXML), + embeddedch.WithConfigDropIn(clusterUsersXML), + ) + return cfg.Host, cfg.Port } func clusterClientConfig(host string, port int, username string, secret string) config.ClickHouseConfig { diff --git a/pkg/server/embedded_clickhouse_helpers_test.go b/pkg/server/embedded_clickhouse_helpers_test.go new file mode 100644 index 0000000..722a1e3 --- /dev/null +++ b/pkg/server/embedded_clickhouse_helpers_test.go @@ -0,0 +1,62 @@ +package server + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/altinity/altinity-mcp/internal/testutil/embeddedch" + "github.com/altinity/altinity-mcp/pkg/clickhouse" + "github.com/altinity/altinity-mcp/pkg/config" + "github.com/stretchr/testify/require" +) + +// setupEmbeddedClickHouse boots a ClickHouse server via embedded-clickhouse +// and seeds the default.test table the broader pkg/server suite relies on. +// Use setupEmbeddedClickHouseUnseeded when the test needs a clean server. +func setupEmbeddedClickHouse(t *testing.T, opts ...embeddedch.Option) *config.ClickHouseConfig { + t.Helper() + chConfig := embeddedch.Setup(t, opts...) + seedDefaultTable(t, chConfig) + return chConfig +} + +// setupEmbeddedClickHouseUnseeded boots a server without seeding default.test. +// Use this for tests that need a clean schema or define their own users via +// config.d drop-ins (where the default user may not have access yet). +func setupEmbeddedClickHouseUnseeded(t *testing.T, opts ...embeddedch.Option) *config.ClickHouseConfig { + t.Helper() + return embeddedch.Setup(t, opts...) +} + +func seedDefaultTable(t *testing.T, chConfig *config.ClickHouseConfig) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + client, err := clickhouse.NewClient(ctx, *chConfig) + require.NoError(t, err) + t.Cleanup(func() { _ = client.Close() }) + + _, err = client.ExecuteQuery(ctx, `CREATE TABLE IF NOT EXISTS default.test ( + id UInt64, + name String, + created_at DateTime + ) ENGINE = MergeTree() ORDER BY id`) + require.NoError(t, err) + _, err = client.ExecuteQuery(ctx, `INSERT INTO default.test VALUES (1, 'test1', now()), (2, 'test2', now())`) + require.NoError(t, err) +} + +// Local re-exports so call sites in pkg/server don't need to import the +// shared package by its full path. +func withFlavor(f embeddedch.Flavor) embeddedch.Option { return embeddedch.WithFlavor(f) } +func withConfigDropIn(xml string) embeddedch.Option { return embeddedch.WithConfigDropIn(xml) } + +const ( + flavorStock = embeddedch.FlavorStock + flavorAntalya = embeddedch.FlavorAntalya +) + +// portString stringifies an integer port for callers building DSNs by hand. +func portString(p int) string { return strconv.Itoa(p) } diff --git a/pkg/server/embedded_xml_config_test.go b/pkg/server/embedded_xml_config_test.go new file mode 100644 index 0000000..32f5f3e --- /dev/null +++ b/pkg/server/embedded_xml_config_test.go @@ -0,0 +1,83 @@ +package server + +import ( + "context" + "database/sql" + "testing" + "time" + + _ "github.com/Altinity/clickhouse-go/v2" + "github.com/stretchr/testify/require" +) + +// TestEmbeddedClickHouseXMLDropIn verifies that ClickHouse's standard +// auto-merge of config.d/*.xml works under embedded-clickhouse: we drop a +// custom user, profile, and a server-level setting into config.d/, then assert +// at runtime that all three took effect. +// +// embedded-clickhouse generates a single config.xml with defined +// inline (no separate users.xml), so users.d/ is ignored. Drop-ins go in +// config.d/, where ClickHouse merges them into the main config — including +// and elements. +func TestEmbeddedClickHouseXMLDropIn(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("skipping CH XML drop-in test in short mode") + } + + const customXML = ` + + 4242 + + + secret123 + + ::/0 + + tester_profile + default + + + + + 123456789 + + + +` + chConfig := setupEmbeddedClickHouseUnseeded(t, + withConfigDropIn(customXML), + ) + + dsn := "http://tester:secret123@" + chConfig.Host + ":" + portString(chConfig.Port) + "/default" + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + db, err := sql.Open("clickhouse", dsn) + require.NoError(t, err, "DSN: %s", dsn) + t.Cleanup(func() { _ = db.Close() }) + + t.Run("custom_user_authenticates", func(t *testing.T) { + var who string + require.NoError(t, db.QueryRowContext(ctx, "SELECT currentUser()").Scan(&who), + "the users.d drop-in must have created the user; auth must succeed") + require.Equal(t, "tester", who) + }) + + t.Run("custom_profile_setting_applied", func(t *testing.T) { + var maxMem string + require.NoError(t, db.QueryRowContext(ctx, "SELECT getSetting('max_memory_usage')").Scan(&maxMem), + "the profile we attached to the custom user via config.d must apply") + require.Equal(t, "123456789", maxMem, + "max_memory_usage from tester_profile in config.d must be honored") + }) + + t.Run("config_d_server_setting_applied", func(t *testing.T) { + var rows int + require.NoError(t, db.QueryRowContext(ctx, + "SELECT count() FROM system.server_settings WHERE name = 'max_connections' AND value = '4242'", + ).Scan(&rows), + "the config.d drop-in must have set max_connections=4242 server-wide") + require.Equal(t, 1, rows, + "system.server_settings should report max_connections=4242 from our drop-in") + }) +} diff --git a/pkg/server/oauth_e2e_test.go b/pkg/server/oauth_e2e_test.go index 4482562..b75dda4 100644 --- a/pkg/server/oauth_e2e_test.go +++ b/pkg/server/oauth_e2e_test.go @@ -11,25 +11,27 @@ import ( "net/http" "net/http/httptest" "net/url" - "os" "strings" "testing" "time" + "github.com/altinity/altinity-mcp/internal/testutil/embeddedch" "github.com/altinity/altinity-mcp/pkg/config" "github.com/go-jose/go-jose/v4" - "github.com/moby/moby/api/types/container" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" ) // TestOAuthE2EWithMockOIDC is an end-to-end test that validates the full OAuth2 flow // through real MCP client and OpenAPI endpoints: -// 1. A lightweight mock OIDC provider (in-process Go HTTP server) -// 2. ClickHouse (Antalya build) with token_processors for JWT auth -// 3. MCP server forwarding Bearer tokens to ClickHouse +// +// 1. A lightweight mock OIDC provider (in-process Go HTTP server bound to 127.0.0.1) +// 2. Altinity Antalya ClickHouse with token_processors for JWT auth, run as a host +// subprocess via embedded-clickhouse + an extracted Antalya binary +// 3. MCP server forwarding Bearer tokens to ClickHouse +// +// Antalya is Linux-only (no darwin binaries published), so this test +// auto-skips on non-Linux hosts via ensureAntalyaBinary. CI runs it on Linux. func TestOAuthE2EWithMockOIDC(t *testing.T) { t.Parallel() if testing.Short() { @@ -38,29 +40,32 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { ctx := context.Background() - // ---------- Step 1: Start mock OIDC provider ---------- - provider, dockerOIDCURL := newTestOAuthProviderReachableFromDocker(t, nil) - t.Logf("Mock OIDC provider URL (Docker): %s", dockerOIDCURL) + // Step 1: mock OIDC provider on 127.0.0.1. Use the full-discovery variant + // because Antalya's token_processors parser requires introspection_endpoint + // (or userinfo_endpoint) plus the standard required OIDC fields — without + // them it logs "Cannot extract userinfo_endpoint or introspection_endpoint + // from OIDC configuration" and silently disables the processor. + provider := newAntalyaOIDCProvider(t, nil) + oidcURL := provider.server.URL + t.Logf("Mock OIDC provider URL: %s", oidcURL) - // ---------- Step 2: Start ClickHouse with token_processors ---------- - chConfig := setupAntalyaClickHouseWithOIDC(t, ctx, dockerOIDCURL) + // Step 2: Antalya ClickHouse via embedded-clickhouse, configured for OIDC. + chConfig := setupEmbeddedAntalyaWithOIDC(t, oidcURL) - // ---------- Step 3: Issue a signed JWT ---------- + // Step 3: issue a signed JWT to use as the Bearer. const tokenSubject = "test-oauth-user" token := provider.issueJWT(t, map[string]interface{}{ "sub": tokenSubject, - "iss": dockerOIDCURL, + "iss": oidcURL, "aud": "test-audience", "exp": time.Now().Add(time.Hour).Unix(), "iat": time.Now().Unix(), }) require.NotEmpty(t, token, "OAuth token should not be empty") - // Verify it looks like a JWT parts := strings.Split(token, ".") require.Equal(t, 3, len(parts), "Token should be a JWT with 3 parts") - // ---------- Step 4: Test via MCP Client (InMemoryTransports) ---------- t.Run("MCP_Client", func(t *testing.T) { t.Parallel() srv := NewClickHouseMCPServer(config.Config{ @@ -73,26 +78,25 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { }, }, "test-e2e") - // Create in-memory transports clientTransport, serverTransport := mcp.NewInMemoryTransports() - // Connect server with context containing the server instance and OAuth token srvCtx := context.WithValue(ctx, CHJWEServerKey, srv) srvCtx = context.WithValue(srvCtx, OAuthTokenKey, token) serverSession, err := srv.MCPServer.Connect(srvCtx, serverTransport, nil) require.NoError(t, err, "Server connect should succeed") - defer serverSession.Close() + defer func() { + require.NoError(t, serverSession.Close()) + }() - // Connect MCP client mcpClient := mcp.NewClient( &mcp.Implementation{Name: "test-oauth-client", Version: "v0.0.1"}, nil, ) clientSession, err := mcpClient.Connect(ctx, clientTransport, nil) require.NoError(t, err, "Client connect should succeed") - defer clientSession.Close() + defer func() { + require.NoError(t, clientSession.Close()) + }() - // NOTE: MCP subtests are NOT parallel — they share clientSession - // 4a. ListTools — verify execute_query is registered t.Run("ListTools", func(t *testing.T) { toolsResult, err := clientSession.ListTools(ctx, nil) require.NoError(t, err) @@ -103,10 +107,8 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { toolNames = append(toolNames, tool.Name) } require.Contains(t, toolNames, "execute_query", "execute_query tool should be registered") - t.Logf("Listed tools: %v", toolNames) }) - // 4b. CallTool(execute_query) — query should reach ClickHouse via OAuth Bearer token t.Run("CallTool_ExecuteQuery", func(t *testing.T) { res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{ Name: "execute_query", @@ -120,9 +122,7 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { textContent, ok := res.Content[0].(*mcp.TextContent) require.True(t, ok, "Content should be TextContent") require.NotEmpty(t, textContent.Text) - t.Logf("execute_query result: %s", textContent.Text) - // Parse the JSON result and verify it has rows var queryResult map[string]interface{} require.NoError(t, json.Unmarshal([]byte(textContent.Text), &queryResult)) rows, ok := queryResult["rows"].([]interface{}) @@ -131,7 +131,6 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { require.Equal(t, tokenSubject, firstStringCell(t, rows)) }) - // 4c. ListResources — verify clickhouse://schema is registered t.Run("ListResources", func(t *testing.T) { resourcesResult, err := clientSession.ListResources(ctx, nil) require.NoError(t, err) @@ -142,10 +141,8 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { resourceURIs = append(resourceURIs, r.URI) } require.Contains(t, resourceURIs, "clickhouse://schema", "Schema resource should be registered") - t.Logf("Listed resources: %v", resourceURIs) }) - // 4d. ReadResource(clickhouse://schema) — should return schema via OAuth t.Run("ReadResource_Schema", func(t *testing.T) { res, err := clientSession.ReadResource(ctx, &mcp.ReadResourceParams{ URI: "clickhouse://schema", @@ -154,11 +151,9 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { require.NotNil(t, res) require.Greater(t, len(res.Contents), 0, "Should have contents") require.NotEmpty(t, res.Contents[0].Text, "Schema should not be empty") - t.Logf("Schema resource (first 200 chars): %.200s...", res.Contents[0].Text) }) }) - // ---------- Step 5: Test via OpenAPI Client (httptest) ---------- t.Run("OpenAPI_Client", func(t *testing.T) { t.Parallel() srv := NewClickHouseMCPServer(config.Config{ @@ -171,14 +166,12 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { }, }, "test-e2e") - // 5a. Execute query via OpenAPI with Bearer token t.Run("ExecuteQuery", func(t *testing.T) { t.Parallel() query := url.QueryEscape("SELECT currentUser() AS user, 1 AS ok") req := httptest.NewRequest(http.MethodGet, "/openapi/execute_query?query="+query, nil) req.Header.Set("Authorization", "Bearer "+token) - reqCtx := context.WithValue(req.Context(), CHJWEServerKey, srv) - req = req.WithContext(reqCtx) + req = req.WithContext(context.WithValue(req.Context(), CHJWEServerKey, srv)) rr := httptest.NewRecorder() srv.OpenAPIHandler(rr, req) @@ -191,31 +184,26 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { require.True(t, ok, "Result should have Rows") require.Greater(t, len(rows), 0, "Should have at least one row") require.Equal(t, tokenSubject, firstStringCell(t, rows)) - t.Logf("OpenAPI result: %s", rr.Body.String()) }) - // 5b. OpenAPI schema endpoint should work t.Run("OpenAPISchema", func(t *testing.T) { t.Parallel() req := httptest.NewRequest(http.MethodGet, "/openapi", nil) req.Header.Set("Authorization", "Bearer "+token) - reqCtx := context.WithValue(req.Context(), CHJWEServerKey, srv) - req = req.WithContext(reqCtx) + req = req.WithContext(context.WithValue(req.Context(), CHJWEServerKey, srv)) rr := httptest.NewRecorder() srv.OpenAPIHandler(rr, req) require.Equal(t, http.StatusOK, rr.Code, "OpenAPI schema should return 200") require.Contains(t, rr.Body.String(), "execute_query", "Schema should contain execute_query") - t.Logf("OpenAPI schema (first 200 chars): %.200s...", rr.Body.String()) }) t.Run("ExecuteQuery_MissingBearerToken", func(t *testing.T) { t.Parallel() query := url.QueryEscape("SELECT currentUser() AS user") req := httptest.NewRequest(http.MethodGet, "/openapi/execute_query?query="+query, nil) - reqCtx := context.WithValue(req.Context(), CHJWEServerKey, srv) - req = req.WithContext(reqCtx) + req = req.WithContext(context.WithValue(req.Context(), CHJWEServerKey, srv)) rr := httptest.NewRecorder() srv.OpenAPIHandler(rr, req) @@ -235,8 +223,7 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { "exp": time.Now().Add(10 * time.Minute).Unix(), "scope": "openid", })) - reqCtx := context.WithValue(req.Context(), CHJWEServerKey, srv) - req = req.WithContext(reqCtx) + req = req.WithContext(context.WithValue(req.Context(), CHJWEServerKey, srv)) rr := httptest.NewRecorder() srv.OpenAPIHandler(rr, req) @@ -248,96 +235,98 @@ func TestOAuthE2EWithMockOIDC(t *testing.T) { }) } -// ---------- Helper functions ---------- - -func generateClickHouseStartupScriptsConfig() string { - return ` - - - - CREATE ROLE OR REPLACE 'default_role' - - - GRANT SELECT ON *.* TO 'default_role' - - - -` -} - -func extractJWTStringClaim(t *testing.T, token, claim string) string { - t.Helper() - - parts := strings.Split(token, ".") - require.Len(t, parts, 3, "token should have three JWT parts") - - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - require.NoError(t, err) - - var claims map[string]any - require.NoError(t, json.Unmarshal(payload, &claims)) - - value, ok := claims[claim].(string) - require.True(t, ok, "token should include string %q claim", claim) - - return value -} - -func firstStringCell(t *testing.T, rows []interface{}) string { +// setupEmbeddedAntalyaWithOIDC boots an Antalya ClickHouse server as a host +// subprocess via embedded-clickhouse, configured with a token_processors +// drop-in pointing at the supplied OIDC discovery URL plus a startup_scripts +// drop-in that creates the default_role used by the token user_directory. +// +// Auto-skips on non-Linux hosts because Antalya only ships Linux binaries. +func setupEmbeddedAntalyaWithOIDC(t *testing.T, oidcDiscoveryURL string) config.ClickHouseConfig { t.Helper() - require.NotEmpty(t, rows, "expected at least one row") - - row, ok := rows[0].([]interface{}) - require.True(t, ok, "expected row to be an array") - require.NotEmpty(t, row, "expected row to have at least one column") - - value, ok := row[0].(string) - require.True(t, ok, "expected first column to be a string") - - return value -} + // Antalya's needs a writable for + // runtime user storage. The Antalya container shipped with + // /var/lib/clickhouse/access/, but the host process running as the CI + // user can't create that path. Use a host-writable temp dir. + accessDir := t.TempDir() -func generateUnsignedJWT(t *testing.T, claims map[string]any) string { - t.Helper() + tokenProcessorXML := fmt.Sprintf(` + + + + openid + %s/.well-known/openid-configuration + 60 + + + + + users.xml + + + %s + + + test_oidc + + + + + + +`, oidcDiscoveryURL, accessDir) - headerJSON, err := json.Marshal(map[string]string{ - "alg": "none", - "typ": "JWT", - }) - require.NoError(t, err) + startupScriptsXML := generateClickHouseStartupScriptsConfig() - payloadJSON, err := json.Marshal(claims) - require.NoError(t, err) + // The token_processor.xml above declares with a + // element pointing at users.xml. The Antalya Docker image + // ships /etc/clickhouse-server/users.xml; embedded-clickhouse does not. + // Without it, ClickHouse fails startup with CANNOT_LOAD_CONFIG. Provide + // a minimal users.xml so the path resolves; the actual users we care + // about come from the OIDC token user_directory at runtime. + const usersXML = ` + + + + + ::/0 + default + default + 1 + + + + + +` - return fmt.Sprintf( - "%s.%s.", - base64.RawURLEncoding.EncodeToString(headerJSON), - base64.RawURLEncoding.EncodeToString(payloadJSON), + cfg := setupEmbeddedClickHouseUnseeded(t, + withFlavor(flavorAntalya), + withConfigDropIn(tokenProcessorXML), + withConfigDropIn(startupScriptsXML), + embeddedch.WithUsersXML(usersXML), ) + return *cfg } -// getDockerHostIP returns the hostname reachable from Docker containers. -// We always return "host.docker.internal" and rely on the ExtraHosts container setting -// (host.docker.internal:host-gateway) so it resolves correctly on both macOS and Linux. -func getDockerHostIP() string { - return "host.docker.internal" -} - -// newTestOAuthProviderReachableFromDocker creates an OIDC provider bound to 0.0.0.0 -// so Docker containers can reach it. Returns (provider, dockerAccessURL). +// newAntalyaOIDCProvider serves the full OIDC discovery document Antalya's +// token_processors parser requires (issuer + auth/token/jwks/userinfo/ +// introspection endpoints + response_types + subject_types + id_token_signing +// algs). The shorter doc returned by newTestOAuthProvider is enough for +// forward-mode bearer-passthrough tests that never look at it — Antalya +// rejects it with "Cannot extract userinfo_endpoint or introspection_endpoint". // -// The discovery document, JWKS, and userinfo endpoints all use dockerURL directly, -// so ClickHouse containers can reach all endpoints without any forwarding. -func newTestOAuthProviderReachableFromDocker(t *testing.T, userInfoClaims map[string]interface{}) (*testOAuthProvider, string) { +// Uses an explicit net.Listen + http.Server.Serve loop instead of +// httptest.NewServer so we control Content-Length / chunked-vs-fixed framing +// exactly as the original docker-reachable variant did. Antalya's HTTP +// client occasionally drops the trailing bytes when chunked encoding kicks +// in mid-response on small bodies. +func newAntalyaOIDCProvider(t *testing.T, userInfoClaims map[string]interface{}) *testOAuthProvider { t.Helper() - - // Bind to all interfaces so Docker containers can reach us. - ln, err := net.Listen("tcp", "0.0.0.0:0") + ln, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) port := ln.Addr().(*net.TCPAddr).Port - dockerURL := fmt.Sprintf("http://%s:%d", getDockerHostIP(), port) - localURL := fmt.Sprintf("http://127.0.0.1:%d", port) // for host-side access in tests + oidcProviderURL := fmt.Sprintf("http://127.0.0.1:%d", port) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) @@ -351,23 +340,17 @@ func newTestOAuthProviderReachableFromDocker(t *testing.T, userInfoClaims map[st mux := http.NewServeMux() mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { - // Antalya CH requires a complete OIDC discovery document with introspection_endpoint - // (or userinfo_endpoint) plus the standard required OIDC fields. Without them CH - // silently ignores the document and logs "Cannot extract userinfo_endpoint or - // introspection_endpoint from OIDC configuration". Content-Length must be set - // explicitly so CH's HTTP client receives the full body over the OrbStack NAT. - doc := map[string]interface{}{ - "issuer": dockerURL, - "authorization_endpoint": dockerURL + "/auth", - "token_endpoint": dockerURL + "/token", - "jwks_uri": dockerURL + "/jwks", - "userinfo_endpoint": dockerURL + "/userinfo", - "introspection_endpoint": dockerURL + "/introspect", + body, _ := json.Marshal(map[string]interface{}{ + "issuer": oidcProviderURL, + "authorization_endpoint": oidcProviderURL + "/auth", + "token_endpoint": oidcProviderURL + "/token", + "jwks_uri": oidcProviderURL + "/jwks", + "userinfo_endpoint": oidcProviderURL + "/userinfo", + "introspection_endpoint": oidcProviderURL + "/introspect", "response_types_supported": []string{"code"}, "subject_types_supported": []string{"public"}, "id_token_signing_alg_values_supported": []string{"RS256"}, - } - body, _ := json.Marshal(doc) + }) w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) w.WriteHeader(200) @@ -402,117 +385,70 @@ func newTestOAuthProviderReachableFromDocker(t *testing.T, userInfoClaims map[st _ = json.NewEncoder(w).Encode(provider.userInfoClaims) }) - // Use plain http.Serve (not httptest) — httptest replaces the listener - // in a way that breaks 0.0.0.0 binding needed for Docker-to-host connectivity. httpSrv := &http.Server{Handler: mux} go func() { _ = httpSrv.Serve(ln) }() - time.Sleep(50 * time.Millisecond) // ensure goroutine is scheduled before container starts + time.Sleep(50 * time.Millisecond) t.Cleanup(func() { shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = httpSrv.Shutdown(shutCtx) }) - // Set a stub server so provider.server.URL works in test verification code. stub := httptest.NewUnstartedServer(nil) - stub.Listener.Close() // free the auto-created listener immediately - stub.URL = localURL + _ = stub.Listener.Close() + stub.URL = oidcProviderURL provider.server = stub - return provider, dockerURL + return provider } -// setupAntalyaClickHouseWithOIDC starts an Antalya ClickHouse container with token_processors -// pointing at the given OIDC discovery URL. -// On Linux, host.docker.internal is resolved via ExtraHosts (host-gateway). -// On macOS/Docker Desktop/OrbStack it resolves automatically. -func setupAntalyaClickHouseWithOIDC(t *testing.T, ctx context.Context, oidcDiscoveryURL string) config.ClickHouseConfig { - t.Helper() - - tokenProcessorXML := fmt.Sprintf(` +// generateClickHouseStartupScriptsConfig returns the XML drop-in that +// pre-creates the default_role granted to OIDC-authenticated users. +func generateClickHouseStartupScriptsConfig() string { + return ` - - - openid - %s/.well-known/openid-configuration - 60 - - - - - users.xml - - - /var/lib/clickhouse/access/ - - - test_oidc - - - - - + + + CREATE ROLE OR REPLACE 'default_role' + + + GRANT SELECT ON *.* TO 'default_role' + + -`, oidcDiscoveryURL) - startupScriptsXML := generateClickHouseStartupScriptsConfig() +` +} - tmpDir := t.TempDir() - tokenProcessorFile := tmpDir + "/token_processor.xml" - require.NoError(t, os.WriteFile(tokenProcessorFile, []byte(tokenProcessorXML), 0644)) - startupScriptsFile := tmpDir + "/startup_scripts.xml" - require.NoError(t, os.WriteFile(startupScriptsFile, []byte(startupScriptsXML), 0644)) - - // Inject CH config via read-only bind mounts rather than testcontainers' - // File-stream-via-archive path: PUT /containers/{id}/archive is blocked - // in some sandboxes (e.g. our agent isolator). Bind mounts go through - // container create and avoid the archive endpoint entirely. - req := testcontainers.ContainerRequest{ - Image: "altinity/clickhouse-server:25.8.16.20001.altinityantalya", - ExposedPorts: []string{"8123/tcp", "9000/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - "CLICKHOUSE_DB": "default", - "CLICKHOUSE_USER": "default", - "CLICKHOUSE_PASSWORD": "", - "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT": "1", - }, - HostConfigModifier: func(hc *container.HostConfig) { - hc.ExtraHosts = append(hc.ExtraHosts, "host.docker.internal:host-gateway") - hc.Binds = append(hc.Binds, - tokenProcessorFile+":/etc/clickhouse-server/config.d/token_processor.xml:ro", - startupScriptsFile+":/etc/clickhouse-server/config.d/startup_scripts.xml:ro", - ) - }, - WaitingFor: wait.ForHTTP("/").WithPort("8123/tcp"). - WithStartupTimeout(120 * time.Second).WithPollInterval(2 * time.Second), - } +func firstStringCell(t *testing.T, rows []interface{}) string { + t.Helper() + + require.NotEmpty(t, rows, "expected at least one row") + + row, ok := rows[0].([]interface{}) + require.True(t, ok, "expected row to be an array") + require.NotEmpty(t, row, "expected row to have at least one column") + + value, ok := row[0].(string) + require.True(t, ok, "expected first column to be a string") + + return value +} + +func generateUnsignedJWT(t *testing.T, claims map[string]any) string { + t.Helper() - ctr, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, + headerJSON, err := json.Marshal(map[string]string{ + "alg": "none", + "typ": "JWT", }) require.NoError(t, err) - t.Cleanup(func() { - cleanupCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - _ = ctr.Terminate(cleanupCtx) - }) - host, err := ctr.Host(ctx) - require.NoError(t, err) - httpPort, err := ctr.MappedPort(ctx, "8123") + payloadJSON, err := json.Marshal(claims) require.NoError(t, err) - t.Logf("Antalya ClickHouse HTTP at %s:%s", host, httpPort.Port()) - - return config.ClickHouseConfig{ - Host: host, - Port: int(httpPort.Num()), - Database: "default", - Username: "default", - Password: "", - Protocol: config.HTTPProtocol, - ReadOnly: false, - MaxExecutionTime: 60, - } + return fmt.Sprintf( + "%s.%s.", + base64.RawURLEncoding.EncodeToString(headerJSON), + base64.RawURLEncoding.EncodeToString(payloadJSON), + ) } diff --git a/pkg/server/oauth_gating_embedded_test.go b/pkg/server/oauth_gating_embedded_test.go new file mode 100644 index 0000000..938c79b --- /dev/null +++ b/pkg/server/oauth_gating_embedded_test.go @@ -0,0 +1,59 @@ +package server + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/altinity/altinity-mcp/pkg/config" + "github.com/stretchr/testify/require" +) + +// TestOAuthGatingViaOpenAPI_Embedded mirrors the +// TestOpenAPIHandlers/combined_auth_oauth_only_via_openapi subtest in +// server_test.go but uses embedded-clickhouse instead of testcontainers. +// +// Gating mode validates the bearer locally (HS256 over the gating secret) and +// connects to CH with static credentials, so this test exercises the full MCP +// OAuth path without any Antalya-specific CH-side config. +func TestOAuthGatingViaOpenAPI_Embedded(t *testing.T) { + t.Parallel() + + chConfig := setupEmbeddedClickHouse(t) + + const gatingSecret = "test-gating-secret-32-byte-key!!" + srv := NewClickHouseMCPServer(config.Config{ + ClickHouse: *chConfig, + Server: config.ServerConfig{ + JWE: config.JWEConfig{ + Enabled: true, + JWESecretKey: "this-is-a-32-byte-secret-key!!", + JWTSecretKey: "jwt-secret", + }, + OAuth: config.OAuthConfig{ + Enabled: true, + Mode: "gating", + GatingSecretKey: gatingSecret, + }, + }, + }, "test") + + oauthToken := mintSelfIssuedToken(t, gatingSecret, map[string]interface{}{ + "sub": "user123", + "exp": time.Now().Add(time.Hour).Unix(), + }) + + req := httptest.NewRequest(http.MethodGet, "/openapi/execute_query?query=SELECT%201", nil) + req.Header.Set("Authorization", "Bearer "+oauthToken) + req = req.WithContext(context.WithValue(req.Context(), CHJWEServerKey, srv)) + + rr := httptest.NewRecorder() + srv.OpenAPIHandler(rr, req) + + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.True(t, strings.Contains(rr.Body.String(), `"data"`) || strings.Contains(rr.Body.String(), `"rows"`), + "response missing data/rows: %s", rr.Body.String()) +} diff --git a/pkg/server/server_auth_jwe_test.go b/pkg/server/server_auth_jwe_test.go index 83cd6e1..98cf1c6 100644 --- a/pkg/server/server_auth_jwe_test.go +++ b/pkg/server/server_auth_jwe_test.go @@ -16,7 +16,7 @@ import ( func TestJWEAuthentication(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) jweSecretKey := "this-is-a-32-byte-secret-key!!" jwtSecretKey := "test-jwt-secret-key-123" diff --git a/pkg/server/server_auth_oauth_test.go b/pkg/server/server_auth_oauth_test.go index d6a2442..8ac268a 100644 --- a/pkg/server/server_auth_oauth_test.go +++ b/pkg/server/server_auth_oauth_test.go @@ -633,7 +633,7 @@ func TestOAuthClearClickHouseCredentials(t *testing.T) { func TestOAuthAndJWECombined(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) provider := newTestOAuthProvider(t, nil) jweSecretKey := "this-is-a-32-byte-secret-key!!" @@ -883,7 +883,7 @@ func TestOAuthAndJWECombined(t *testing.T) { // TestOAuthOpenAPIHandler tests OpenAPI handler with OAuth authentication func TestOAuthOpenAPIHandler(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) provider := newTestOAuthProvider(t, nil) t.Run("oauth_only_valid", func(t *testing.T) { @@ -1070,7 +1070,7 @@ func TestGetOAuthClaimsFromCtx(t *testing.T) { func TestGetClickHouseClientWithOAuth(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) t.Run("no_oauth_forwarding", func(t *testing.T) { t.Parallel() @@ -1330,7 +1330,7 @@ func TestValidateAuth(t *testing.T) { func TestOAuthMCPToolExecution(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) provider := newTestOAuthProvider(t, nil) t.Run("execute_query_with_oauth", func(t *testing.T) { @@ -1456,21 +1456,24 @@ func TestOAuthMCPToolExecution(t *testing.T) { // TestOAuthOpenAPIFullFlow tests complete OAuth flow for OpenAPI endpoint func TestOAuthOpenAPIFullFlow(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) provider := newTestOAuthProvider(t, nil) t.Run("complete_oauth_openapi_flow", func(t *testing.T) { t.Parallel() - ctx := context.Background() - dockerProvider, dockerOIDCURL := newTestOAuthProviderReachableFromDocker(t, nil) - dockerChConfig := setupAntalyaClickHouseWithOIDC(t, ctx, dockerOIDCURL) + // Antalya is required for token_processors-driven OIDC validation in CH. + // Use newAntalyaOIDCProvider (full discovery doc) — Antalya rejects + // the shorter doc returned by newTestOAuthProvider. + // setupEmbeddedAntalyaWithOIDC auto-skips on non-Linux hosts. + oidcProvider := newAntalyaOIDCProvider(t, nil) + antalyaCH := setupEmbeddedAntalyaWithOIDC(t, oidcProvider.server.URL) srv := NewClickHouseMCPServer(config.Config{ - ClickHouse: dockerChConfig, + ClickHouse: antalyaCH, Server: config.ServerConfig{OAuth: config.OAuthConfig{Enabled: true, Mode: "forward"}}, }, "test") - oauthToken := dockerProvider.issueJWT(t, map[string]interface{}{ + oauthToken := oidcProvider.issueJWT(t, map[string]interface{}{ "sub": "service-account-123", - "iss": dockerOIDCURL, + "iss": oidcProvider.server.URL, "exp": time.Now().Add(time.Hour).Unix(), }) req := httptest.NewRequest(http.MethodGet, "/openapi/execute_query?query=SELECT%20version()%20as%20version", nil) diff --git a/pkg/server/server_dynamic_tools_test.go b/pkg/server/server_dynamic_tools_test.go index 4a18040..7f353f7 100644 --- a/pkg/server/server_dynamic_tools_test.go +++ b/pkg/server/server_dynamic_tools_test.go @@ -98,7 +98,7 @@ func TestMakeDynamicToolHandler_NoServerInContext(t *testing.T) { func TestMakeDynamicToolHandler_WithClickHouse(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) // prepare parameterized view client, err := clickhouse.NewClient(ctx, *chConfig) @@ -158,7 +158,7 @@ func TestMakeDynamicToolHandler_WithClickHouse(t *testing.T) { func TestRegisterDynamicTools_SuccessAndOverlap(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) client, err := clickhouse.NewClient(ctx, *chConfig) require.NoError(t, err) defer func() { require.NoError(t, client.Close()) }() @@ -352,7 +352,7 @@ func TestEnsureDynamicTools_NoRules(t *testing.T) { // TestEnsureDynamicTools_InvalidRegexp tests invalid regexp in rules func TestEnsureDynamicTools_InvalidRegexp(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -372,7 +372,7 @@ func TestEnsureDynamicTools_InvalidRegexp(t *testing.T) { // TestEnsureDynamicTools_NamedRuleNoMatch tests named rule that matches no views func TestEnsureDynamicTools_NamedRuleNoMatch(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -394,7 +394,7 @@ func TestEnsureDynamicTools_NamedRuleNoMatch(t *testing.T) { func TestEnsureDynamicTools_NamedRuleMultipleMatches(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) client, err := clickhouse.NewClient(ctx, *chConfig) require.NoError(t, err) @@ -426,7 +426,7 @@ func TestEnsureDynamicTools_NamedRuleMultipleMatches(t *testing.T) { // TestMakeDynamicToolHandler_QueryError tests handler when query fails func TestMakeDynamicToolHandler_QueryError(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := &ClickHouseJWEServer{ Config: config.Config{ @@ -519,7 +519,7 @@ func TestMakeDynamicToolHandler_GetClientError(t *testing.T) { func TestMakeDynamicToolHandler_WithParams(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) // Create a view with multiple param types client, err := clickhouse.NewClient(ctx, *chConfig) @@ -959,7 +959,7 @@ func TestBuildDynamicToolAnnotations(t *testing.T) { func TestEnsureDynamicToolsE2E(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) t.Run("no_dynamic_tools_config", func(t *testing.T) { t.Parallel() diff --git a/pkg/server/server_query_test.go b/pkg/server/server_query_test.go index 2bbe026..f90e802 100644 --- a/pkg/server/server_query_test.go +++ b/pkg/server/server_query_test.go @@ -17,7 +17,7 @@ import ( func TestHandleExecuteQuery(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -227,7 +227,7 @@ func TestHandleExecuteQuery_EmptyQuery(t *testing.T) { // TestHandleExecuteQuery_ExceedsMaxLimit tests limit exceeding config max func TestHandleExecuteQuery_ExceedsMaxLimit(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: config.ClickHouseConfig{ @@ -261,7 +261,7 @@ func TestHandleExecuteQuery_ExceedsMaxLimit(t *testing.T) { // TestHandleExecuteQuery_WithQueryWithExistingLimit tests query already having limit func TestHandleExecuteQuery_WithQueryWithExistingLimit(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -547,7 +547,7 @@ func TestEffectiveMaxQueryLength(t *testing.T) { func TestHandleExecuteQueryE2E(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, diff --git a/pkg/server/server_resources_test.go b/pkg/server/server_resources_test.go index 4b52bb5..757fd8e 100644 --- a/pkg/server/server_resources_test.go +++ b/pkg/server/server_resources_test.go @@ -13,7 +13,7 @@ import ( func TestHandleSchemaResource(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -44,7 +44,7 @@ func TestHandleSchemaResource(t *testing.T) { func TestHandleTableResource(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -133,7 +133,7 @@ func TestHandleTableResource_EmptyDatabaseOrTable(t *testing.T) { func TestHandleSchemaResourceE2E(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -160,7 +160,7 @@ func TestHandleSchemaResourceE2E_NoServer(t *testing.T) { func TestHandleTableResourceE2E(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 9a1beb1..aedb65d 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -14,8 +14,6 @@ import ( "github.com/altinity/altinity-mcp/pkg/jwe_auth" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" ) type captureServer struct { @@ -52,86 +50,10 @@ func generateJWEToken(t *testing.T, claims map[string]interface{}, jweSecretKey return token } -// setupClickHouseContainer sets up a ClickHouse container for testing. -func setupClickHouseContainer(t *testing.T) *config.ClickHouseConfig { - t.Helper() - ctx := context.Background() // Use background context instead of test context to avoid cancellation issues - - totalStart := time.Now() - - req := testcontainers.ContainerRequest{ - Image: "clickhouse/clickhouse-server:latest", - ExposedPorts: []string{"8123/tcp", "9000/tcp"}, - Env: map[string]string{ - "CLICKHOUSE_SKIP_USER_SETUP": "1", - "CLICKHOUSE_DB": "default", - "CLICKHOUSE_USER": "default", - "CLICKHOUSE_PASSWORD": "", - "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT": "1", - }, - WaitingFor: wait.ForHTTP("/").WithPort("8123/tcp").WithStartupTimeout(30 * time.Second).WithPollInterval(2 * time.Second), - } - containerStart := time.Now() - chContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ - ContainerRequest: req, - Started: true, - }) - containerElapsed := time.Since(containerStart) - require.NoError(t, err) - - t.Cleanup(func() { - cleanupStart := time.Now() - if err := chContainer.Terminate(context.Background()); err != nil { - t.Logf("Failed to terminate container: %v", err) - } - t.Logf("[container/%s] cleanup took %s", req.Image, time.Since(cleanupStart)) - }) - - host, err := chContainer.Host(ctx) - require.NoError(t, err) - - port, err := chContainer.MappedPort(ctx, "8123") - require.NoError(t, err) - - chConfig := &config.ClickHouseConfig{ - Host: host, - Port: int(port.Num()), - Database: "default", - Username: "default", - Protocol: config.HTTPProtocol, - } - - // Create a client to test the connection and create test tables - setupStart := time.Now() - client, err := clickhouse.NewClient(ctx, *chConfig) - require.NoError(t, err) - - // Create a test table - _, err = client.ExecuteQuery(ctx, `CREATE TABLE IF NOT EXISTS default.test ( - id UInt64, - name String, - created_at DateTime - ) ENGINE = MergeTree() ORDER BY id`) - require.NoError(t, err) - - // Insert some test data - _, err = client.ExecuteQuery(ctx, `INSERT INTO default.test VALUES (1, 'test1', now()), (2, 'test2', now())`) - require.NoError(t, err) - - // Close the client after setup - err = client.Close() - require.NoError(t, err) - setupElapsed := time.Since(setupStart) - - t.Logf("[container/%s] start=%s setup=%s total=%s", req.Image, containerElapsed, setupElapsed, time.Since(totalStart)) - - return chConfig -} - // TestOpenAPIHandlers tests the OpenAPI handlers func TestOpenAPIHandlers(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) t.Run("serves_openapi_schema", func(t *testing.T) { t.Parallel() @@ -542,7 +464,7 @@ func TestOpenAPI_SchemaIncludesCombinedAuthPaths(t *testing.T) { func TestHandleDynamicToolOpenAPI_PostExecutes(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) client, err := clickhouse.NewClient(ctx, *chConfig) require.NoError(t, err) defer func() { require.NoError(t, client.Close()) }() @@ -636,7 +558,7 @@ func TestHandleDynamicToolOpenAPI_Errors(t *testing.T) { func TestLazyLoading_OpenAPISchema(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) // Create view client, err := clickhouse.NewClient(ctx, *chConfig) @@ -692,7 +614,7 @@ func TestLazyLoading_OpenAPISchema(t *testing.T) { func TestLazyLoading_MCPTools(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) // Create view client, err := clickhouse.NewClient(ctx, *chConfig) @@ -791,7 +713,7 @@ func TestAddPrompt(t *testing.T) { func TestHandleExecuteQueryOpenAPI_MethodNotAllowed(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -810,7 +732,7 @@ func TestHandleExecuteQueryOpenAPI_MethodNotAllowed(t *testing.T) { // TestHandleExecuteQueryOpenAPI_InvalidLimit tests invalid limit parameter func TestHandleExecuteQueryOpenAPI_InvalidLimit(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -854,7 +776,7 @@ func TestHandleExecuteQueryOpenAPI_InvalidLimit(t *testing.T) { // TestHandleExecuteQueryOpenAPI_ExceedsMaxLimit tests limit exceeding max func TestHandleExecuteQueryOpenAPI_ExceedsMaxLimit(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: config.ClickHouseConfig{ @@ -900,7 +822,7 @@ func TestHandleDynamicToolOpenAPI_MethodNotAllowed(t *testing.T) { // TestHandleDynamicToolOpenAPI_MissingRequiredParam tests missing required parameter func TestHandleDynamicToolOpenAPI_MissingRequiredParam(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := &ClickHouseJWEServer{ Config: config.Config{ @@ -1009,7 +931,7 @@ func TestOpenAPIHandler_InvalidJWEToken(t *testing.T) { // TestHandleDynamicToolOpenAPI_QueryError tests query execution failure func TestHandleDynamicToolOpenAPI_QueryError(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := &ClickHouseJWEServer{ Config: config.Config{ @@ -1042,7 +964,7 @@ func TestHandleDynamicToolOpenAPI_QueryError(t *testing.T) { // TestHandleExecuteQueryOpenAPI_QueryError tests query execution failure func TestHandleExecuteQueryOpenAPI_QueryError(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -1062,7 +984,7 @@ func TestHandleExecuteQueryOpenAPI_QueryError(t *testing.T) { // TestHandleExecuteQueryOpenAPI_NonSelectWithLimit tests limit on non-select query func TestHandleExecuteQueryOpenAPI_NonSelectWithLimit(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -1083,7 +1005,7 @@ func TestHandleExecuteQueryOpenAPI_NonSelectWithLimit(t *testing.T) { func TestHandleDynamicToolOpenAPI_WithOptionalParams(t *testing.T) { t.Parallel() ctx := context.Background() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) // Create a simple view client, err := clickhouse.NewClient(ctx, *chConfig) @@ -1126,7 +1048,7 @@ func TestHandleDynamicToolOpenAPI_WithOptionalParams(t *testing.T) { func TestOpenAPIHandlerE2E(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) srv := NewClickHouseMCPServer(config.Config{ ClickHouse: *chConfig, @@ -1163,7 +1085,7 @@ func TestOpenAPIHandlerE2E(t *testing.T) { func TestGetClickHouseClientWithOAuthE2E(t *testing.T) { t.Parallel() - chConfig := setupClickHouseContainer(t) + chConfig := setupEmbeddedClickHouse(t) t.Run("default_config_no_oauth", func(t *testing.T) { t.Parallel()