diff --git a/Dockerfile b/Dockerfile index 21cb8bba..d1ae60bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,19 @@ -FROM debian +ARG GO_VER=1.22 +FROM golang:${GO_VER} AS build -RUN apt update && apt install -y ca-certificates curl +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 -COPY chproxy / +FROM alpine +RUN apk add --no-cache curl ca-certificates +COPY --from=build /go/src/github.com/contentsquare/chproxy/chproxy* / EXPOSE 9090 -ENTRYPOINT ["/chproxy"] +ENTRYPOINT [ "/chproxy" ] CMD [ "--help" ] diff --git a/Dockerfile_boringcrypto b/Dockerfile_boringcrypto new file mode 100644 index 00000000..0111e059 --- /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 GOVERSION=1.22 + +RUN mkdir /scr +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-$GOVERSION-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-$GOVERSION /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..3e02c9b1 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ run: build lint: go vet $(pkgs) - golangci-lint run + go list ./... | grep -v /vendor/ | xargs -n1 golint tidy: go mod tidy @@ -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)" @@ -58,4 +58,9 @@ 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 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/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.sum b/go.sum index 61868a65..f9f5cff2 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= +github.com/bsm/ginkgo/v2 v2.5.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8= +github.com/bsm/gomega v1.20.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/main_test.go b/main_test.go index 7e721a29..89b53e0b 100644 --- a/main_test.go +++ b/main_test.go @@ -443,11 +443,13 @@ 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", + "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 @@ -455,9 +457,21 @@ func TestServe(t *testing.T) { 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, diff --git a/proxy.go b/proxy.go index 24940762..02389a85 100644 --- a/proxy.go +++ b/proxy.go @@ -139,6 +139,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)