diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index ec278b14..bfbfeb14 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -12,23 +12,22 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.22 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: go test -v ./... - - - name: Update coverage report - uses: ncruces/go-coverage-report@21fa4b59396f242b81896a3cd212a463589b6742 - with: - report: 'false' - chart: 'true' - amend: 'false' + - name: Test + run: go test -v ./... + - name: Update coverage report + uses: ncruces/go-coverage-report@21fa4b59396f242b81896a3cd212a463589b6742 + with: + report: 'false' + chart: 'true' + amend: 'false' \ No newline at end of file diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index c913fe68..ddc0dd62 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -23,5 +23,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.57.2 - + version: v1.57.2 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8b26a811..f60de02c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,5 +46,4 @@ jobs: env: BUILD_TAG: 'latest' GOPATH: ${{ env.GOPATH }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff44fe7a..3470997a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ # Examples data directories examples/*/resources/chproxy/data examples/*/resources/clickhouse/data -examples/*/resources/clickhouse/config/*-preprocessed.xml \ No newline at end of file +examples/*/resources/clickhouse/config/*-preprocessed.xml +/config.yml diff --git a/Dockerfile b/Dockerfile index 21cb8bba..09580943 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,21 @@ -FROM debian +ARG GO_VERSION=1.22.7 +FROM golang:${GO_VERSION}-alpine AS build -RUN apt update && apt install -y ca-certificates curl +RUN apk add --no-cache --update zstd-static zstd-dev make gcc musl-dev git libc6-compat && \ + apk del openssl && \ + apk add --no-cache openssl=3.3.2-r1 -COPY chproxy / +RUN mkdir -p /go/src/github.com/contentsquare/chproxy +WORKDIR /go/src/github.com/contentsquare/chproxy +COPY . ./ +ARG EXT_BUILD_TAG +ENV EXT_BUILD_TAG=${EXT_BUILD_TAG} +RUN make release-build +RUN ls -al /go/src/github.com/contentsquare/chproxy -EXPOSE 9090 +FROM alpine +RUN apk add --no-cache curl ca-certificates +COPY --from=build /go/src/github.com/contentsquare/chproxy/chproxy* / -ENTRYPOINT ["/chproxy"] +ENTRYPOINT [ "/chproxy" ] CMD [ "--help" ] diff --git a/Dockerfile_boringcrypto b/Dockerfile_boringcrypto new file mode 100644 index 00000000..581a9537 --- /dev/null +++ b/Dockerfile_boringcrypto @@ -0,0 +1,44 @@ +ARG UBUNTU_IMAGE=ubuntu:20.04 +FROM ${UBUNTU_IMAGE} AS build + +ENV GOPATH=/gocode +ENV PATH=$PATH:$GOPATH/bin +ENV GO_VERSION=1.22 + +RUN mkdir /src +WORKDIR /src + +# golang +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + apt-utils \ + software-properties-common \ + gcc \ + libc-dev \ + && add-apt-repository -y ppa:longsleep/golang-backports \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + golang-$GO_VERSION-go \ + golang-golang-x-tools \ + && apt-get autoremove -y \ + && apt-get remove -y \ + apt-utils \ + software-properties-common + +# Create symbolic link +RUN ln -s /usr/lib/go-$GO_VERSION /gocode + +# tools +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + git \ + make \ + tzdata \ + curl \ + ca-certificates + +FROM build + +# Build chproxy +COPY . ./ +ARG EXT_BUILD_TAG +ENV GOEXPERIMENT=boringcrypto +RUN make release-build diff --git a/Makefile b/Makefile index dd6af0f1..32673055 100644 --- a/Makefile +++ b/Makefile @@ -48,8 +48,8 @@ clean: release-build: @echo "Ver: $(BUILD_TAG), OPTS: $(BUILD_OPTS)" GOOS=linux GOARCH=amd64 go build $(BUILD_OPTS) + rm chproxy-linux-amd64-*.tar.gz || true tar czf chproxy-linux-amd64-$(BUILD_TAG).tar.gz chproxy - rm chproxy-linux-amd64-*.tar.gz release: format lint test clean release-build @echo "Ver: $(BUILD_TAG), OPTS: $(BUILD_OPTS)" @@ -57,5 +57,10 @@ release: format lint test clean release-build release-build-docker: @echo "Ver: $(BUILD_TAG)" - @DOCKER_BUILDKIT=1 docker build --target build --build-arg EXT_BUILD_TAG=$(BUILD_TAG) --progress plain -t chproxy-build . - @docker run --rm --entrypoint "/bin/sh" -v $(CURDIR):/host chproxy-build -c "/bin/cp /go/src/github.com/contentsquare/chproxy/*.tar.gz /host" + @DOCKER_BUILDKIT=1 docker build -f Dockerfile --target build --build-arg EXT_BUILD_TAG=$(BUILD_TAG) --progress plain -t chproxy-build . + @docker run --rm --entrypoint "/bin/sh" -v $(CURDIR):/host chproxy-build -c "/bin/cp chproxy-linux-*-*.tar.gz /host" + +release-build-docker-fips: + @echo "Ver: $(BUILD_TAG)" + @DOCKER_BUILDKIT=1 docker build -f Dockerfile_boringcrypto --build-arg EXT_BUILD_TAG=$(BUILD_TAG)-fips --build-arg EXT_BUILD_OPTS="-tags fips" --progress plain -t chproxy-build . + @docker run --rm --entrypoint "/bin/sh" -v $(CURDIR):/host chproxy-build -c "/bin/cp /src/chproxy-*.tar.gz /host" diff --git a/dockerfile_version_test.go b/dockerfile_version_test.go new file mode 100644 index 00000000..5e401822 --- /dev/null +++ b/dockerfile_version_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "bufio" + "os" + "regexp" + "strings" + "testing" +) + +func getVersionFromFile(filename string, searchPhrase string) (string, error) { + file, err := os.Open(filename) + if err != nil { + return "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + re := regexp.MustCompile(searchPhrase) // Regex to capture Go version + for scanner.Scan() { + line := scanner.Text() + matches := re.FindStringSubmatch(line) + if len(matches) > 1 { + return matches[1], nil + } + } + + return "", scanner.Err() +} + +// Test function to check if Go version in go.mod matches Dockerfile +func TestGoVersionMatching(t *testing.T) { + goModVersionSearchPhrase := `^go\s+(\d+\.\d+)` + goModVersion, err := getVersionFromFile("go.mod", goModVersionSearchPhrase) + if err != nil { + t.Fatalf("Error getting Go version from go.mod: %v", err) + } + + dockerFileVersionSearchPhrase := "GO_VERSION=(.*)" + dockerfileVersion, err := getVersionFromFile("Dockerfile", dockerFileVersionSearchPhrase) + if err != nil { + t.Fatalf("Error getting Go version from Dockerfile: %v", err) + } + + if !strings.HasPrefix(dockerfileVersion, goModVersion) { + t.Errorf("Go version mismatch: go.mod (%s), Dockerfile (%s)", goModVersion, dockerfileVersion) + } +} + +// Test function to check if Go version in go.mod matches Dockerfile_boringcrypto +func TestGoVersionMatchingBoringCrypto(t *testing.T) { + goModVersionSearchPhrase := `^go\s+(\d+\.\d+)` + goModVersion, err := getVersionFromFile("go.mod", goModVersionSearchPhrase) + if err != nil { + t.Fatalf("Error getting Go version from go.mod: %v", err) + } + + dockerFileVersionSearchPhrase := "GO_VERSION=(.*)" + dockerfileVersion, err := getVersionFromFile("Dockerfile_boringcrypto", dockerFileVersionSearchPhrase) + if err != nil { + t.Fatalf("Error getting Go version from Dockerfile_boringcrypto: %v", err) + } + + if !strings.HasPrefix(dockerfileVersion, goModVersion) { + t.Errorf("Go version mismatch: go.mod (%s), Dockerfile (%s)", goModVersion, dockerfileVersion) + } +} diff --git a/docs/src/content/docs/index.md b/docs/src/content/docs/index.md index 446ac69e..06a66aae 100644 --- a/docs/src/content/docs/index.md +++ b/docs/src/content/docs/index.md @@ -26,6 +26,14 @@ Chproxy is an HTTP proxy and load balancer for [ClickHouse](https://ClickHouse.y - Exposes various useful [metrics](/configuration/metrics) in [Prometheus text format](https://prometheus.io/docs/instrumenting/exposition_formats/). - Configuration may be updated without restart - just send `SIGHUP` signal to `chproxy` process. - Easy to manage and run - just pass config file path to a single `chproxy` binary. +- Facilitates session affinity through `session_id` mapping, guaranteeing requests from the same user session are routed to the same upstream server (It is useful if one application server performs an initial processing step and stores the results in a temporary table; other servers can efficiently access and utilize that data by reaching the same server where the data is.) +- Service can be built to use only cryptographic algorithms approved by the Federal Information Processing Standard (FIPS) 140-2, making it suitable for processing sensitive government data. +```bash +-- to build regular artifact +make release-build-docker +-- to build artifact with FIPS support relying on Borring Crypto Module +make release-build-docker-fips: +``` - Easy to [configure](https://github.com/contentsquare/chproxy/blob/master/config/examples/simple.yml): ```yml server: diff --git a/fips.go b/fips.go new file mode 100644 index 00000000..8c9f52ea --- /dev/null +++ b/fips.go @@ -0,0 +1,5 @@ +//go:build fips + +package main + +import _ "crypto/tls/fipsonly" diff --git a/go.mod b/go.mod index 9efe9ee1..27a49e11 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.22 require ( github.com/alicebob/miniredis/v2 v2.21.0 - github.com/coreos/go-systemd/v22 v22.5.0 github.com/google/go-cmp v0.5.7 github.com/klauspost/compress v1.15.11 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 @@ -31,7 +30,7 @@ require ( github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/go.sum b/go.sum index c0c90402..5214e1cd 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 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= @@ -38,7 +36,6 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -141,8 +138,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/main_test.go b/main_test.go index 7e721a29..14f33b85 100644 --- a/main_test.go +++ b/main_test.go @@ -248,7 +248,7 @@ func TestServe(t *testing.T) { "testdata/https.cache.yml", func(t *testing.T) { // do request which response must be cached - queryURLParam := "SELECT * FROM system.numbers" + queryURLParam := "select * from system.numbers" queryBody := "LIMIT 10" expectedQuery := queryURLParam + "\n" + queryBody buf := bytes.NewBufferString(queryBody) @@ -443,21 +443,36 @@ func TestServe(t *testing.T) { startHTTP, }, { - "http POST request with session id", + "http POST request with session_id and session_timeout", "testdata/http-session-id.yml", func(t *testing.T) { + sessionName := "name" + sessionTimeout := 900 req, err := http.NewRequest("POST", - "http://127.0.0.1:9090/?query_id=45395792-a432-4b92-8cc9-536c14e1e1a9&extremes=0&session_id=default-session-id233", - bytes.NewBufferString("SELECT * FROM system.numbers LIMIT 10")) + "http://127.0.0.1:9090/?query_id=45395792-a432-4b92-8cc9-536c14e1e1a9&extremes=0&session_id="+sessionName+"&session_timeout="+strconv.Itoa(sessionTimeout), + bytes.NewBufferString("select * from system.numbers LIMIT 10")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded;") // This makes it work checkErr(t, err) resp, err := http.DefaultClient.Do(req) checkErr(t, err) - if resp.StatusCode != http.StatusOK || resp.StatusCode != http.StatusOK && resp.Header.Get("X-Clickhouse-Server-Session-Id") == "" { + if resp.StatusCode != http.StatusOK { t.Fatalf("unexpected status code: %d; expected: %d", resp.StatusCode, http.StatusOK) } + + // verify correctness of session_id + _sessionName := resp.Header.Get("X-Clickhouse-Server-Session-Id") + if _sessionName != sessionName { + t.Fatalf("unexpected value of X-Clickhouse-Server-Session-Id: %s; expected: %s", _sessionName, sessionName) + } + + // verify correctness of session_id + _sessionTimeout, _ := strconv.Atoi(resp.Header.Get("X-Clickhouse-Server-Session-Timeout")) + if _sessionTimeout != sessionTimeout { + t.Fatalf("unexpected value of X-Clickhouse-Server-Session-Timeout: %d; expected: %d", _sessionTimeout, sessionTimeout) + } + resp.Body.Close() }, startHTTP, @@ -548,7 +563,7 @@ func TestServe(t *testing.T) { func(t *testing.T) { var buf bytes.Buffer zw := gzip.NewWriter(&buf) - _, err := zw.Write([]byte("SELECT * FROM system.numbers LIMIT 10")) + _, err := zw.Write([]byte("select * from system.numbers LIMIT 10")) checkErr(t, err) zw.Close() req, err := http.NewRequest("POST", "http://127.0.0.1:9090", &buf) @@ -569,7 +584,7 @@ func TestServe(t *testing.T) { "http POST request", "testdata/http.yml", func(t *testing.T) { - buf := bytes.NewBufferString("SELECT * FROM system.numbers LIMIT 10") + buf := bytes.NewBufferString("select * from system.numbers LIMIT 10") req, err := http.NewRequest("POST", "http://127.0.0.1:9090", buf) checkErr(t, err) resp, err := http.DefaultClient.Do(req) @@ -587,7 +602,7 @@ func TestServe(t *testing.T) { func(t *testing.T) { var buf bytes.Buffer zw := gzip.NewWriter(&buf) - _, err := zw.Write([]byte("SELECT * FROM system.numbers LIMIT 1000")) + _, err := zw.Write([]byte("select * from system.numbers LIMIT 1000")) checkErr(t, err) zw.Close() req, err := http.NewRequest("POST", "http://127.0.0.1:9090", &buf) diff --git a/proxy.go b/proxy.go index 15ca73ea..072a3ee8 100644 --- a/proxy.go +++ b/proxy.go @@ -137,6 +137,11 @@ func (rp *reverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { rw.Header().Set("X-ClickHouse-Server-Session-Id", s.sessionId) } + // publish session_timeout if needed + if s.sessionId != "" { + rw.Header().Set("X-ClickHouse-Server-Session-Timeout", strconv.Itoa(s.sessionTimeout)) + } + q, shouldReturnFromCache, err := shouldRespondFromCache(s, origParams, req) if err != nil { respondWith(srw, err, http.StatusBadRequest)