diff --git a/changelog.d/2-features/one-swagger-page-per-internal-endpoint b/changelog.d/2-features/one-swagger-page-per-internal-endpoint new file mode 100644 index 0000000000..f89eb28750 --- /dev/null +++ b/changelog.d/2-features/one-swagger-page-per-internal-endpoint @@ -0,0 +1 @@ +Render one Swagger page per internal endpoint. This superseeds the previous Swagger docs page for all internal endpoints. diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 89b581ab1d..2427de7753 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -154,10 +154,6 @@ nginx_conf: disable_zauth: true envs: - staging - - path: /api-internal/swagger.json$ - disable_zauth: true - envs: - - all - path: /api-internal/swagger-ui disable_zauth: true envs: diff --git a/docs/src/understand/api-client-perspective/swagger.md b/docs/src/understand/api-client-perspective/swagger.md index 125299d430..a7f1b0fb64 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -23,19 +23,19 @@ docs. ### Public endpoints - Version `v0`: - - [new staging swagger page - **public** - endpoints](https://staging-nginz-https.zinfra.io/v0/api/swagger-ui/) + - [**public** + endpoints](https://staging-nginz-https.zinfra.io/v0/api/swagger-ui/) - Version `v1`: - - [new staging swagger page - **public** + - [**public** endpoints](https://staging-nginz-https.zinfra.io/v1/api/swagger-ui/) - Version `v2`: - - [new staging swagger page - **public** + - [**public** endpoints](https://staging-nginz-https.zinfra.io/v2/api/swagger-ui/) - Version `v3`: - - [new staging swagger page - **public** + - [**public** endpoints](https://staging-nginz-https.zinfra.io/v3/api/swagger-ui/) -- Unversioned - - [new staging swagger page - **public** +- Unversioned: + - [**public** endpoints](https://staging-nginz-https.zinfra.io/api/swagger-ui/) The first part of the URL's path is the version. No specified version means @@ -65,12 +65,37 @@ The URL to open in your browser for the development version `3` is `https://staging-nginz-https.zinfra.io/v3/api/swagger-ui/`. ### Internal endpoints + +Swagger docs for internal endpoints are served per service. I.e. there's one for +`brig`, one for `cannon`, etc.. This is because Swagger doesn't play well with +multiple actions having the same combination of HTTP method and URL path. + - Version `v3`: - - [new staging swagger page - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/) -- Unversioned - - [new staging swagger page - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/) + - [`brig` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/brig) + - [`cannon` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/cannon) + - [`cargohold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/cargohold) + - [`galley` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/galley) + - [`legalhold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/legalhold) + - [`spar` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/spar) +- Unversioned: + - [`brig` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig) + - [`cannon` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cannon) + - [`cargohold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cargohold) + - [`galley` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley) + - [`legalhold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/legalhold) + - [`spar` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/spar) The URL pattern is similar to that of public endpoints: `https:///v/api-internal/swagger-ui/`. No specified version diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index fbef99b7ba..0743610bb6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -58,7 +58,6 @@ import Wire.API.Routes.Internal.Brig.EJPD import qualified Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named -import Wire.API.SwaggerServant import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.Auth @@ -318,8 +317,7 @@ type GetVerificationCode = :> Get '[Servant.JSON] (Maybe Code.Value) type API = - SwaggerTag "brig" - :> "i" + "i" :> ( EJPD_API :<|> AccountAPI :<|> MLSAPI diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs index b8aca5109b..ff0fe916a1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs @@ -12,11 +12,9 @@ import Wire.API.Internal.BulkPush import Wire.API.RawJson import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named -import Wire.API.SwaggerServant type API = - SwaggerTag "cannon" - :> "i" + "i" :> ( Named "get-status" ( "status" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index de11695246..825623ac9c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -23,11 +23,9 @@ import Imports import Servant import Servant.Swagger import Wire.API.Routes.MultiVerb -import Wire.API.SwaggerServant type InternalAPI = - SwaggerTag "cargohold" - :> "i" + "i" :> "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index 1f9b9ba9ef..20008dac42 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -40,7 +40,6 @@ import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Galley.Feature -import Wire.API.SwaggerServant import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member @@ -166,7 +165,7 @@ type IFeatureAPI = :> Get '[Servant.JSON] AllFeatureConfigs ) -type InternalAPI = SwaggerTag "galley" :> "i" :> InternalAPIBase +type InternalAPI = "i" :> InternalAPIBase type InternalAPIBase = Named diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index 785313aca0..69d114dca8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -24,12 +24,10 @@ import Data.Swagger import Imports import Servant.API hiding (Header, WithStatus) import Servant.Swagger -import Wire.API.SwaggerServant import Wire.API.Team.Feature type InternalLegalHoldAPI = - SwaggerTag "legalhold" - :> "i" + "i" :> "teams" :> ( Capture "tid" TeamId :> "legalhold" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index b9a76a4b94..63f2358f5e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -23,13 +23,11 @@ import Data.Swagger import Imports import Servant import Servant.Swagger -import Wire.API.SwaggerServant import Wire.API.User import Wire.API.User.Saml type InternalAPI = - SwaggerTag "spar" - :> "i" + "i" :> ( "status" :> Get '[JSON] NoContent :<|> "teams" :> Capture "team" TeamId :> DeleteNoContent :<|> "sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent diff --git a/libs/wire-api/src/Wire/API/SwaggerServant.hs b/libs/wire-api/src/Wire/API/SwaggerServant.hs index ef7adb1c79..89973fb59a 100644 --- a/libs/wire-api/src/Wire/API/SwaggerServant.hs +++ b/libs/wire-api/src/Wire/API/SwaggerServant.hs @@ -17,88 +17,16 @@ -- | Servant combinators related to Swagger docs module Wire.API.SwaggerServant - ( SwaggerTag, - OmitDocs, + ( OmitDocs, ) where -import Control.Lens -import qualified Data.HashMap.Strict.InsOrd as InsOrdMap -import qualified Data.HashSet.InsOrd as InsOrdSet import Data.Metrics.Servant import Data.Proxy -import Data.Swagger (allOperations) -import qualified Data.Swagger as S -import Data.Text as T -import GHC.TypeLits import Imports hiding (head) import Servant -import Servant.Client.Core import Servant.Swagger (HasSwagger (toSwagger)) --- | Add tag @tag@ to endpoints --- --- @tag@ is a type level string. In swagger terms this combinator adds a --- `Data.Swagger.Internal.TagName` to all `Data.Swagger.Internal.Operation`s --- it's applied to. Tags are rendered as sections in swagger doc. --- --- @ --- SwaggerTag "cannon" --- :> "i" --- :> ( Named --- "get-status" --- ( "status" --- :> MultiVerb --- 'GET --- '[PlainText] --- '[RespondEmpty 200 "Service is alive."] --- () --- ) --- @ -data SwaggerTag tag - --- | Adjust `Swagger` according to its `SwaggerTag` --- --- Unfortunately, paths are stored as keys in a `InsOrdMap.InsOrdHashMap`. --- Because those have to be unique, prefix them with the @tag@ (service name.) --- Otherwise, the `Monoid` instance of `Swagger.Swagger` would throw duplicated --- paths away. The @tag@ is also used as a `Swagger.TagName` for all --- `Swagger.Operations`. -instance (HasSwagger b, KnownSymbol tag) => HasSwagger (SwaggerTag tag :> b) where - toSwagger _ = tagSwagger $ toSwagger (Proxy :: Proxy b) - where - tagString :: String - tagString = symbolVal (Proxy @tag) - - tagSwagger :: S.Swagger -> S.Swagger - tagSwagger s = - s - & S.paths .~ tagPaths s - & allOperations . S.tags <>~ tag - - tag :: InsOrdSet.InsOrdHashSet S.TagName - tag = InsOrdSet.singleton @S.TagName (T.pack tagString) - - tagPaths :: S.Swagger -> InsOrdMap.InsOrdHashMap FilePath S.PathItem - tagPaths s = - let m = s ^. S.paths - in InsOrdMap.mapKeys (\k -> "/<" ++ tagString ++ ">" ++ k) m - -instance HasServer api ctx => HasServer (SwaggerTag tag :> api) ctx where - type ServerT (SwaggerTag tag :> api) m = ServerT api m - - route _ = route (Proxy :: Proxy api) - hoistServerWithContext _ pc nt s = - hoistServerWithContext (Proxy :: Proxy api) pc nt s - -instance RoutesToPaths api => RoutesToPaths (SwaggerTag tag :> api) where - getRoutes = getRoutes @api - -instance HasClient m api => HasClient m (SwaggerTag tag :> api) where - type Client m (SwaggerTag tag :> api) = Client m api - clientWithRoute proxyM _ = clientWithRoute proxyM (Proxy @api) - hoistClientMonad proxyM _ = hoistClientMonad proxyM (Proxy @api) - -- | A type-level tag that lets us omit any branch from Swagger docs. -- -- FUTUREWORK(fisx): this is currently only used for the spar internal api diff --git a/services/brig/docs/swagger-internal-endpoints.md b/services/brig/docs/swagger-internal-endpoints.md deleted file mode 100644 index dfa7fe8bcf..0000000000 --- a/services/brig/docs/swagger-internal-endpoints.md +++ /dev/null @@ -1,12 +0,0 @@ -These Swagger docs document the internal API of `wire-server`. I.e. the -endpoints are only reachable inside the Wire cluster, usually used for -communication between services, between services and test executables, or -between services and site operators for forensics. - -Request execution does not work as Swagger expects a single target host whereas -these endpoints are served by multiple hosts. - -Displayed paths are prefixed by the service name (e.g. `/`). This is -necessary because Swagger does not expect more than one endpoint for a given -path and method. Though, some paths (e.g. `/i/status`) exist on more than one -service. To use the a path, remove the prefix. diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 4702fb8dcd..c3cf258f7a 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -94,6 +94,7 @@ import qualified Data.ZAuth.Token as ZAuth import FileEmbedLzma import Galley.Types.Teams (HiddenPerm (..), hasPermission) import Imports hiding (head) +import Network.Socket (PortNumber) import Network.Wai.Routing import Network.Wai.Utilities as Utilities import Polysemy @@ -112,7 +113,6 @@ import qualified Wire.API.Routes.Internal.Brig as BrigInternalAPI import qualified Wire.API.Routes.Internal.Cannon as CannonInternalAPI import qualified Wire.API.Routes.Internal.Cargohold as CargoholdInternalAPI import qualified Wire.API.Routes.Internal.Galley as GalleyInternalAPI -import qualified Wire.API.Routes.Internal.LegalHold as LegalHoldInternalAPI import qualified Wire.API.Routes.Internal.Spar as SparInternalAPI import qualified Wire.API.Routes.MultiTablePaging as Public import Wire.API.Routes.Named (Named (Named)) @@ -147,7 +147,14 @@ import Wire.Sem.Now (Now) -- User API ----------------------------------------------------------- docsAPI :: Servant.Server DocsAPI -docsAPI = versionedSwaggerDocsAPI :<|> pure eventNotificationSchemas :<|> internalEndpointsSwaggerDocsAPI +docsAPI = + versionedSwaggerDocsAPI + :<|> pure eventNotificationSchemas + :<|> internalEndpointsSwaggerDocsAPI "brig" 9082 BrigInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "cannon" 9093 CannonInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "cargohold" 9094 CargoholdInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "galley" 9095 GalleyInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "spar" 9098 SparInternalAPI.swaggerDoc -- | Serves Swagger docs for public endpoints -- @@ -178,31 +185,21 @@ versionedSwaggerDocsAPI Nothing = versionedSwaggerDocsAPI (Just maxBound) -- empty. It would have been too tedious to create them. Please add -- pre-generated docs on version increase as it's done in -- `versionedSwaggerDocsAPI`. -internalEndpointsSwaggerDocsAPI :: Servant.Server InternalEndpointsSwaggerDocsAPI -internalEndpointsSwaggerDocsAPI (Just V3) = +internalEndpointsSwaggerDocsAPI :: + String -> + PortNumber -> + S.Swagger -> + Servant.Server (VersionedSwaggerDocsAPIBase service) +internalEndpointsSwaggerDocsAPI service examplePort swagger (Just V3) = swaggerSchemaUIServer $ - ( BrigInternalAPI.swaggerDoc - <> CannonInternalAPI.swaggerDoc - <> CargoholdInternalAPI.swaggerDoc - <> LegalHoldInternalAPI.swaggerDoc - <> GalleyInternalAPI.swaggerDoc - <> SparInternalAPI.swaggerDoc - ) - & S.info . S.title .~ "Wire-Server internal API" - & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger-internal-endpoints.md") + swagger + & adjustSwaggerForInternalEndpoint service examplePort & cleanupSwagger -internalEndpointsSwaggerDocsAPI (Just V0) = emptySwagger -internalEndpointsSwaggerDocsAPI (Just V1) = emptySwagger -internalEndpointsSwaggerDocsAPI (Just V2) = emptySwagger -internalEndpointsSwaggerDocsAPI Nothing = internalEndpointsSwaggerDocsAPI (Just maxBound) - -emptySwagger :: Servant.Server VersionedSwaggerDocsAPIBase -emptySwagger = - swaggerSchemaUIServer $ - mempty @S.Swagger - & S.info . S.title .~ "Wire-Server internal API" - & S.info . S.description - ?~ "There is no Swagger documentation for this version. Please refer to v3 or later." +internalEndpointsSwaggerDocsAPI _ _ _ (Just V0) = emptySwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just V1) = emptySwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just V2) = emptySwagger +internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = + internalEndpointsSwaggerDocsAPI service examplePort swagger (Just maxBound) servantSitemap :: forall r p. diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index e894b6f315..0e75164817 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -2,10 +2,14 @@ module Brig.API.Public.Swagger ( VersionedSwaggerDocsAPI, InternalEndpointsSwaggerDocsAPI, VersionedSwaggerDocsAPIBase, + SwaggerDocsAPIBase, + ServiceSwaggerDocsAPIBase, DocsAPI, pregenSwagger, swaggerPregenUIServer, eventNotificationSchemas, + adjustSwaggerForInternalEndpoint, + emptySwagger, ) where @@ -13,12 +17,15 @@ import Control.Lens import qualified Data.Aeson as A import Data.FileEmbed import qualified Data.HashMap.Strict.InsOrd as HM +import qualified Data.HashSet.InsOrd as InsOrdSet import qualified Data.Swagger as S import qualified Data.Swagger.Declare as S import qualified Data.Text as T import FileEmbedLzma +import GHC.TypeLits import Imports hiding (head) import Language.Haskell.TH +import Network.Socket import Servant import Servant.Swagger.Internal.Orphans () import Servant.Swagger.UI @@ -27,14 +34,23 @@ import qualified Wire.API.Event.FeatureConfig import qualified Wire.API.Event.Team import Wire.API.Routes.Version -type VersionedSwaggerDocsAPIBase = SwaggerSchemaUI "swagger-ui" "swagger.json" +type SwaggerDocsAPIBase = SwaggerSchemaUI "swagger-ui" "swagger.json" -type VersionedSwaggerDocsAPI = "api" :> Header VersionHeader Version :> VersionedSwaggerDocsAPIBase +type VersionedSwaggerDocsAPI = "api" :> Header VersionHeader Version :> SwaggerDocsAPIBase + +type ServiceSwaggerDocsAPIBase service = SwaggerSchemaUI service (AppendSymbol service "-swagger.json") + +type VersionedSwaggerDocsAPIBase service = Header VersionHeader Version :> ServiceSwaggerDocsAPIBase service type InternalEndpointsSwaggerDocsAPI = "api-internal" - :> Header VersionHeader Version - :> VersionedSwaggerDocsAPIBase + :> "swagger-ui" + :> ( VersionedSwaggerDocsAPIBase "brig" + :<|> VersionedSwaggerDocsAPIBase "cannon" + :<|> VersionedSwaggerDocsAPIBase "cargohold" + :<|> VersionedSwaggerDocsAPIBase "galley" + :<|> VersionedSwaggerDocsAPIBase "spar" + ) type NotificationSchemasAPI = "api" :> "event-notification-schemas" :> Get '[JSON] [S.Definitions S.Schema] @@ -46,12 +62,50 @@ pregenSwagger v = =<< makeRelativeToProject ("docs/swagger-v" <> T.unpack (toUrlPiece v) <> ".json") -swaggerPregenUIServer :: LByteString -> Server VersionedSwaggerDocsAPIBase +swaggerPregenUIServer :: LByteString -> Server SwaggerDocsAPIBase swaggerPregenUIServer = swaggerSchemaUIServer . fromMaybe A.Null . A.decode +adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.Swagger -> S.Swagger +adjustSwaggerForInternalEndpoint service examplePort swagger = + swagger + & S.info . S.title .~ T.pack ("Wire-Server internal API (" ++ service ++ ")") + & S.info . S.description ?~ renderedDescription + & S.host ?~ S.Host "localhost" (Just examplePort) + & S.allOperations . S.tags <>~ tag + -- Enforce HTTP as the services themselves don't understand HTTPS + & S.schemes ?~ [S.Http] + & S.allOperations . S.schemes ?~ [S.Http] + where + tag :: InsOrdSet.InsOrdHashSet S.TagName + tag = InsOrdSet.singleton @S.TagName (T.pack service) + + renderedDescription :: Text + renderedDescription = + T.pack . Imports.unlines $ + [ "To have access to this *internal* endpoint, create a port forwarding to `" + ++ service + ++ "` into the Kubernetes cluster. E.g.:", + "```", + "kubectl port-forward -n wire service/" + ++ service + ++ " " + ++ show examplePort + ++ ":8080", + "```", + "**N.B.:** Execution via this UI won't work due to CORS issues." + ++ " But, the proposed `curl` commands will." + ] + +emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) +emptySwagger = + swaggerSchemaUIServer $ + mempty @S.Swagger + & S.info . S.description + ?~ "There is no Swagger documentation for this version. Please refer to v3 or later." + {- FUTUREWORK(fisx): there are a few things that need to be fixed before this schema collection is of any practical use!