Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add HTTP registration endpoint for garden bootstrap

Add POST /api/v1/gardens/register endpoint so gardens can register via
HTTP before connecting the websocket, moving registration tokens off
the websocket path.

Server: new GardenController with OpenApiSpex operation, permission
check consistent with other API controllers, delegates to existing
register_new_garden/1.

Client: SowerClient.Registration HTTP client module, Garden.Socket
tries HTTP registration before falling back to websocket registration
token for backward compat.

sow-149

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+589 -1
+42 -1
apps/garden/lib/garden/socket.ex
··· 177 177 try_reauthenticate(storage) || registration_token() 178 178 179 179 _ -> 180 - registration_token() 180 + try_http_registration(storage) || registration_token() 181 181 end 182 182 end 183 183 ··· 215 215 end 216 216 217 217 defp try_reauthenticate(_), do: nil 218 + 219 + defp try_http_registration(storage) do 220 + config = Garden.Config.get() 221 + {public_key_pem, storage} = Garden.Auth.ensure_keypair(storage) 222 + 223 + req = 224 + Req.new( 225 + base_url: "#{config.endpoint}/api/v1", 226 + auth: {:bearer, config.access_token}, 227 + retry: false 228 + ) 229 + 230 + case SowerClient.Registration.register(req, config.name, public_key_pem) do 231 + {:ok, %{garden_sid: garden_sid, client_id: client_id}} -> 232 + Logger.info(msg: "Registered via HTTP", garden_sid: garden_sid, client_id: client_id) 233 + 234 + oauth_creds = %{client_id: client_id} 235 + storage = Map.merge(storage, %{garden_sid: garden_sid, oauth_credentials: oauth_creds}) 236 + 237 + case Garden.Auth.request_token(client_id, storage.private_key_pem) do 238 + {:ok, token_response} -> 239 + updated_creds = 240 + Map.merge(oauth_creds, %{ 241 + access_token: token_response.access_token, 242 + expires_in: token_response.expires_in, 243 + token_issued_at: System.system_time(:second) 244 + }) 245 + 246 + storage |> Map.put(:oauth_credentials, updated_creds) |> Storage.write() 247 + "boruta:#{token_response.access_token}" 248 + 249 + {:error, _} -> 250 + Storage.write(storage) 251 + nil 252 + end 253 + 254 + {:error, reason} -> 255 + Logger.debug(msg: "HTTP registration failed, falling back", reason: inspect(reason)) 256 + nil 257 + end 258 + end 218 259 219 260 defp registration_token do 220 261 Logger.info(msg: "Using registration token")
+1
apps/sower/lib/sower/orchestration.ex
··· 15 15 defdelegate delete_garden(garden), to: Garden 16 16 defdelegate get_garden!(id), to: Garden 17 17 defdelegate get_garden(hello, socket), to: Garden 18 + defdelegate register_new_garden(attrs), to: Garden 18 19 defdelegate get_garden_local_sid!(local_sid), to: Garden 19 20 defdelegate get_garden_local_sid(local_sid), to: Garden 20 21 defdelegate get_garden_sid!(sid), to: Garden
+69
apps/sower/lib/sower_web/controllers/api/garden_controller.ex
··· 1 + defmodule SowerWeb.Api.GardenController do 2 + use SowerWeb, :controller 3 + use OpenApiSpex.ControllerSpecs 4 + 5 + require Logger 6 + 7 + alias OpenApiSpex.Schema 8 + import Sower.Authorization 9 + 10 + plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true 11 + 12 + action_fallback SowerWeb.Api.FallbackController 13 + 14 + operation(:register, 15 + operation_id: "RegisterGarden", 16 + summary: "Register a new garden", 17 + request_body: 18 + {"Garden registration params", "application/json", SowerClient.GardenRegistration}, 19 + responses: %{ 20 + created: 21 + {"Garden registration response", "application/json", 22 + %Schema{ 23 + type: :object, 24 + properties: %{ 25 + sid: %Schema{type: :string, description: "Garden SID"}, 26 + oauth_credentials: SowerClient.Auth.OAuthCredentials 27 + }, 28 + required: [:sid, :oauth_credentials] 29 + }}, 30 + unauthorized: 31 + {"Unauthorized", "application/json", 32 + %Schema{type: :object, properties: %{error: %Schema{type: :string}}}}, 33 + unprocessable_entity: 34 + {"Validation error", "application/json", 35 + %Schema{type: :object, properties: %{error: %Schema{type: :string}}}} 36 + } 37 + ) 38 + 39 + def register( 40 + %Plug.Conn{ 41 + body_params: %SowerClient.GardenRegistration{ 42 + name: name, 43 + public_key: public_key 44 + } 45 + } = conn, 46 + _params 47 + ) do 48 + access_token = conn.assigns.access_token 49 + 50 + if can(access_token) 51 + |> create?(%Sower.Orchestration.Garden{org_id: access_token.org_id}) do 52 + case Sower.Orchestration.register_new_garden(%{name: name, public_key: public_key}) do 53 + {:ok, garden, %{client_id: client_id}} -> 54 + conn 55 + |> put_status(:created) 56 + |> render(:register, garden: garden, client_id: client_id) 57 + 58 + {:error, reason} -> 59 + Logger.error(msg: "Garden registration failed", error: inspect(reason)) 60 + 61 + conn 62 + |> put_status(:unprocessable_entity) 63 + |> render(:error, error: "registration failed") 64 + end 65 + else 66 + conn |> put_status(:unauthorized) |> render(:error, error: "unauthorized") 67 + end 68 + end 69 + end
+14
apps/sower/lib/sower_web/controllers/api/garden_json.ex
··· 1 + defmodule SowerWeb.Api.GardenJSON do 2 + def register(%{garden: garden, client_id: client_id}) do 3 + %{ 4 + sid: garden.sid, 5 + oauth_credentials: %{ 6 + client_id: client_id 7 + } 8 + } 9 + end 10 + 11 + def error(%{error: error}) do 12 + %{error: error} 13 + end 14 + end
+2
apps/sower/lib/sower_web/router.ex
··· 112 112 113 113 get "/auth/verify", AuthController, :verify 114 114 115 + post "/gardens/register", GardenController, :register 116 + 115 117 get "/nix/caches", Nix.CacheController, :list 116 118 get "/seeds", SeedController, :list 117 119 get "/seeds/latest", SeedController, :latest
+82
apps/sower/test/sower_web/controllers/api/garden_controller_test.exs
··· 1 + defmodule SowerWeb.Api.GardenControllerTest do 2 + use SowerWeb.ConnCase, async: true 3 + 4 + alias Sower.AccountsFixtures 5 + 6 + setup %{conn: conn} do 7 + user = AccountsFixtures.user_fixture() 8 + 9 + {:ok, access_token} = 10 + Sower.Accounts.AccessToken.create(%{ 11 + "description" => "test", 12 + "user_id" => user.id, 13 + "org_id" => user.org_id, 14 + "permissions" => [%{"role" => "garden:register"}] 15 + }) 16 + 17 + conn = 18 + conn 19 + |> put_req_header("authorization", "Bearer #{access_token.token}") 20 + |> put_req_header("content-type", "application/json") 21 + 22 + {_private_pem, public_pem} = 23 + JOSE.JWK.generate_key({:rsa, 2048}) 24 + |> then(fn jwk -> 25 + {_, priv} = JOSE.JWK.to_pem(jwk) 26 + {_, pub} = jwk |> JOSE.JWK.to_public() |> JOSE.JWK.to_pem() 27 + {priv, pub} 28 + end) 29 + 30 + %{conn: conn, user: user, public_pem: public_pem} 31 + end 32 + 33 + describe "POST /api/v1/gardens/register" do 34 + test "registers a new garden and returns sid + oauth credentials", %{ 35 + conn: conn, 36 + public_pem: public_pem 37 + } do 38 + conn = 39 + post(conn, ~p"/api/v1/gardens/register", %{ 40 + "name" => "test-garden", 41 + "public_key" => public_pem 42 + }) 43 + 44 + assert %{"sid" => sid, "oauth_credentials" => %{"client_id" => client_id}} = 45 + json_response(conn, 201) 46 + 47 + assert is_binary(sid) 48 + assert is_binary(client_id) 49 + end 50 + 51 + test "returns 401 when token lacks garden:register permission", %{ 52 + conn: conn, 53 + public_pem: public_pem 54 + } do 55 + user = AccountsFixtures.user_fixture() 56 + 57 + {:ok, read_only_token} = 58 + Sower.Accounts.AccessToken.create(%{ 59 + "description" => "read only", 60 + "user_id" => user.id, 61 + "org_id" => user.org_id, 62 + "permissions" => [%{"role" => "seed:read"}] 63 + }) 64 + 65 + conn = 66 + conn 67 + |> put_req_header("authorization", "Bearer #{read_only_token.token}") 68 + |> post(~p"/api/v1/gardens/register", %{ 69 + "name" => "test-garden", 70 + "public_key" => public_pem 71 + }) 72 + 73 + assert json_response(conn, 401) == %{"error" => "unauthorized"} 74 + end 75 + 76 + test "returns 422 when required fields are missing", %{conn: conn} do 77 + conn = post(conn, ~p"/api/v1/gardens/register", %{}) 78 + 79 + assert conn.status == 422 80 + end 81 + end 82 + end
+1
apps/sower_client/lib/sower_client.ex
··· 24 24 } 25 25 |> OpenApiSpex.add_schemas([ 26 26 SowerClient.GardenHello, 27 + SowerClient.GardenRegistration, 27 28 SowerClient.AgentHello, 28 29 SowerClient.Auth.OAuthCredentials, 29 30 SowerClient.Auth.TokenInfo,
+20
apps/sower_client/lib/sower_client/garden_registration.ex
··· 1 + defmodule SowerClient.GardenRegistration do 2 + use SowerClient.Schema 3 + 4 + OpenApiSpex.schema(%{ 5 + title: "GardenRegistration", 6 + description: "HTTP registration request for a new garden", 7 + type: :object, 8 + properties: %{ 9 + name: %Schema{ 10 + type: :string, 11 + description: "Name of garden" 12 + }, 13 + public_key: %Schema{ 14 + type: :string, 15 + description: "PEM-encoded RSA public key for private_key_jwt authentication" 16 + } 17 + }, 18 + required: [:name, :public_key] 19 + }) 20 + end
+35
apps/sower_client/lib/sower_client/registration.ex
··· 1 + defmodule SowerClient.Registration do 2 + require Logger 3 + 4 + def register(%Req.Request{} = req, name, public_key_pem) do 5 + case Req.post(req, 6 + url: "/gardens/register", 7 + json: %{ 8 + name: name, 9 + public_key: public_key_pem 10 + } 11 + ) do 12 + {:ok, %{status: 201, body: body}} -> 13 + {:ok, 14 + %{ 15 + garden_sid: body["sid"], 16 + client_id: body["oauth_credentials"]["client_id"] 17 + }} 18 + 19 + {:ok, %{status: 401}} -> 20 + {:error, :unauthorized} 21 + 22 + {:ok, %{body: %{"error" => error}}} -> 23 + {:error, error} 24 + 25 + {:ok, response} -> 26 + {:error, response} 27 + 28 + {:error, %Req.TransportError{reason: reason}} -> 29 + {:error, {:connection_error, reason}} 30 + 31 + {:error, _} = err -> 32 + err 33 + end 34 + end 35 + end
+193
client-go/client.gen.go
··· 29 29 Service SeedSeedType = "service" 30 30 ) 31 31 32 + // GardenRegistration HTTP registration request for a new garden 33 + type GardenRegistration struct { 34 + // Name Name of garden 35 + Name string `json:"name"` 36 + 37 + // PublicKey PEM-encoded RSA public key for private_key_jwt authentication 38 + PublicKey string `json:"public_key"` 39 + } 40 + 32 41 // NixCache A Nix binary cache 33 42 type NixCache struct { 34 43 // PublicKey Trusted public key for signed NARs ··· 41 50 Url *string `json:"url,omitempty"` 42 51 } 43 52 53 + // OAuthCredentials OAuth client registration returned to a garden after registration 54 + type OAuthCredentials struct { 55 + // ClientId Boruta OAuth client ID 56 + ClientId string `json:"client_id"` 57 + } 58 + 44 59 // Seed A seed is an installable unit 45 60 type Seed struct { 46 61 // Artifact Artifact of the seed ··· 112 127 // SeedType Seed type 113 128 SeedType *string `form:"seed_type,omitempty" json:"seed_type,omitempty"` 114 129 } 130 + 131 + // RegisterGardenJSONRequestBody defines body for RegisterGarden for application/json ContentType. 132 + type RegisterGardenJSONRequestBody = GardenRegistration 115 133 116 134 // NewSeedJSONRequestBody defines body for NewSeed for application/json ContentType. 117 135 type NewSeedJSONRequestBody = Seed ··· 192 210 // VerifyToken request 193 211 VerifyToken(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) 194 212 213 + // RegisterGardenWithBody request with any body 214 + RegisterGardenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) 215 + 216 + RegisterGarden(ctx context.Context, body RegisterGardenJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) 217 + 195 218 // ListNixCaches request 196 219 ListNixCaches(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) 197 220 ··· 222 245 return c.Client.Do(req) 223 246 } 224 247 248 + func (c *Client) RegisterGardenWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { 249 + req, err := NewRegisterGardenRequestWithBody(c.Server, contentType, body) 250 + if err != nil { 251 + return nil, err 252 + } 253 + req = req.WithContext(ctx) 254 + if err := c.applyEditors(ctx, req, reqEditors); err != nil { 255 + return nil, err 256 + } 257 + return c.Client.Do(req) 258 + } 259 + 260 + func (c *Client) RegisterGarden(ctx context.Context, body RegisterGardenJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { 261 + req, err := NewRegisterGardenRequest(c.Server, body) 262 + if err != nil { 263 + return nil, err 264 + } 265 + req = req.WithContext(ctx) 266 + if err := c.applyEditors(ctx, req, reqEditors); err != nil { 267 + return nil, err 268 + } 269 + return c.Client.Do(req) 270 + } 271 + 225 272 func (c *Client) ListNixCaches(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { 226 273 req, err := NewListNixCachesRequest(c.Server) 227 274 if err != nil { ··· 321 368 return req, nil 322 369 } 323 370 371 + // NewRegisterGardenRequest calls the generic RegisterGarden builder with application/json body 372 + func NewRegisterGardenRequest(server string, body RegisterGardenJSONRequestBody) (*http.Request, error) { 373 + var bodyReader io.Reader 374 + buf, err := json.Marshal(body) 375 + if err != nil { 376 + return nil, err 377 + } 378 + bodyReader = bytes.NewReader(buf) 379 + return NewRegisterGardenRequestWithBody(server, "application/json", bodyReader) 380 + } 381 + 382 + // NewRegisterGardenRequestWithBody generates requests for RegisterGarden with any type of body 383 + func NewRegisterGardenRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { 384 + var err error 385 + 386 + serverURL, err := url.Parse(server) 387 + if err != nil { 388 + return nil, err 389 + } 390 + 391 + operationPath := fmt.Sprintf("/api/v1/gardens/register") 392 + if operationPath[0] == '/' { 393 + operationPath = "." + operationPath 394 + } 395 + 396 + queryURL, err := serverURL.Parse(operationPath) 397 + if err != nil { 398 + return nil, err 399 + } 400 + 401 + req, err := http.NewRequest("POST", queryURL.String(), body) 402 + if err != nil { 403 + return nil, err 404 + } 405 + 406 + req.Header.Add("Content-Type", contentType) 407 + 408 + return req, nil 409 + } 410 + 324 411 // NewListNixCachesRequest generates requests for ListNixCaches 325 412 func NewListNixCachesRequest(server string) (*http.Request, error) { 326 413 var err error ··· 636 723 // VerifyTokenWithResponse request 637 724 VerifyTokenWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*VerifyTokenResponse, error) 638 725 726 + // RegisterGardenWithBodyWithResponse request with any body 727 + RegisterGardenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterGardenResponse, error) 728 + 729 + RegisterGardenWithResponse(ctx context.Context, body RegisterGardenJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterGardenResponse, error) 730 + 639 731 // ListNixCachesWithResponse request 640 732 ListNixCachesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListNixCachesResponse, error) 641 733 ··· 679 771 return 0 680 772 } 681 773 774 + type RegisterGardenResponse struct { 775 + Body []byte 776 + HTTPResponse *http.Response 777 + JSON201 *struct { 778 + // OauthCredentials OAuth client registration returned to a garden after registration 779 + OauthCredentials OAuthCredentials `json:"oauth_credentials"` 780 + 781 + // Sid Garden SID 782 + Sid string `json:"sid"` 783 + } 784 + JSON401 *struct { 785 + Error *string `json:"error,omitempty"` 786 + } 787 + JSON422 *struct { 788 + Error *string `json:"error,omitempty"` 789 + } 790 + } 791 + 792 + // Status returns HTTPResponse.Status 793 + func (r RegisterGardenResponse) Status() string { 794 + if r.HTTPResponse != nil { 795 + return r.HTTPResponse.Status 796 + } 797 + return http.StatusText(0) 798 + } 799 + 800 + // StatusCode returns HTTPResponse.StatusCode 801 + func (r RegisterGardenResponse) StatusCode() int { 802 + if r.HTTPResponse != nil { 803 + return r.HTTPResponse.StatusCode 804 + } 805 + return 0 806 + } 807 + 682 808 type ListNixCachesResponse struct { 683 809 Body []byte 684 810 HTTPResponse *http.Response ··· 825 951 return ParseVerifyTokenResponse(rsp) 826 952 } 827 953 954 + // RegisterGardenWithBodyWithResponse request with arbitrary body returning *RegisterGardenResponse 955 + func (c *ClientWithResponses) RegisterGardenWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RegisterGardenResponse, error) { 956 + rsp, err := c.RegisterGardenWithBody(ctx, contentType, body, reqEditors...) 957 + if err != nil { 958 + return nil, err 959 + } 960 + return ParseRegisterGardenResponse(rsp) 961 + } 962 + 963 + func (c *ClientWithResponses) RegisterGardenWithResponse(ctx context.Context, body RegisterGardenJSONRequestBody, reqEditors ...RequestEditorFn) (*RegisterGardenResponse, error) { 964 + rsp, err := c.RegisterGarden(ctx, body, reqEditors...) 965 + if err != nil { 966 + return nil, err 967 + } 968 + return ParseRegisterGardenResponse(rsp) 969 + } 970 + 828 971 // ListNixCachesWithResponse request returning *ListNixCachesResponse 829 972 func (c *ClientWithResponses) ListNixCachesWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListNixCachesResponse, error) { 830 973 rsp, err := c.ListNixCaches(ctx, reqEditors...) ··· 907 1050 return nil, err 908 1051 } 909 1052 response.JSON401 = &dest 1053 + 1054 + } 1055 + 1056 + return response, nil 1057 + } 1058 + 1059 + // ParseRegisterGardenResponse parses an HTTP response from a RegisterGardenWithResponse call 1060 + func ParseRegisterGardenResponse(rsp *http.Response) (*RegisterGardenResponse, error) { 1061 + bodyBytes, err := io.ReadAll(rsp.Body) 1062 + defer func() { _ = rsp.Body.Close() }() 1063 + if err != nil { 1064 + return nil, err 1065 + } 1066 + 1067 + response := &RegisterGardenResponse{ 1068 + Body: bodyBytes, 1069 + HTTPResponse: rsp, 1070 + } 1071 + 1072 + switch { 1073 + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: 1074 + var dest struct { 1075 + // OauthCredentials OAuth client registration returned to a garden after registration 1076 + OauthCredentials OAuthCredentials `json:"oauth_credentials"` 1077 + 1078 + // Sid Garden SID 1079 + Sid string `json:"sid"` 1080 + } 1081 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { 1082 + return nil, err 1083 + } 1084 + response.JSON201 = &dest 1085 + 1086 + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: 1087 + var dest struct { 1088 + Error *string `json:"error,omitempty"` 1089 + } 1090 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { 1091 + return nil, err 1092 + } 1093 + response.JSON401 = &dest 1094 + 1095 + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: 1096 + var dest struct { 1097 + Error *string `json:"error,omitempty"` 1098 + } 1099 + if err := json.Unmarshal(bodyBytes, &dest); err != nil { 1100 + return nil, err 1101 + } 1102 + response.JSON422 = &dest 910 1103 911 1104 } 912 1105
+130
openapi.json
··· 2 2 "components": { 3 3 "responses": {}, 4 4 "schemas": { 5 + "GardenRegistration": { 6 + "description": "HTTP registration request for a new garden", 7 + "properties": { 8 + "name": { 9 + "description": "Name of garden", 10 + "type": "string", 11 + "x-struct": null, 12 + "x-validate": null 13 + }, 14 + "public_key": { 15 + "description": "PEM-encoded RSA public key for private_key_jwt authentication", 16 + "type": "string", 17 + "x-struct": null, 18 + "x-validate": null 19 + } 20 + }, 21 + "required": [ 22 + "name", 23 + "public_key" 24 + ], 25 + "title": "GardenRegistration", 26 + "type": "object", 27 + "x-struct": "Elixir.SowerClient.GardenRegistration", 28 + "x-validate": null 29 + }, 5 30 "Nix Cache": { 6 31 "description": "A Nix binary cache", 7 32 "example": { ··· 37 62 "title": "Nix Cache", 38 63 "type": "object", 39 64 "x-struct": "Elixir.SowerClient.Nix.Cache", 65 + "x-validate": null 66 + }, 67 + "OAuthCredentials": { 68 + "description": "OAuth client registration returned to a garden after registration", 69 + "properties": { 70 + "client_id": { 71 + "description": "Boruta OAuth client ID", 72 + "type": "string", 73 + "x-struct": null, 74 + "x-validate": null 75 + } 76 + }, 77 + "required": [ 78 + "client_id" 79 + ], 80 + "title": "OAuthCredentials", 81 + "type": "object", 82 + "x-struct": "Elixir.SowerClient.Auth.OAuthCredentials", 40 83 "x-validate": null 41 84 }, 42 85 "Seed": { ··· 239 282 } 240 283 }, 241 284 "summary": "Verify access token", 285 + "tags": [] 286 + } 287 + }, 288 + "/api/v1/gardens/register": { 289 + "post": { 290 + "callbacks": {}, 291 + "operationId": "RegisterGarden", 292 + "parameters": [], 293 + "requestBody": { 294 + "content": { 295 + "application/json": { 296 + "schema": { 297 + "$ref": "#/components/schemas/GardenRegistration" 298 + } 299 + } 300 + }, 301 + "description": "Garden registration params", 302 + "required": false 303 + }, 304 + "responses": { 305 + "201": { 306 + "content": { 307 + "application/json": { 308 + "schema": { 309 + "properties": { 310 + "oauth_credentials": { 311 + "$ref": "#/components/schemas/OAuthCredentials" 312 + }, 313 + "sid": { 314 + "description": "Garden SID", 315 + "type": "string", 316 + "x-struct": null, 317 + "x-validate": null 318 + } 319 + }, 320 + "required": [ 321 + "sid", 322 + "oauth_credentials" 323 + ], 324 + "type": "object", 325 + "x-struct": null, 326 + "x-validate": null 327 + } 328 + } 329 + }, 330 + "description": "Garden registration response" 331 + }, 332 + "401": { 333 + "content": { 334 + "application/json": { 335 + "schema": { 336 + "properties": { 337 + "error": { 338 + "type": "string", 339 + "x-struct": null, 340 + "x-validate": null 341 + } 342 + }, 343 + "type": "object", 344 + "x-struct": null, 345 + "x-validate": null 346 + } 347 + } 348 + }, 349 + "description": "Unauthorized" 350 + }, 351 + "422": { 352 + "content": { 353 + "application/json": { 354 + "schema": { 355 + "properties": { 356 + "error": { 357 + "type": "string", 358 + "x-struct": null, 359 + "x-validate": null 360 + } 361 + }, 362 + "type": "object", 363 + "x-struct": null, 364 + "x-validate": null 365 + } 366 + } 367 + }, 368 + "description": "Validation error" 369 + } 370 + }, 371 + "summary": "Register a new garden", 242 372 "tags": [] 243 373 } 244 374 },