Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cmd/relayfile-cli/control_plane.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,15 @@ func handleControlPlaneHello(w http.ResponseWriter, r *http.Request) {
writeControlPlaneError(w, http.StatusMethodNotAllowed, controlPlaneErrInvalidArgument, "method not allowed")
return
}
// GET /v1/hello is the discovery endpoint. It must answer regardless of the
// caller's requested API version so newer clients can inspect an older
// daemon's supportedApiVersions and report (or repair) incompatibility.
if r.Method == http.MethodGet {
writeControlPlaneJSON(w, http.StatusOK, controlPlaneHello())
return
}
var requested uint32
if r.Method == http.MethodPost && r.Body != nil {
if r.Body != nil {
var req helloRequest
if err := decodeControlPlaneJSON(r, &req); err != nil {
writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, err.Error())
Expand Down
25 changes: 17 additions & 8 deletions cmd/relayfile-cli/control_plane_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,22 @@ func TestControlPlaneHelloVersionAndCLIVersion(t *testing.T) {
t.Fatalf("unexpected supported API versions: %#v", hello.SupportedAPIVersions)
}

var discovery helloResponse
status = controlPlaneJSONWithRequestedVersion(t, client, http.MethodGet, baseURL+"/v1/hello?apiVersion=4", nil, &discovery, "4")
if status != http.StatusOK {
t.Fatalf("discovery hello status = %d, want %d", status, http.StatusOK)
}
if discovery.DaemonVersion != hello.DaemonVersion || discovery.APIVersion != hello.APIVersion {
t.Fatalf("unexpected discovery hello response: %#v", discovery)
}

var errResp map[string]controlPlaneError
status = controlPlaneJSONWithoutVersion(t, client, http.MethodGet, baseURL+"/v1/hello?apiVersion=4", nil, &errResp)
status = controlPlaneJSONWithoutVersion(t, client, http.MethodGet, baseURL+"/v1/integrations/providers?apiVersion=4", nil, &errResp)
if status != http.StatusUpgradeRequired {
t.Fatalf("incompatible hello status = %d, want %d", status, http.StatusUpgradeRequired)
t.Fatalf("incompatible non-hello status = %d, want %d", status, http.StatusUpgradeRequired)
}
if errResp["error"].Code != controlPlaneErrVersionIncompatible {
t.Fatalf("unexpected incompatible error: %#v", errResp)
t.Fatalf("unexpected non-hello incompatible error: %#v", errResp)
}
}

Expand Down Expand Up @@ -456,15 +465,15 @@ func startControlPlaneTestServer(t *testing.T) (*http.Client, string, func()) {

func controlPlaneJSON(t *testing.T, client *http.Client, method, url string, body any, out any) int {
t.Helper()
return controlPlaneJSONWithVersionHeader(t, client, method, url, body, out, true)
return controlPlaneJSONWithRequestedVersion(t, client, method, url, body, out, "1")
}

func controlPlaneJSONWithoutVersion(t *testing.T, client *http.Client, method, url string, body any, out any) int {
t.Helper()
return controlPlaneJSONWithVersionHeader(t, client, method, url, body, out, false)
return controlPlaneJSONWithRequestedVersion(t, client, method, url, body, out, "")
}

func controlPlaneJSONWithVersionHeader(t *testing.T, client *http.Client, method, url string, body any, out any, sendVersion bool) int {
func controlPlaneJSONWithRequestedVersion(t *testing.T, client *http.Client, method, url string, body any, out any, version string) int {
t.Helper()
var reader *bytes.Reader
if body == nil {
Expand All @@ -480,8 +489,8 @@ func controlPlaneJSONWithVersionHeader(t *testing.T, client *http.Client, method
if err != nil {
t.Fatalf("new request failed: %v", err)
}
if sendVersion {
req.Header.Set("X-Relayfile-API-Version", "1")
if version != "" {
req.Header.Set("X-Relayfile-API-Version", version)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
Expand Down
20 changes: 7 additions & 13 deletions openapi/relayfile-control-plane-v1.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ info:
Versioned local control-plane contract for the relayfile daemon/CLI.
The service listens on a user-owned Unix domain socket
(`RELAYFILE_SOCK`, or `$XDG_RUNTIME_DIR/relayfile.sock`) and uses
`X-Relayfile-API-Version: 2` for client compatibility checks.
`X-Relayfile-API-Version: 3` for compatibility checks after discovery.
servers:
- url: http://relayfile.local
description: Logical HTTP origin over the relayfile Unix domain socket.
Expand All @@ -15,18 +15,19 @@ paths:
get:
operationId: hello
summary: Negotiate control-plane API compatibility
parameters:
- $ref: "#/components/parameters/ApiVersionHeader"
- $ref: "#/components/parameters/ApiVersionQuery"
description: |
Unversioned discovery endpoint. The daemon always returns its current
and supported API versions; any version header or query is ignored.
No version parameters are declared here on purpose: discovery must stay
callable by clients of any API version, including versions this contract
does not know about.
responses:
"200":
description: Daemon version and supported API versions
content:
application/json:
schema:
$ref: "#/components/schemas/HelloResponse"
"426":
$ref: "#/components/responses/VersionIncompatible"
post:
operationId: helloPost
summary: Negotiate control-plane API compatibility
Expand Down Expand Up @@ -300,13 +301,6 @@ components:
type: integer
format: uint32
const: 3
ApiVersionQuery:
name: apiVersion
in: query
required: false
schema:
type: integer
format: uint32
responses:
VersionIncompatible:
description: Client and daemon API versions are incompatible
Expand Down
150 changes: 134 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading