Skip to content

Commit

Permalink
Expose a method to get assigned ports for 'bazel run'
Browse files Browse the repository at this point in the history
  • Loading branch information
dzbarsky committed Aug 25, 2024
1 parent 15bcbc7 commit 60523eb
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 30 deletions.
5 changes: 5 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
Expand Down
12 changes: 10 additions & 2 deletions cmd/svcinit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os/exec"
"os/signal"
"slices"
"strconv"
"strings"
"syscall"
"text/tabwriter"
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 12 additions & 9 deletions docs/itest.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.


<a id="itest_service"></a>
Expand Down Expand Up @@ -64,8 +68,8 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
| <a id="itest_service-health_check_timeout"></a>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, <code>200ms</code>, <code>1s</code>, <code>2m</code>, <code>3h</code>, <code>4d</code>. If empty or not set, the health check will not have a timeout. | String | optional | <code>""</code> |
| <a id="itest_service-hot_reloadable"></a>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 | <code>False</code> |
| <a id="itest_service-http_health_check_address"></a>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, <code>$${@@//label/for:service:port_name}</code> can be used in the address. Example: <code>http_health_check_address = "http://127.0.0.1:$${@@//label/for:service:port_name}",</code> | String | optional | <code>""</code> |
| <a id="itest_service-named_ports"></a>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 <code>names_ports = ["http_port"]</code> will be assigned a fully-qualified name of <code>@@//label/for:service:http_port</code>.<br><br> Named ports are accessible through the service-port mapping. For more details, see <code>autoassign_port</code>. | List of strings | optional | <code>[]</code> |
| <a id="itest_service-so_reuseport_aware"></a>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.<br><br> Must only be set when autoassign_port is enabled. | Boolean | optional | <code>False</code> |
| <a id="itest_service-named_ports"></a>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 <code>named_ports = ["http_port"]</code> will be assigned a fully-qualified name of <code>@@//label/for:service:http_port</code>.<br><br> Named ports are accessible through the service-port mapping. For more details, see <code>autoassign_port</code>. | List of strings | optional | <code>[]</code> |
| <a id="itest_service-so_reuseport_aware"></a>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.<br><br> Must only be set when <code>autoassign_port</code> is enabled or <code>named_ports</code> are used. | Boolean | optional | <code>False</code> |


<a id="itest_service_group"></a>
Expand All @@ -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**

Expand All @@ -101,7 +104,7 @@ dev environments.
itest_task(<a href="#itest_task-name">name</a>, <a href="#itest_task-data">data</a>, <a href="#itest_task-deps">deps</a>, <a href="#itest_task-env">env</a>, <a href="#itest_task-exe">exe</a>)
</pre>

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`.
Expand Down
5 changes: 4 additions & 1 deletion gazelle/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
load("@gazelle//:def.bzl", "gazelle")

gazelle(name = "gazelle")
gazelle(
name = "gazelle",
visibility = ["//visibility:public"],
)
13 changes: 8 additions & 5 deletions private/itest.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion runner/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
5 changes: 2 additions & 3 deletions svcctl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
29 changes: 27 additions & 2 deletions svcctl/svcctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion tests/so_reuseport/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
2 changes: 1 addition & 1 deletion tests/svcctl/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
19 changes: 17 additions & 2 deletions tests/svcctl/svcctl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 3 additions & 3 deletions tests/test_env/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -30,7 +30,7 @@ service_test(
)

cc_binary(
name = "_task_cc",
name = "_task_cc",
srcs = ["task.cc"],
env = env,
)
Expand All @@ -41,7 +41,7 @@ itest_task(
)

sh_binary(
name = "_task_sh",
name = "_task_sh",
srcs = ["task.sh"],
env = env,
)
Expand Down

0 comments on commit 60523eb

Please sign in to comment.