diff --git a/docs/v3/develop/settings-ref.mdx b/docs/v3/develop/settings-ref.mdx index 0ba217822056..8c3e274d6970 100644 --- a/docs/v3/develop/settings-ref.mdx +++ b/docs/v3/develop/settings-ref.mdx @@ -950,7 +950,7 @@ Controls maximum overflow of the connection pool. To prevent overflow, set to -1 ## ServerAPISettings Settings for controlling API server behavior ### `auth_string` -A string to use for basic authentication with the API; typically in the form 'user:password' but can be any string. +A string to use for basic authentication with the API in the form 'user:password'. **Type**: `string | None` @@ -985,6 +985,18 @@ The API's port address (defaults to `4200`). **Supported environment variables**: `PREFECT_SERVER_API_PORT` +### `base_path` +The base URL path to serve the API under. + +**Type**: `string | None` + +**Default**: `None` + +**TOML dotted key path**: `server.api.base_path` + +**Supported environment variables**: +`PREFECT_SERVER_API_BASE_PATH` + ### `default_limit` The default limit applied to queries that can return multiple objects, such as `POST /flow_runs/filter`. diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index d11dc71be268..c6970673ff43 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -834,7 +834,7 @@ } ], "default": null, - "description": "A string to use for basic authentication with the API; typically in the form 'user:password' but can be any string.", + "description": "A string to use for basic authentication with the API in the form 'user:password'.", "supported_environment_variables": [ "PREFECT_SERVER_API_AUTH_STRING" ], @@ -858,6 +858,25 @@ "title": "Port", "type": "integer" }, + "base_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The base URL path to serve the API under.", + "examples": [ + "/v2/api" + ], + "supported_environment_variables": [ + "PREFECT_SERVER_API_BASE_PATH" + ], + "title": "Base Path" + }, "default_limit": { "default": 200, "description": "The default limit applied to queries that can return multiple objects, such as `POST /flow_runs/filter`.", diff --git a/src/prefect/server/api/server.py b/src/prefect/server/api/server.py index 60777d40f214..e5b64d5272ce 100644 --- a/src/prefect/server/api/server.py +++ b/src/prefect/server/api/server.py @@ -55,6 +55,7 @@ PREFECT_DEBUG_MODE, PREFECT_MEMO_STORE_PATH, PREFECT_MEMOIZE_BLOCK_AUTO_REGISTRATION, + PREFECT_SERVER_API_BASE_PATH, PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS, PREFECT_UI_SERVE_BASE, get_current_settings, @@ -356,7 +357,10 @@ async def token_validation(request: Request, call_next: Any): # type: ignore[re header_token = request.headers.get("Authorization") # used for probes in k8s and such - if request.url.path in ["/api/health", "/api/ready"]: + if ( + request.url.path.endswith(("health", "ready")) + and request.method.upper() == "GET" + ): return await call_next(request) try: if header_token is None: @@ -691,7 +695,10 @@ async def metrics() -> Response: # type: ignore[reportUnusedFunction] name="static", ) app.api_app = api_app - app.mount("/api", app=api_app, name="api") + if PREFECT_SERVER_API_BASE_PATH: + app.mount(PREFECT_SERVER_API_BASE_PATH.value(), app=api_app, name="api") + else: + app.mount("/api", app=api_app, name="api") app.mount("/", app=ui_app, name="ui") def openapi(): diff --git a/src/prefect/settings/models/root.py b/src/prefect/settings/models/root.py index 945b1833ce14..ba66621b6de9 100644 --- a/src/prefect/settings/models/root.py +++ b/src/prefect/settings/models/root.py @@ -381,7 +381,11 @@ def _warn_on_misconfigured_api_url(settings: "Settings"): warnings_list.append(warning) parsed_url = urlparse(api_url) - if parsed_url.path and not parsed_url.path.startswith("/api"): + if ( + parsed_url.path + and "api.prefect.cloud" in api_url + and not parsed_url.path.startswith("/api") + ): warnings_list.append( "`PREFECT_API_URL` should have `/api` after the base URL." ) diff --git a/src/prefect/settings/models/server/api.py b/src/prefect/settings/models/server/api.py index be173565606c..0c458ef73210 100644 --- a/src/prefect/settings/models/server/api.py +++ b/src/prefect/settings/models/server/api.py @@ -18,7 +18,7 @@ class ServerAPISettings(PrefectBaseSettings): auth_string: Optional[SecretStr] = Field( default=None, - description="A string to use for basic authentication with the API; typically in the form 'user:password' but can be any string.", + description="A string to use for basic authentication with the API in the form 'user:password'.", ) host: str = Field( @@ -31,6 +31,12 @@ class ServerAPISettings(PrefectBaseSettings): description="The API's port address (defaults to `4200`).", ) + base_path: Optional[str] = Field( + default=None, + description="The base URL path to serve the API under.", + examples=["/v2/api"], + ) + default_limit: int = Field( default=200, description="The default limit applied to queries that can return multiple objects, such as `POST /flow_runs/filter`.", diff --git a/tests/test_settings.py b/tests/test_settings.py index 583ade3d83eb..c99427aa946f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -292,6 +292,7 @@ "PREFECT_RUNNER_SERVER_PORT": {"test_value": 8080}, "PREFECT_SERVER_ALLOW_EPHEMERAL_MODE": {"test_value": True, "legacy": True}, "PREFECT_SERVER_API_AUTH_STRING": {"test_value": "admin:admin"}, + "PREFECT_SERVER_API_BASE_PATH": {"test_value": "/v2/api"}, "PREFECT_SERVER_ANALYTICS_ENABLED": {"test_value": True}, "PREFECT_SERVER_API_CORS_ALLOWED_HEADERS": {"test_value": "foo"}, "PREFECT_SERVER_API_CORS_ALLOWED_METHODS": {"test_value": "foo"},