diff --git a/MODULE.bazel b/MODULE.bazel index a151efc55..0e5ace6c3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -2,8 +2,8 @@ # which is pulled in transitively by rules_go/rules_proto. bazel_dep(name = "toolchains_protoc", version = "0.6.1") bazel_dep(name = "git", version = "2.55.0") -bazel_dep(name = "rules_go", version = "0.57.0") -bazel_dep(name = "gazelle", version = "0.45.0") +bazel_dep(name = "rules_go", version = "0.59.0") +bazel_dep(name = "gazelle", version = "0.53.0") bazel_dep(name = "rules_proto", version = "7.1.0") bazel_dep(name = "aspect_rules_js", version = "3.5.0") bazel_dep(name = "aspect_rules_ts", version = "3.10.1") @@ -15,6 +15,8 @@ bazel_dep(name = "rules_python", version = "2.2.0") bazel_dep(name = "bazel_lib", version = "3.7.2") bazel_dep(name = "platforms", version = "1.1.0") +bazel_dep(name = "rules_docker_compose", version = "0.4.0", dev_dependency = True) + single_version_override( module_name = "git", version = "2.55.0", diff --git a/Makefile b/Makefile index ac1961db9..f40320156 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,27 @@ # Bazel wrapper BAZEL = ./tool/bazel -# Docker Compose wrapper -COMPOSE = docker-compose +# Bazel supplies the pinned Docker Compose release for every local stack. +COMPOSE = $(BAZEL) run @rules_docker_compose//docker_compose -- # SubmitQueue compose files -COMPOSE_FILE = service/submitqueue/docker-compose.yml -WEB_COMPOSE_FILE = service/submitqueue/docker-compose.web.yml -GATEWAY_COMPOSE_FILE = service/submitqueue/gateway/server/docker-compose.yml -ORCHESTRATOR_COMPOSE_FILE = service/submitqueue/orchestrator/server/docker-compose.yml +COMPOSE_FILE = $(abspath service/submitqueue/docker-compose.yml) +GATEWAY_COMPOSE_FILE = $(abspath service/submitqueue/gateway/server/docker-compose.yml) +ORCHESTRATOR_COMPOSE_FILE = $(abspath service/submitqueue/orchestrator/server/docker-compose.yml) +WEB_COMPOSE_FILE = $(abspath service/submitqueue/docker-compose.web.yml) # Fixed project name for local manual testing (tests use unique random names) SUBMITQUEUE_LOCAL_PROJECT = submitqueue # Stovepipe compose file (single Ping-only service) -STOVEPIPE_COMPOSE_FILE = service/stovepipe/docker-compose.yml -STOVEPIPE_DEBUG_COMPOSE_FILE = service/stovepipe/docker-compose.debug.yml +STOVEPIPE_COMPOSE_FILE = $(abspath service/stovepipe/docker-compose.yml) +STOVEPIPE_DEBUG_COMPOSE_FILE = $(abspath service/stovepipe/docker-compose.debug.yml) # Fixed project name for local manual testing (tests use unique random names) STOVEPIPE_LOCAL_PROJECT = stovepipe # Runway compose files -RUNWAY_COMPOSE_FILE = service/runway/server/docker-compose.yml +RUNWAY_COMPOSE_FILE = $(abspath service/runway/server/docker-compose.yml) # Fixed project name for local manual testing (tests use unique random names) RUNWAY_LOCAL_PROJECT = runway @@ -61,9 +61,9 @@ export SQ_PROVIDER_CONFIG_DIR ?= $(REPO_ROOT)/service/submitqueue/demo/provider/ # Which compose overlay each mode needs. This cannot live in the provider # directory: the two config files say how the services are configured, not what # has to be mounted or which credential has to be present for them to start. -PROVIDER_COMPOSE_FILE_fake = service/submitqueue/docker-compose.fake.yml -PROVIDER_COMPOSE_FILE_git = service/submitqueue/docker-compose.git.yml -PROVIDER_COMPOSE_FILE_github = service/submitqueue/docker-compose.provider.yml +PROVIDER_COMPOSE_FILE_fake = $(abspath service/submitqueue/docker-compose.fake.yml) +PROVIDER_COMPOSE_FILE_git = $(abspath service/submitqueue/docker-compose.git.yml) +PROVIDER_COMPOSE_FILE_github = $(abspath service/submitqueue/docker-compose.provider.yml) PROVIDER_COMPOSE_FILE = $(PROVIDER_COMPOSE_FILE_$(PROVIDER)) # Where PROVIDER=git keeps the bare repository it lands into. Outside the diff --git a/README.md b/README.md index 71e759d03..7c7d7b177 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Cross-domain Go code (errors, metrics, consumer framework, HTTP helpers, shared ## Quick Start -Put traffic through the queue and watch it land. Requires Docker and Docker Compose, and nothing else — no repository, no account, no token. +Put traffic through the queue and watch it land. Requires Docker; Bazel supplies Docker Compose. No repository, no account, no token. ```bash # Start the full stack (Gateway + Orchestrator + Runway + MySQL) diff --git a/doc/howto/DEVELOPMENT.md b/doc/howto/DEVELOPMENT.md index b7f45f968..4586a9d25 100644 --- a/doc/howto/DEVELOPMENT.md +++ b/doc/howto/DEVELOPMENT.md @@ -3,7 +3,7 @@ ## Prerequisites - **Go 1.25+** — needed for `gopls`, `go mod`, and installing protoc plugins. Download from [go.dev/dl](https://go.dev/dl/). Note: Bazel manages its own Go toolchain for builds, but a local Go installation is required for editor tooling and dependency management. -- **Docker** and **Docker Compose** — for integration and e2e tests, and for running services locally. +- **Docker** — for integration and e2e tests, and for running services locally. Bazel supplies Docker Compose. - **direnv** (recommended) — automatically loads `.envrc` so you can use `bazel` directly instead of `./tool/bazel`. The project includes `./tool/bazel` (Bazelisk wrapper) and `.bazelversion`, so you don't need to install Bazel separately. Bazel manages its own Go toolchain for building and testing. diff --git a/doc/howto/QUICKSTART.md b/doc/howto/QUICKSTART.md index 23721e845..ea6e30e0f 100644 --- a/doc/howto/QUICKSTART.md +++ b/doc/howto/QUICKSTART.md @@ -183,9 +183,13 @@ SQ_TOKEN=$(cat ~/.sq-token) bazel run //service/submitqueue/gateway/client:gatew ### Service logs +Follow logs from every service, or just Runway: + ```bash -make local-submitqueue-logs # every service -docker compose -p submitqueue logs -f runway-service # one of them +make local-submitqueue-logs +REPO_ROOT="$PWD" ./tool/bazel run @rules_docker_compose//docker_compose -- \ + -f "$PWD/service/submitqueue/docker-compose.yml" \ + -p submitqueue logs -f runway-service ``` The message queue logs a line per message published, fetched, leased and acked, which at debug level buries everything else a service says. It is levelled separately from the rest of the service, at info by default. To follow the queue itself — chasing a message that never arrived, or a partition that never got leased — turn it back up: diff --git a/go.mod b/go.mod index 55d47b469..732fc8d8a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/bazelbuild/rules_go v0.59.0 github.com/go-sql-driver/mysql v1.6.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index b5037e22e..7b4438147 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/bazelbuild/rules_go v0.59.0 h1:RLhOwYIqeMgBpKelHEWTfIPjA37so3oa/rX+/qqq/P4= +github.com/bazelbuild/rules_go v0.59.0/go.mod h1:Pn30cb4M513fe2rQ6GiJ3q8QyrRsgC7zhuDvi50Lw4Y= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/service/runway/server/Dockerfile b/service/runway/server/Dockerfile index a4dd19fba..1bf4a03b9 100644 --- a/service/runway/server/Dockerfile +++ b/service/runway/server/Dockerfile @@ -16,7 +16,8 @@ RUN apt-get update && apt-get install -y ca-certificates git && rm -rf /var/lib/ WORKDIR /app # Built via: make build-runway-linux -COPY --chmod=0555 .docker-bin/runway ./runway +COPY .docker-bin/runway ./runway +RUN chmod 0555 ./runway EXPOSE 8080 diff --git a/service/submitqueue/gateway/server/Dockerfile b/service/submitqueue/gateway/server/Dockerfile index d000aabee..304212ad2 100644 --- a/service/submitqueue/gateway/server/Dockerfile +++ b/service/submitqueue/gateway/server/Dockerfile @@ -6,11 +6,13 @@ WORKDIR /app # Copy pre-built Linux binary # Built via: make build-submitqueue-gateway-linux -COPY --chmod=0555 .docker-bin/gateway ./gateway +COPY .docker-bin/gateway ./gateway +RUN chmod 0555 ./gateway # Sample queue configuration; the gateway reads it on startup via # QUEUE_CONFIG_PATH (set in docker-compose.yml). -COPY --chmod=0444 service/submitqueue/gateway/server/queues.yaml ./queues.yaml +COPY service/submitqueue/gateway/server/queues.yaml ./queues.yaml +RUN chmod 0444 ./queues.yaml EXPOSE 8080 diff --git a/service/submitqueue/orchestrator/server/Dockerfile b/service/submitqueue/orchestrator/server/Dockerfile index dbb9d596e..15dd1d3f8 100644 --- a/service/submitqueue/orchestrator/server/Dockerfile +++ b/service/submitqueue/orchestrator/server/Dockerfile @@ -17,7 +17,8 @@ WORKDIR /app # Copy pre-built Linux binary # Built via: make build-submitqueue-orchestrator-linux -COPY --chmod=0555 .docker-bin/orchestrator ./orchestrator +COPY .docker-bin/orchestrator ./orchestrator +RUN chmod 0555 ./orchestrator EXPOSE 8080 diff --git a/test/testutil/BUILD.bazel b/test/testutil/BUILD.bazel index 0f548dc67..e06238234 100644 --- a/test/testutil/BUILD.bazel +++ b/test/testutil/BUILD.bazel @@ -7,6 +7,7 @@ go_library( "logger.go", "schema.go", ], + data = ["@rules_docker_compose//docker_compose"], importpath = "github.com/uber/submitqueue/test/testutil", visibility = ["//test:__subpackages__"], deps = [ @@ -14,5 +15,6 @@ go_library( "@com_github_stretchr_testify//require:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//credentials/insecure:go_default_library", + "@rules_go//go/runfiles:go_default_library", ], ) diff --git a/test/testutil/compose.go b/test/testutil/compose.go index 8bf056ab6..ebc2caf6a 100644 --- a/test/testutil/compose.go +++ b/test/testutil/compose.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "github.com/bazelbuild/rules_go/go/runfiles" _ "github.com/go-sql-driver/mysql" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -43,7 +44,7 @@ type ComposeStack struct { t *testing.T log *TestLogger ctx context.Context - composeCmd []string // docker-compose command (either ["docker-compose"] or ["docker", "compose"]) + composeCmd []string // pinned Compose executable from Bazel runfiles composeEnv []string // environment shared by compose commands logCmd *exec.Cmd // background "docker compose logs -f" process } @@ -67,19 +68,6 @@ func (s *ComposeStack) command(args ...string) []string { return append(full, args...) } -// getDockerComposeCommand returns the docker compose command to use. -// Prefers the Compose V2 plugin ("docker compose") and falls back to a -// standalone docker-compose binary only when the plugin is unavailable: a -// standalone binary is often the legacy Python V1, which lacks flags the -// stack relies on (e.g. `up --wait`). -func getDockerComposeCommand() []string { - if exec.Command("docker", "compose", "version").Run() == nil { - return []string{"docker", "compose"} - } - - return []string{"docker-compose"} -} - // ComposeOption customizes how a ComposeStack is created. type ComposeOption func(*composeOptions) @@ -134,6 +122,8 @@ func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, compose // Setup Docker environment setupDockerEnv(t) + composeExecutable, err := runfiles.Rlocation("rules_docker_compose/docker_compose/current_docker_compose_toolchain/docker-compose") + require.NoError(t, err) // The image prefix is derived from the content of the declared inputs, so // every compose file participates in the hash alongside the staged build @@ -163,7 +153,7 @@ func NewComposeStack(t *testing.T, log *TestLogger, ctx context.Context, compose t: t, log: log, ctx: ctx, - composeCmd: getDockerComposeCommand(), + composeCmd: []string{composeExecutable}, composeEnv: composeEnvironment(inputHash.Sum(nil), buildContextDir), } @@ -444,11 +434,8 @@ func setupDockerEnv(t *testing.T) { t.Setenv("HOME", t.TempDir()) } - // Bazel gives tests a scratch HOME, which hides user-level Docker CLI - // plugin installs (e.g. compose v2 in ~/.docker/cli-plugins). Point - // DOCKER_CONFIG at the invoking user's docker config dir when not set - // explicitly — mirroring how the docker CLI resolves it outside the - // sandbox. Machine-level plugin installs are unaffected either way. + // Bazel's scratch HOME hides Docker contexts and credentials. Preserve + // the invoking user's Docker config unless one was supplied explicitly. if os.Getenv("DOCKER_CONFIG") == "" { if u, err := user.Current(); err == nil && u.HomeDir != "" { dockerConfig := filepath.Join(u.HomeDir, ".docker") diff --git a/tool/compose/BUILD.bazel b/tool/compose/BUILD.bazel new file mode 100644 index 000000000..b6ace7c7e --- /dev/null +++ b/tool/compose/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_python//python:defs.bzl", "py_test") + +py_test( + name = "compose_test", + size = "small", + srcs = ["compose_test.py"], + data = ["@rules_docker_compose//docker_compose"], + legacy_create_init = 0, + python_version = "3.13", + deps = ["@rules_python//python/runfiles"], +) diff --git a/tool/compose/compose_test.py b/tool/compose/compose_test.py new file mode 100644 index 000000000..52a5fb30b --- /dev/null +++ b/tool/compose/compose_test.py @@ -0,0 +1,63 @@ +"""Exercise the pinned Compose executable in runfiles without a Docker daemon.""" + +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +from python.runfiles import runfiles + + +class ComposeTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.compose = runfiles.Create().Rlocation( + "rules_docker_compose/docker_compose/current_docker_compose_toolchain/docker-compose" + ) + + def test_project_directory_and_environment(self): + with tempfile.TemporaryDirectory() as project: + Path(project, "compose.yaml").write_text( + "services:\n" + " demo:\n" + " image: ${COMPOSE_TEST_IMAGE}\n" + " volumes:\n" + " - ./content:/data\n" + ) + for absolute_config in (False, True): + with self.subTest(absolute_config=absolute_config): + env = {**os.environ, "COMPOSE_TEST_IMAGE": "busybox:1.37.0"} + config_file = str(Path(project, "compose.yaml")) if absolute_config else "compose.yaml" + result = subprocess.run( + [self.compose, "-f", config_file, "-p", "compose-test", "config", "--format", "json"], + cwd=os.path.dirname(project) if absolute_config else project, + env=env, + capture_output=True, + text=True, + check=True, + ) + config = json.loads(result.stdout) + self.assertEqual(config["name"], "compose-test") + self.assertEqual(config["services"]["demo"]["image"], "busybox:1.37.0") + self.assertEqual( + config["services"]["demo"]["volumes"][0]["source"], + str(Path(project, "content").resolve()), + ) + + def test_invalid_config_returns_failure(self): + with tempfile.TemporaryDirectory() as project: + Path(project, "compose.yaml").write_text("services: []\n") + result = subprocess.run( + [self.compose, "-p", "compose-test", "config"], + cwd=project, + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertTrue(result.stderr) + + +if __name__ == "__main__": + unittest.main()