Skip to content

Commit d605a82

Browse files
authored
Minimal client API versioning (#2116)
* Add GET /api-version endpoint * Add version middleware This rewrites requests from `/vN/path` to `/path`. * Install version middleware on every service * Use wai-extra for rewriting WAI requests Simply changing the `pathInfo` field is not enough, because some Wai applications or middleware are relying on the `rawPathInfo` field. * Add version prefixes to nginz chart
1 parent ae447b1 commit d605a82

File tree

22 files changed

+307
-44
lines changed

22 files changed

+307
-44
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
For wire.com operators: to enable versioned API paths, make sure that nginz is deployed.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added minimal API version support: a list of supported API versions can be found at the endpoint `GET /api-version`. Versions can be selected by adding a prefix of the form `/vN` to every route, where `N` is the desired version number (so for example `/v1/conversations` to access version 1 of the `/conversations` endpoint).

charts/nginz/templates/conf/_nginx.conf.tpl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ http {
207207
}
208208

209209
{{ range $path := .Values.nginx_conf.disabled_paths }}
210-
location {{ $path }} {
210+
location ~* ^(/v[0-9]+)?{{ $path }} {
211211
212212
return 404;
213213
}
@@ -227,7 +227,10 @@ http {
227227
rewrite ^/api-docs{{ $location.path }} {{ $location.path }}/api-docs?base_url=https://{{ $.Values.nginx_conf.env }}-nginz-https.{{ $.Values.nginx_conf.external_env_domain }}/ break;
228228
{{- end }}
229229

230-
location {{ $location.path }} {
230+
{{- $versioned := ternary $location.versioned true (hasKey $location "versioned") -}}
231+
{{- $path := printf "%s%s" (ternary "(/v[0-9]+)?" "" $versioned) $location.path }}
232+
233+
location ~* ^{{ $path }} {
231234
232235
# remove access_token from logs, see 'Note sanitized_request' above.
233236
set $sanitized_request $request;

charts/nginz/values.yaml

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@ nginx_conf:
4343
# title: "Production"
4444
disabled_paths:
4545
- /conversations/last-events
46-
- ~* ^/conversations/([^/]*)/knock
47-
- ~* ^/conversations/([^/]*)/hot-knock
48-
- ~* ^/conversations/([^/]*)/messages
49-
- ~* ^/conversations/([^/]*)/client-messages
50-
- ~* ^/conversations/([^/]*)/events
51-
- ~* ^/conversations/([^/]*)/call
52-
- ~* ^/conversations/([^/]*)/call/state
46+
- /conversations/([^/]*)/knock
47+
- /conversations/([^/]*)/hot-knock
48+
- /conversations/([^/]*)/messages
49+
- /conversations/([^/]*)/client-messages
50+
- /conversations/([^/]*)/events
51+
- /conversations/([^/]*)/call
52+
- /conversations/([^/]*)/call/state
5353
- /search/top
5454
- /search/common
5555
# -- The origins from which we allow CORS requests. These are combined with 'external_env_domain' to form a full url
@@ -59,12 +59,12 @@ nginx_conf:
5959
- account
6060
upstreams:
6161
cargohold:
62-
- path: ~* ^/conversations/([^/]*)/assets
62+
- path: /conversations/([^/]*)/assets
6363
envs:
6464
- all
6565
max_body_size: "0"
6666
disable_request_buffering: true
67-
- path: ~* ^/conversations/([^/]*)/otr/assets
67+
- path: /conversations/([^/]*)/otr/assets
6868
envs:
6969
- all
7070
max_body_size: "0"
@@ -96,7 +96,7 @@ nginx_conf:
9696
- path: /list-users
9797
envs:
9898
- all
99-
- path: ~* ^/api/swagger.json$
99+
- path: /api/swagger.json$
100100
disable_zauth: true
101101
envs:
102102
- all
@@ -110,7 +110,7 @@ nginx_conf:
110110
- path: /connections
111111
envs:
112112
- all
113-
- path: ~* ^/list-connections$
113+
- path: /list-connections$
114114
envs:
115115
- all
116116
- path: /invitations
@@ -162,7 +162,7 @@ nginx_conf:
162162
- path: /bot/users
163163
envs:
164164
- all
165-
- path: ~* ^/conversations/([^/]*)/bots
165+
- path: /conversations/([^/]*)/bots
166166
envs:
167167
- all
168168
- path: /invitations/info
@@ -196,41 +196,49 @@ nginx_conf:
196196
- staging
197197
disable_zauth: true
198198
basic_auth: true
199+
versioned: false
199200
- path: /i/users/login-code
200201
envs:
201202
- staging
202203
disable_zauth: true
203204
basic_auth: true
205+
versioned: false
204206
- path: /i/users/invitation-code
205207
envs:
206208
- staging
207209
disable_zauth: true
208210
basic_auth: true
209-
- path: ~* ^/i/users/([^/]*)/rich-info
211+
versioned: false
212+
- path: /i/users/([^/]*)/rich-info
210213
envs:
211214
- staging
212215
disable_zauth: true
213216
basic_auth: true
214-
- path: ~* ^/i/teams/([^/]*)/suspend
217+
versioned: false
218+
- path: /i/teams/([^/]*)/suspend
215219
envs:
216220
- staging
217221
disable_zauth: true
218222
basic_auth: true
219-
- path: ~* ^/i/teams/([^/]*)/unsuspend
223+
versioned: false
224+
- path: /i/teams/([^/]*)/unsuspend
220225
envs:
221226
- staging
222227
disable_zauth: true
223228
basic_auth: true
229+
versioned: false
224230
- path: /i/provider/activation-code
225231
envs:
226232
- staging
227233
disable_zauth: true
228234
basic_auth: true
229-
- path: ~* ^/i/legalhold/whitelisted-teams(.*)
235+
versioned: false
236+
- path: /i/legalhold/whitelisted-teams(.*)
230237
envs:
231238
- staging
232239
disable_zauth: true
233240
basic_auth: true
241+
versioned: false
234242
- path: /cookies
235243
envs:
236244
- all
@@ -253,17 +261,17 @@ nginx_conf:
253261
- path: /search
254262
envs:
255263
- all
256-
- path: ~* ^/teams/([^/]*)/invitations(.*)
264+
- path: /teams/([^/]*)/invitations(.*)
257265
envs:
258266
- all
259-
- path: ~* ^/teams/([^/]*)/services(.*)
267+
- path: /teams/([^/]*)/services(.*)
260268
envs:
261269
- all
262-
- path: ~* ^/teams/invitations/info$
270+
- path: /teams/invitations/info$
263271
envs:
264272
- all
265273
disable_zauth: true
266-
- path: ~* ^/teams/invitations/by-email$
274+
- path: /teams/invitations/by-email$
267275
envs:
268276
- all
269277
disable_zauth: true
@@ -272,13 +280,14 @@ nginx_conf:
272280
- staging
273281
disable_zauth: true
274282
basic_auth: true
283+
versioned: false
275284
- path: /calls
276285
envs:
277286
- all
278-
- path: ~* ^/teams/([^/]*)/size$
287+
- path: /teams/([^/]*)/size$
279288
envs:
280289
- all
281-
- path: ~* ^/teams/([^/]*)/search$
290+
- path: /teams/([^/]*)/search$
282291
envs:
283292
- all
284293
- path: /verification-code/send
@@ -290,12 +299,12 @@ nginx_conf:
290299
disable_zauth: true
291300
envs:
292301
- all
293-
- path: ~* ^/conversations/([^/]*)/otr/messages
302+
- path: /conversations/([^/]*)/otr/messages
294303
envs:
295304
- all
296305
max_body_size: 40m
297306
body_buffer_size: 256k
298-
- path: ~* ^/conversations/([^/]*)/([^/]*)/proteus/messages
307+
- path: /conversations/([^/]*)/([^/]*)/proteus/messages
299308
envs:
300309
- all
301310
max_body_size: 40m
@@ -317,57 +326,60 @@ nginx_conf:
317326
envs:
318327
- all
319328
doc: true
320-
- path: ~* ^/teams$
329+
- path: /teams$
321330
envs:
322331
- all
323-
- path: ~* ^/teams/([^/]*)$
332+
- path: /teams/([^/]*)$
324333
envs:
325334
- all
326-
- path: ~* ^/teams/([^/]*)/members(.*)
335+
- path: /teams/([^/]*)/members(.*)
327336
envs:
328337
- all
329-
- path: ~* ^/teams/([^/]*)/get-members-by-ids-using-post(.*)
338+
- path: /teams/([^/]*)/get-members-by-ids-using-post(.*)
330339
envs:
331340
- all
332-
- path: ~* ^/teams/([^/]*)/conversations(.*)
341+
- path: /teams/([^/]*)/conversations(.*)
333342
envs:
334343
- all
335-
- path: ~* ^/teams/([^/]*)/members/csv$
344+
- path: /teams/([^/]*)/members/csv$
336345
envs:
337346
- all
338-
- path: ~* ^/teams/([^/]*)/legalhold(.*)
347+
- path: /teams/([^/]*)/legalhold(.*)
339348
envs:
340349
- all
341-
- path: ~* ^/i/teams/([^/]*)/legalhold(.*)
350+
- path: /i/teams/([^/]*)/legalhold(.*)
342351
envs:
343352
- staging
344353
disable_zauth: true
345354
basic_auth: true
346-
- path: ~* ^/custom-backend/by-domain/([^/]*)$
355+
versioned: false
356+
- path: /custom-backend/by-domain/([^/]*)$
347357
disable_zauth: true
348358
envs:
349359
- all
350-
- path: ~* ^/i/custom-backend/by-domain/([^/]*)$
360+
- path: /i/custom-backend/by-domain/([^/]*)$
351361
disable_zauth: true
352362
basic_auth: true
353363
envs:
354364
- staging
355-
- path: ~* ^/teams/api-docs
365+
versioned: false
366+
- path: /teams/api-docs
356367
envs:
357368
- all
358369
disable_zauth: true
359-
- path: ~* ^/teams/([^/]*)/features
370+
- path: /teams/([^/]*)/features
360371
envs:
361372
- all
362-
- path: ~* ^/teams/([^/]*)/features/([^/])*
373+
- path: /teams/([^/]*)/features/([^/])*
363374
envs:
364375
- all
365-
- path: ~* /i/teams/([^/]*)/features/([^/]*)
376+
- path: /i/teams/([^/]*)/features/([^/]*)
366377
envs:
367378
- staging
368379
disable_zauth: true
369380
basic_auth: true
370-
- path: ~* ^/feature-configs(.*)
381+
versioned: false
382+
- path: /feature-configs(.*)
371383
envs:
372384
- all
373385
- path: /galley-api/swagger-ui
@@ -395,6 +407,7 @@ nginx_conf:
395407
basic_auth: true
396408
envs:
397409
- staging
410+
versioned: false
398411
- path: /sso-initiate-bind
399412
envs:
400413
- all
@@ -436,7 +449,7 @@ nginx_conf:
436449
envs:
437450
- all
438451
disable_zauth: true
439-
- path: ~* ^/teams/([^/]*)/billing(.*)
452+
- path: /teams/([^/]*)/billing(.*)
440453
envs:
441454
- all
442455
calling-test:

libs/wire-api/package.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ library:
9090
- wire-message-proto-lens
9191
- x509
9292
- wai
93+
- wai-extra
94+
- wai-utilities
9395
- wai-websockets
9496
- websockets
9597

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
-- This file is part of the Wire Server implementation.
2+
--
3+
-- Copyright (C) 2022 Wire Swiss GmbH <[email protected]>
4+
--
5+
-- This program is free software: you can redistribute it and/or modify it under
6+
-- the terms of the GNU Affero General Public License as published by the Free
7+
-- Software Foundation, either version 3 of the License, or (at your option) any
8+
-- later version.
9+
--
10+
-- This program is distributed in the hope that it will be useful, but WITHOUT
11+
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
-- details.
14+
--
15+
-- You should have received a copy of the GNU Affero General Public License along
16+
-- with this program. If not, see <https://www.gnu.org/licenses/>.
17+
18+
module Wire.API.Routes.Version where
19+
20+
import Control.Lens ((?~))
21+
import Data.Aeson (FromJSON, ToJSON (..))
22+
import qualified Data.Aeson as Aeson
23+
import Data.Schema
24+
import qualified Data.Swagger as S
25+
import qualified Data.Text as Text
26+
import qualified Data.Text.Read as Text
27+
import Imports
28+
import Servant
29+
import Servant.Swagger
30+
import Wire.API.Routes.Named
31+
import Wire.API.VersionInfo
32+
33+
data Version = V0 | V1
34+
deriving stock (Eq, Ord, Bounded, Enum, Show)
35+
deriving (FromJSON, ToJSON) via (Schema Version)
36+
37+
instance ToSchema Version where
38+
schema =
39+
enum @Integer "Version" . mconcat $
40+
[ element 0 V0,
41+
element 1 V1
42+
]
43+
44+
readVersionNumber :: Text -> Maybe Integer
45+
readVersionNumber v = do
46+
('v', rest) <- Text.uncons v
47+
case Text.decimal rest of
48+
Right (n, "") -> pure n
49+
_ -> Nothing
50+
51+
mkVersion :: Integer -> Maybe Version
52+
mkVersion n = case Aeson.fromJSON (Aeson.Number (fromIntegral n)) of
53+
Aeson.Error _ -> Nothing
54+
Aeson.Success v -> pure v
55+
56+
supportedVersions :: [Version]
57+
supportedVersions = [minBound .. maxBound]
58+
59+
newtype VersionInfo = VersionInfo {vinfoSupported :: [Version]}
60+
deriving (FromJSON, ToJSON, S.ToSchema) via (Schema VersionInfo)
61+
62+
instance ToSchema VersionInfo where
63+
schema =
64+
(S.schema . S.example ?~ toJSON (VersionInfo supportedVersions))
65+
(VersionInfo <$> vinfoSupported .= vinfoSchema schema)
66+
67+
type VersionAPI =
68+
Named
69+
"get-version"
70+
( "api-version"
71+
:> Get '[JSON] VersionInfo
72+
)
73+
74+
versionAPI :: Applicative m => ServerT VersionAPI m
75+
versionAPI =
76+
Named @"get-version" $
77+
pure . VersionInfo $ supportedVersions
78+
79+
versionSwagger :: S.Swagger
80+
versionSwagger = toSwagger (Proxy @VersionAPI)

0 commit comments

Comments
 (0)