diff --git a/BUILD.bazel b/BUILD.bazel
index adb7bf6..e7a1c0d 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,5 +1,10 @@
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
+alias(
+ name = "gazelle",
+ actual = "//gazelle",
+)
+
cc_binary(
name = "exit0",
srcs = ["exit0.c"],
diff --git a/cmd/svcinit/main.go b/cmd/svcinit/main.go
index 7308d92..9b873e5 100644
--- a/cmd/svcinit/main.go
+++ b/cmd/svcinit/main.go
@@ -13,6 +13,7 @@ import (
"os/exec"
"os/signal"
"slices"
+ "strconv"
"strings"
"syscall"
"text/tabwriter"
@@ -111,14 +112,21 @@ func main() {
go func() {
defer listener.Close()
- err := svcctl.Serve(ctx, listener, r, servicesErrCh)
+ err := svcctl.Serve(ctx, listener, r, ports, servicesErrCh)
if err != nil {
log.Fatalf("svcctl.Serve: %v", err)
}
}()
port := listener.Addr().(*net.TCPAddr).Port
- os.Setenv("SVCCTL_PORT", fmt.Sprintf("%d", port))
+ portString := strconv.Itoa(port)
+ os.Setenv("SVCCTL_PORT", portString)
+
+ if testLabel == "" {
+ err = os.WriteFile("/tmp/svcctl_port", []byte(portString), 0600)
+ must(err)
+ defer os.Remove("/tmp/svcctl_port")
+ }
signalCh := make(chan os.Signal, 1)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
diff --git a/docs/itest.md b/docs/itest.md
index aa20534..fb3cef8 100644
--- a/docs/itest.md
+++ b/docs/itest.md
@@ -17,20 +17,24 @@ query:enable-reload --@rules_itest//:enable_per_service_reload
`ibazel run --config enable-reload //path/to:target`
-In addition, if can set the `hot_reloadable` attribute on an `itest_service`, the service manager will
+In addition, if the `hot_reloadable` attribute is set on an `itest_service`, the service manager will
forward the ibazel hot-reload notification over stdin instead of restarting the service.
# Service control
-The svcinit also exposes a HTTP server on `http://127.0.0.1:{SVCCTL_PORT}`. It is useful for tests
-that need to start / stop services in the midst of the test run. There are currently 4 API endpoint
-available. All of them are GET requests:
+The service manager exposes a HTTP server on `http://127.0.0.1:{SVCCTL_PORT}`. It can be used to
+start / stop services during a test run. There are currently 5 API endpoints available.
+All of them are GET requests:
1. `/v0/healthcheck?service={label}`: Returns 200 if the service is healthy, 503 otherwise.
2. `/v0/start?service={label}`: Starts the service if it is not already running.
3. `/v0/kill?service={label}[&signal={signal}]`: Send kill signal to the service if it is running.
You can optionally specify the signal to send to the service (valid values: SIGTERM and SIGKILL).
4. `/v0/wait?service={label}`: Wait for the service to exit and returns the exit code in the body.
+5. `/v0/port?service={label}`: Returns the assigned port for the given label. May be a named port.
+
+In `bazel run` mode, the service manager will write the value of `SVCCTL_PORT` to `/tmp/svcctl_port`.
+This can be used in conjunction with the `/v0/port` API to let other tools interact with the managed services.
@@ -64,8 +68,8 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
| health_check_timeout | The timeout to wait for the health check. The syntax is based on common time duration with a number, followed by the time unit. For example, 200ms
, 1s
, 2m
, 3h
, 4d
. If empty or not set, the health check will not have a timeout. | String | optional | ""
|
| hot_reloadable | If set to True, the service manager will propagate ibazel's reload notification over stdin instead of restarting the service. See the ruleset docstring for more info on using ibazel | Boolean | optional | False
|
| http_health_check_address | If set, the service manager will send an HTTP request to this address to check if the service came up in a healthy state. This check will be retried until it returns a 200 HTTP code. When used in conjunction with autoassigned ports, $${@@//label/for:service:port_name}
can be used in the address. Example: http_health_check_address = "http://127.0.0.1:$${@@//label/for:service:port_name}",
| String | optional | ""
|
-| named_ports | For each element of the list, the service manager will pick a free port and assign it to the service. The port's fully-qualified name is the service's fully-qualified label and the port name, separated by a colon. For example, a port assigned with names_ports = ["http_port"]
will be assigned a fully-qualified name of @@//label/for:service:http_port
.
Named ports are accessible through the service-port mapping. For more details, see autoassign_port
. | List of strings | optional | []
|
-| so_reuseport_aware | If set, the service manager will not release the autoassigned port. The service binary must use SO_REUSEPORT when binding it. This reduces the possibility of port collisions when running many service_tests in parallel, or when code binds port 0 without being aware of the port assignment mechanism.
Must only be set when autoassign_port is enabled. | Boolean | optional | False
|
+| named_ports | For each element of the list, the service manager will pick a free port and assign it to the service. The port's fully-qualified name is the service's fully-qualified label and the port name, separated by a colon. For example, a port assigned with named_ports = ["http_port"]
will be assigned a fully-qualified name of @@//label/for:service:http_port
.
Named ports are accessible through the service-port mapping. For more details, see autoassign_port
. | List of strings | optional | []
|
+| so_reuseport_aware | If set, the service manager will not release the autoassigned port. The service binary must use SO_REUSEPORT when binding it. This reduces the possibility of port collisions when running many service_tests in parallel, or when code binds port 0 without being aware of the port assignment mechanism.
Must only be set when autoassign_port
is enabled or named_ports
are used. | Boolean | optional | False
|
@@ -81,8 +85,7 @@ A service group is a collection of services/tasks.
It serves as a convenient way for a downstream target to depend on multiple services with a single label, without
forcing the services within the group to define a specific startup ordering with their `deps`.
-It is also useful to bring up multiple services with a single `bazel run` command, which is useful for creating
-dev environments.
+It can bring up multiple services with a single `bazel run` command, which is useful for creating dev environments.
**ATTRIBUTES**
@@ -101,7 +104,7 @@ dev environments.
itest_task(name, data, deps, env, exe)
-A task is a one-shot (not long-running binary) that is intended to be executed as part of the itest scenario creation.
+A task is a one-shot execution of a binary that is intended to run as part of the itest scenario creation.
Examples include: filesystem setup, dynamic config file generation (especially if it depends on ports), DB migrations or seed data creation.
All [common binary attributes](https://bazel.build/reference/be/common-definitions#common-attributes-binaries) are supported including `args`.
diff --git a/gazelle/BUILD.bazel b/gazelle/BUILD.bazel
index abccfc7..609e258 100644
--- a/gazelle/BUILD.bazel
+++ b/gazelle/BUILD.bazel
@@ -1,3 +1,6 @@
load("@gazelle//:def.bzl", "gazelle")
-gazelle(name = "gazelle")
+gazelle(
+ name = "gazelle",
+ visibility = ["//visibility:public"],
+)
diff --git a/private/itest.bzl b/private/itest.bzl
index caa6bfc..e5cbd0c 100644
--- a/private/itest.bzl
+++ b/private/itest.bzl
@@ -20,15 +20,19 @@ forward the ibazel hot-reload notification over stdin instead of restarting the
# Service control
-The serice manager exposes a HTTP server on `http://127.0.0.1:{SVCCTL_PORT}`. It can be used to
-start / stop services during a test run. There are currently 4 API endpoint
-available. All of them are GET requests:
+The service manager exposes a HTTP server on `http://127.0.0.1:{SVCCTL_PORT}`. It can be used to
+start / stop services during a test run. There are currently 5 API endpoints available.
+All of them are GET requests:
1. `/v0/healthcheck?service={label}`: Returns 200 if the service is healthy, 503 otherwise.
2. `/v0/start?service={label}`: Starts the service if it is not already running.
3. `/v0/kill?service={label}[&signal={signal}]`: Send kill signal to the service if it is running.
You can optionally specify the signal to send to the service (valid values: SIGTERM and SIGKILL).
4. `/v0/wait?service={label}`: Wait for the service to exit and returns the exit code in the body.
+5. `/v0/port?service={label}`: Returns the assigned port for the given label. May be a named port.
+
+In `bazel run` mode, the service manager will write the value of `SVCCTL_PORT` to `/tmp/svcctl_port`.
+This can be used in conjunction with the `/v0/port` API to let other tools interact with the managed services.
"""
load("@aspect_bazel_lib//lib:paths.bzl", "to_rlocation_path")
@@ -318,8 +322,7 @@ itest_service_group = rule(
It serves as a convenient way for a downstream target to depend on multiple services with a single label, without
forcing the services within the group to define a specific startup ordering with their `deps`.
-It can bring up multiple services with a single `bazel run` command, which is useful for creating
-dev environments.""",
+It can bring up multiple services with a single `bazel run` command, which is useful for creating dev environments.""",
)
def _create_svcinit_actions(ctx, services):
diff --git a/runner/BUILD.bazel b/runner/BUILD.bazel
index dc4aa5e..c978345 100644
--- a/runner/BUILD.bazel
+++ b/runner/BUILD.bazel
@@ -3,9 +3,9 @@ load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "runner",
srcs = [
- "runner.go",
"pgroup_unix.go",
"pgroup_windows.go",
+ "runner.go",
"service_instance.go",
"topo.go",
],
diff --git a/svcctl/BUILD.bazel b/svcctl/BUILD.bazel
index 175294f..2bc9009 100644
--- a/svcctl/BUILD.bazel
+++ b/svcctl/BUILD.bazel
@@ -2,12 +2,11 @@ load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "svcctl",
- srcs = [
- "svcctl.go",
- ],
+ srcs = ["svcctl.go"],
importpath = "rules_itest/svcctl",
visibility = ["//visibility:public"],
deps = [
"//runner",
+ "//svclib",
],
)
diff --git a/svcctl/svcctl.go b/svcctl/svcctl.go
index 2e1077c..9bd1182 100644
--- a/svcctl/svcctl.go
+++ b/svcctl/svcctl.go
@@ -8,9 +8,11 @@ import (
"net"
"net/http"
"os/exec"
- "rules_itest/runner"
"syscall"
"time"
+
+ "rules_itest/runner"
+ "rules_itest/svclib"
)
type handlerFn = func(context.Context, *runner.Runner, chan error, http.ResponseWriter, *http.Request)
@@ -154,11 +156,34 @@ func handleWait(ctx context.Context, r *runner.Runner, _ chan error, w http.Resp
}
}
-func Serve(ctx context.Context, listener net.Listener, r *runner.Runner, servicesErrCh chan error) error {
+type portHandler struct {
+ ports svclib.Ports
+}
+
+func (p portHandler) handle(ctx context.Context, r *runner.Runner, _ chan error, w http.ResponseWriter, req *http.Request) {
+ params := req.URL.Query()
+ service := params.Get("service")
+ if service == "" {
+ http.Error(w, "service parameter is required", http.StatusBadRequest)
+ return
+ }
+
+ port, ok := p.ports[service]
+ if !ok {
+ http.Error(w, "port is not autoassigned", http.StatusBadRequest)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(port))
+}
+
+func Serve(ctx context.Context, listener net.Listener, r *runner.Runner, ports svclib.Ports, servicesErrCh chan error) error {
mux := http.NewServeMux()
handle(ctx, mux, r, servicesErrCh, "GET /v0/healthcheck", handleHealthCheck)
handle(ctx, mux, r, servicesErrCh, "GET /v0/start", handleStart)
handle(ctx, mux, r, servicesErrCh, "GET /v0/kill", handleKill)
handle(ctx, mux, r, servicesErrCh, "GET /v0/wait", handleWait)
+ handle(ctx, mux, r, servicesErrCh, "GET /v0/port", portHandler{ports}.handle)
return http.Serve(listener, mux)
}
diff --git a/tests/so_reuseport/BUILD.bazel b/tests/so_reuseport/BUILD.bazel
index 8e839ff..ff207ae 100644
--- a/tests/so_reuseport/BUILD.bazel
+++ b/tests/so_reuseport/BUILD.bazel
@@ -1,6 +1,6 @@
load("@aspect_rules_js//js:defs.bzl", "js_test")
-load("@rules_itest//:itest.bzl", "itest_service", "service_test")
load("@rules_go//go:def.bzl", "go_test")
+load("@rules_itest//:itest.bzl", "itest_service", "service_test")
load("//:must_fail.bzl", "must_fail")
NOT_WINDOWS = select({
diff --git a/tests/svcctl/BUILD.bazel b/tests/svcctl/BUILD.bazel
index d1d7496..093bdad 100644
--- a/tests/svcctl/BUILD.bazel
+++ b/tests/svcctl/BUILD.bazel
@@ -1,5 +1,5 @@
-load("@rules_itest//:itest.bzl", "service_test")
load("@rules_go//go:def.bzl", "go_test")
+load("@rules_itest//:itest.bzl", "service_test")
go_test(
name = "_svcctl_test",
diff --git a/tests/svcctl/svcctl_test.go b/tests/svcctl/svcctl_test.go
index d0998c7..fd7c487 100644
--- a/tests/svcctl/svcctl_test.go
+++ b/tests/svcctl/svcctl_test.go
@@ -38,11 +38,26 @@ func TestSvcctl(t *testing.T) {
}
svcctlHost := "http://127.0.0.1:" + port
- // Kill speedy service with SIGTERM
+ //
params := url.Values{}
params.Add("service", "@@//:_speedy_service")
+ resp, err := http.Get(svcctlHost + "/v0/port?" + params.Encode())
+ if err != nil {
+ t.Errorf("Failed to get port for speedy service: %v", err)
+ }
+ speedyPort2, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Errorf("Failed to get port for speedy service: %v", err)
+ }
+ if string(speedyPort2) != speedyPort {
+ t.Errorf("Got port %s, want %s", string(speedyPort2), speedyPort)
+ }
+
+ // Kill speedy service with SIGTERM
+ params = url.Values{}
+ params.Add("service", "@@//:_speedy_service")
params.Add("signal", "SIGTERM")
- resp, err := http.Get(svcctlHost + "/v0/kill?" + params.Encode())
+ resp, err = http.Get(svcctlHost + "/v0/kill?" + params.Encode())
if err != nil {
t.Errorf("Failed to kill speedy service: %v", err)
}
diff --git a/tests/test_env/BUILD.bazel b/tests/test_env/BUILD.bazel
index ea18752..dee3c1e 100644
--- a/tests/test_env/BUILD.bazel
+++ b/tests/test_env/BUILD.bazel
@@ -1,5 +1,5 @@
load("@rules_go//go:def.bzl", "go_test")
-load("@rules_itest//:itest.bzl", "service_test", "itest_task")
+load("@rules_itest//:itest.bzl", "itest_task", "service_test")
env = {
"ITEST_ENV_VAR": "ITEST_ENV_VAR_VALUE",
@@ -30,7 +30,7 @@ service_test(
)
cc_binary(
- name = "_task_cc",
+ name = "_task_cc",
srcs = ["task.cc"],
env = env,
)
@@ -41,7 +41,7 @@ itest_task(
)
sh_binary(
- name = "_task_sh",
+ name = "_task_sh",
srcs = ["task.sh"],
env = env,
)