detached 81b458d

Merge pull request 'fix(host): call RunnerService under ermite's /-/api path' (#4) from fix/api-path into main

commit
81b458debe7c721dbbf6c3a36ca2049dae99b891
author
bnema · · 2026-10-12 22:25 UTC
merge of
16c0ee5 + 0642256 · diff against the first parent
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
5 files · +37 -6
M README.md +1 -1
@@ -91,7 +91,7 @@ the host process; the sandbox receives the workflow and its contexts only.
9191
9292 | Variable | Meaning |
9393 | --- | --- |
94-| `ERMITE_RUNNER_URL` | base URL of ermite |
94+| `ERMITE_RUNNER_URL` | base URL of ermite, as in a browser (the runner adds `/-/api` itself) |
9595 | `ERMITE_RUNNER_NAME` | runner name (default: the host name) |
9696 | `ERMITE_RUNNER_LABELS` | extra labels, comma separated (`ubuntu-latest,ubuntu-24.04`) |
9797 | `ERMITE_RUNNER_REGISTER_TOKEN` | token for `register` (see above) |
@@ -91,7 +91,7 @@ the host process; the sandbox receives the workflow and its contexts only.
91 91
92 | Variable | Meaning |92 | Variable | Meaning |
93 | --- | --- |93 | --- | --- |
94-| `ERMITE_RUNNER_URL` | base URL of ermite |94+| `ERMITE_RUNNER_URL` | base URL of ermite, as in a browser (the runner adds `/-/api` itself) |
95 | `ERMITE_RUNNER_NAME` | runner name (default: the host name) |95 | `ERMITE_RUNNER_NAME` | runner name (default: the host name) |
96 | `ERMITE_RUNNER_LABELS` | extra labels, comma separated (`ubuntu-latest,ubuntu-24.04`) |96 | `ERMITE_RUNNER_LABELS` | extra labels, comma separated (`ubuntu-latest,ubuntu-24.04`) |
97 | `ERMITE_RUNNER_REGISTER_TOKEN` | token for `register` (see above) |97 | `ERMITE_RUNNER_REGISTER_TOKEN` | token for `register` (see above) |
M internal/host/fake_test.go +1 -2
@@ -273,8 +273,7 @@ func (s *fakeServer) WatchJob(ctx context.Context, r *connect.Request[ermite.Wat
273273 // start serves the handler over HTTP/1.1 on a local port (Connect streams server messages over it) and returns its URL.
274274 func (s *fakeServer) start(t *testing.T) string {
275275 t.Helper()
276- _, h := ermiteconnect.NewRunnerServiceHandler(s)
277- srv := httptest.NewServer(h)
276+ srv := httptest.NewServer(hosttest.Mount(ermiteconnect.NewRunnerServiceHandler(s)))
278277 t.Cleanup(srv.Close)
279278 return srv.URL
280279 }
@@ -273,8 +273,7 @@ func (s *fakeServer) WatchJob(ctx context.Context, r *connect.Request[ermite.Wat
273 // start serves the handler over HTTP/1.1 on a local port (Connect streams server messages over it) and returns its URL.273 // start serves the handler over HTTP/1.1 on a local port (Connect streams server messages over it) and returns its URL.
274 func (s *fakeServer) start(t *testing.T) string {274 func (s *fakeServer) start(t *testing.T) string {
275 t.Helper()275 t.Helper()
276- _, h := ermiteconnect.NewRunnerServiceHandler(s)276+ srv := httptest.NewServer(hosttest.Mount(ermiteconnect.NewRunnerServiceHandler(s)))
277- srv := httptest.NewServer(h)
278 t.Cleanup(srv.Close)277 t.Cleanup(srv.Close)
279 return srv.URL278 return srv.URL
280 }279 }
M internal/host/hosttest/hosttest.go +9 -2
@@ -60,11 +60,18 @@ type jobState struct {
6060 complete *ermite.CompleteJobRequest
6161 }
6262
63+// Mount serves a RunnerService handler where ermite does: under /-/api, so
64+// that a client that forgets the prefix fails here as it would against ermite.
65+func Mount(path string, h http.Handler) http.Handler {
66+ mux := http.NewServeMux()
67+ mux.Handle("/-/api"+path, http.StripPrefix("/-/api", h))
68+ return mux
69+}
70+
6371 // New starts the server; Close stops it.
6472 func New() *Server {
6573 s := &Server{jobs: map[string]*jobState{}, assigned: map[string]*ermite.Job{}, done: make(chan string, 256)}
66- _, h := ermiteconnect.NewRunnerServiceHandler(s)
67- s.srv = httptest.NewServer(h)
74+ s.srv = httptest.NewServer(Mount(ermiteconnect.NewRunnerServiceHandler(s)))
6875 s.URL = s.srv.URL
6976 return s
7077 }
@@ -60,11 +60,18 @@ type jobState struct {
60 complete *ermite.CompleteJobRequest60 complete *ermite.CompleteJobRequest
61 }61 }
62 62
63+// Mount serves a RunnerService handler where ermite does: under /-/api, so
64+// that a client that forgets the prefix fails here as it would against ermite.
65+func Mount(path string, h http.Handler) http.Handler {
66+ mux := http.NewServeMux()
67+ mux.Handle("/-/api"+path, http.StripPrefix("/-/api", h))
68+ return mux
69+}
70+
63 // New starts the server; Close stops it.71 // New starts the server; Close stops it.
64 func New() *Server {72 func New() *Server {
65 s := &Server{jobs: map[string]*jobState{}, assigned: map[string]*ermite.Job{}, done: make(chan string, 256)}73 s := &Server{jobs: map[string]*jobState{}, assigned: map[string]*ermite.Job{}, done: make(chan string, 256)}
66- _, h := ermiteconnect.NewRunnerServiceHandler(s)74+ s.srv = httptest.NewServer(Mount(ermiteconnect.NewRunnerServiceHandler(s)))
67- s.srv = httptest.NewServer(h)
68 s.URL = s.srv.URL75 s.URL = s.srv.URL
69 return s76 return s
70 }77 }
M internal/host/rpc.go +5 -1
@@ -103,9 +103,13 @@ func newClient(baseURL, token string, base http.RoundTripper, insecure bool) erm
103103 Transport: bearer{token: token, base: base, allowInsecure: insecure},
104104 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
105105 }
106- return ermiteconnect.NewRunnerServiceClient(hc, strings.TrimRight(baseURL, "/"))
106+ return ermiteconnect.NewRunnerServiceClient(hc, strings.TrimRight(baseURL, "/")+APIPath)
107107 }
108108
109+// APIPath is where ermite serves its API under the base URL, as in ermite's own
110+// client (internal/adapters/client.APIPath there).
111+const APIPath = "/-/api"
112+
109113 // ClientFactory builds a client for a token; tests replace it.
110114 type ClientFactory func(token string) ermiteconnect.RunnerServiceClient
111115
@@ -103,9 +103,13 @@ func newClient(baseURL, token string, base http.RoundTripper, insecure bool) erm
103 Transport: bearer{token: token, base: base, allowInsecure: insecure},103 Transport: bearer{token: token, base: base, allowInsecure: insecure},
104 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },104 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
105 }105 }
106- return ermiteconnect.NewRunnerServiceClient(hc, strings.TrimRight(baseURL, "/"))106+ return ermiteconnect.NewRunnerServiceClient(hc, strings.TrimRight(baseURL, "/")+APIPath)
107 }107 }
108 108
109+// APIPath is where ermite serves its API under the base URL, as in ermite's own
110+// client (internal/adapters/client.APIPath there).
111+const APIPath = "/-/api"
112+
109 // ClientFactory builds a client for a token; tests replace it.113 // ClientFactory builds a client for a token; tests replace it.
110 type ClientFactory func(token string) ermiteconnect.RunnerServiceClient114 type ClientFactory func(token string) ermiteconnect.RunnerServiceClient
111 115
M internal/host/rpc_test.go +21 -0
@@ -93,6 +93,27 @@ func TestClientRefusesToSendTheTokenInClear(t *testing.T) {
9393 }
9494 }
9595
96+// The API lives under /-/api of the base URL, which may carry a path prefix
97+// and a trailing slash.
98+func TestClientCallsTheAPIPath(t *testing.T) {
99+ for base, want := range map[string]string{
100+ "https://h": "/-/api/ermite.RunnerService/FetchJob",
101+ "https://h/": "/-/api/ermite.RunnerService/FetchJob",
102+ "https://h/ermite": "/ermite/-/api/ermite.RunnerService/FetchJob",
103+ "https://h/ermite/": "/ermite/-/api/ermite.RunnerService/FetchJob",
104+ } {
105+ var got string
106+ rt := roundTripFunc(func(r *http.Request) (*http.Response, error) {
107+ got = r.URL.Path
108+ return nil, context.Canceled
109+ })
110+ _, _ = NewClient(base, "ermiterun_secret", rt).FetchJob(context.Background(), connect.NewRequest(&ermite.FetchJobRequest{}))
111+ if got != want {
112+ t.Errorf("%s: called %q, want %q", base, got, want)
113+ }
114+ }
115+}
116+
96117 type roundTripFunc func(*http.Request) (*http.Response, error)
97118
98119 func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
@@ -93,6 +93,27 @@ func TestClientRefusesToSendTheTokenInClear(t *testing.T) {
93 }93 }
94 }94 }
95 95
96+// The API lives under /-/api of the base URL, which may carry a path prefix
97+// and a trailing slash.
98+func TestClientCallsTheAPIPath(t *testing.T) {
99+ for base, want := range map[string]string{
100+ "https://h": "/-/api/ermite.RunnerService/FetchJob",
101+ "https://h/": "/-/api/ermite.RunnerService/FetchJob",
102+ "https://h/ermite": "/ermite/-/api/ermite.RunnerService/FetchJob",
103+ "https://h/ermite/": "/ermite/-/api/ermite.RunnerService/FetchJob",
104+ } {
105+ var got string
106+ rt := roundTripFunc(func(r *http.Request) (*http.Response, error) {
107+ got = r.URL.Path
108+ return nil, context.Canceled
109+ })
110+ _, _ = NewClient(base, "ermiterun_secret", rt).FetchJob(context.Background(), connect.NewRequest(&ermite.FetchJobRequest{}))
111+ if got != want {
112+ t.Errorf("%s: called %q, want %q", base, got, want)
113+ }
114+ }
115+}
116+
96 type roundTripFunc func(*http.Request) (*http.Response, error)117 type roundTripFunc func(*http.Request) (*http.Response, error)
97 118
98 func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }119 func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close