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, )