Merge pull request 'fix(host): call RunnerService under ermite's /-/api path' (#4) from fix/api-path into main
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. | |||
| 91 | 91 | ||
| 92 | 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 | 95 | | `ERMITE_RUNNER_NAME` | runner name (default: the host name) | | |
| 96 | 96 | | `ERMITE_RUNNER_LABELS` | extra labels, comma separated (`ubuntu-latest,ubuntu-24.04`) | | |
| 97 | 97 | | `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 | |||
| 273 | 273 | // start serves the handler over HTTP/1.1 on a local port (Connect streams server messages over it) and returns its URL. | |
| 274 | 274 | func (s *fakeServer) start(t *testing.T) string { | |
| 275 | 275 | t.Helper() | |
| 276 | - | _, h := ermiteconnect.NewRunnerServiceHandler(s) | |
| 277 | - | srv := httptest.NewServer(h) | |
| 276 | + | srv := httptest.NewServer(hosttest.Mount(ermiteconnect.NewRunnerServiceHandler(s))) | |
| 278 | 277 | t.Cleanup(srv.Close) | |
| 279 | 278 | return srv.URL | |
| 280 | 279 | } | |
| @@ -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.URL | 278 | return srv.URL | ||
| 280 | } | 279 | } | ||
M internal/host/hosttest/hosttest.go +9 -2
| @@ -60,11 +60,18 @@ type jobState struct { | |||
| 60 | 60 | 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 | 71 | // New starts the server; Close stops it. | |
| 64 | 72 | func New() *Server { | |
| 65 | 73 | 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))) | |
| 68 | 75 | s.URL = s.srv.URL | |
| 69 | 76 | return s | |
| 70 | 77 | } | |
| @@ -60,11 +60,18 @@ type jobState struct { | |||||
| 60 | complete *ermite.CompleteJobRequest | 60 | 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.URL | 75 | s.URL = s.srv.URL | ||
| 69 | return s | 76 | 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 | |||
| 103 | 103 | Transport: bearer{token: token, base: base, allowInsecure: insecure}, | |
| 104 | 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 | 113 | // ClientFactory builds a client for a token; tests replace it. | |
| 110 | 114 | type ClientFactory func(token string) ermiteconnect.RunnerServiceClient | |
| 111 | 115 | ||
| @@ -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.RunnerServiceClient | 114 | 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) { | |||
| 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 | 117 | type roundTripFunc func(*http.Request) (*http.Response, error) | |
| 97 | 118 | ||
| 98 | 119 | 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) } | ||