Skip to content

Commit

Permalink
AIP-84 Get Configs / Get Config Value (#43841)
Browse files Browse the repository at this point in the history
* AIP-84 Get Config

* Fix section param type and text format

* Add test for Get Config

* Consolidate redundant for check expose config

* Add `Accept` header depends for Json and Text

* Refactor config, dag_source with common header

* Refactor test_config with non-sensitive-only case

* Fix OpenAPI schema for Headers, Get Config

* Refactor test_config

- add `HEADERS_NONE` and `HEADERS_ANY` cases
- modularize common json response cases
- add `_validate_response` common method

* Fix style by ruff format
  • Loading branch information
jason810496 authored Nov 20, 2024
1 parent aa7a3b2 commit 3b8f834
Show file tree
Hide file tree
Showing 17 changed files with 1,468 additions and 20 deletions.
3 changes: 3 additions & 0 deletions airflow/api_connexion/endpoints/config_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from airflow.api_connexion.schemas.config_schema import Config, ConfigOption, ConfigSection, config_schema
from airflow.configuration import conf
from airflow.settings import json
from airflow.utils.api_migration import mark_fastapi_migration_done

LINE_SEP = "\n" # `\n` cannot appear in f-strings

Expand Down Expand Up @@ -65,6 +66,7 @@ def _config_to_json(config: Config) -> str:
return json.dumps(config_schema.dump(config), indent=4)


@mark_fastapi_migration_done
@security.requires_access_configuration("GET")
def get_config(*, section: str | None = None) -> Response:
"""Get current configuration."""
Expand Down Expand Up @@ -102,6 +104,7 @@ def get_config(*, section: str | None = None) -> Response:
)


@mark_fastapi_migration_done
@security.requires_access_configuration("GET")
def get_value(*, section: str, option: str) -> Response:
serializer = {
Expand Down
48 changes: 48 additions & 0 deletions airflow/api_fastapi/common/headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from fastapi import Depends, Header, HTTPException, status
from typing_extensions import Annotated

from airflow.api_fastapi.common.types import Mimetype


def header_accept_json_or_text_depends(
accept: Annotated[
str,
Header(
json_schema_extra={
"type": "string",
"enum": [Mimetype.JSON, Mimetype.TEXT, Mimetype.ANY],
}
),
] = Mimetype.ANY,
) -> Mimetype:
if accept.startswith(Mimetype.ANY):
return Mimetype.ANY
if accept.startswith(Mimetype.JSON):
return Mimetype.JSON
if accept.startswith(Mimetype.TEXT):
return Mimetype.TEXT
raise HTTPException(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
detail="Only application/json or text/plain is supported",
)


HeaderAcceptJsonOrText = Annotated[Mimetype, Depends(header_accept_json_or_text_depends)]
9 changes: 9 additions & 0 deletions airflow/api_fastapi/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from __future__ import annotations

from datetime import timedelta
from enum import Enum
from typing import Annotated

from pydantic import AfterValidator, AliasGenerator, AwareDatetime, BaseModel, BeforeValidator, ConfigDict
Expand Down Expand Up @@ -56,3 +57,11 @@ class TimeDelta(BaseModel):


TimeDeltaWithValidation = Annotated[TimeDelta, BeforeValidator(_validate_timedelta_field)]


class Mimetype(str, Enum):
"""Mimetype for the `Content-Type` header."""

TEXT = "text/plain"
JSON = "application/json"
ANY = "*/*"
64 changes: 64 additions & 0 deletions airflow/api_fastapi/core_api/datamodels/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

from pydantic import BaseModel


class ConfigOption(BaseModel):
"""Config option."""

key: str
value: str | tuple[str, str]

@property
def text_format(self):
if isinstance(self.value, tuple):
return f"{self.key} = {self.value[0]} {self.value[1]}"
return f"{self.key} = {self.value}"


class ConfigSection(BaseModel):
"""Config Section Schema."""

name: str
options: list[ConfigOption]

@property
def text_format(self):
"""
Convert the config section to text format.
Example:
```
[section_name]
key1 = value1
key2 = value2
```
"""
return f"[{self.name}]\n" + "\n".join(option.text_format for option in self.options) + "\n"


class Config(BaseModel):
"""List of config sections with their options."""

sections: list[ConfigSection]

@property
def text_format(self):
# convert all config sections to text
return "\n".join(section.text_format for section in self.sections)
210 changes: 210 additions & 0 deletions airflow/api_fastapi/core_api/openapi/v1-generated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,10 @@ paths:
required: false
schema:
type: string
enum:
- application/json
- text/plain
- '*/*'
default: '*/*'
title: Accept
responses:
Expand Down Expand Up @@ -1785,6 +1789,163 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/public/config/:
get:
tags:
- Config
summary: Get Config
operationId: get_config
parameters:
- name: section
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Section
- name: accept
in: header
required: false
schema:
type: string
enum:
- application/json
- text/plain
- '*/*'
default: '*/*'
title: Accept
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Config'
text/plain:
schema:
type: string
example: '[core]
dags_folder = /opt/airflow/dags
base_log_folder = /opt/airflow/logs
[smtp]
smtp_host = localhost
smtp_mail_from = airflow@example.com
'
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Unauthorized
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Forbidden
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Not Found
'406':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Not Acceptable
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/public/config/section/{section}/option/{option}:
get:
tags:
- Config
summary: Get Config Value
operationId: get_config_value
parameters:
- name: section
in: path
required: true
schema:
type: string
title: Section
- name: option
in: path
required: true
schema:
type: string
title: Option
- name: accept
in: header
required: false
schema:
type: string
enum:
- application/json
- text/plain
- '*/*'
default: '*/*'
title: Accept
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Config'
text/plain:
schema:
type: string
example: '[core]
dags_folder = /opt/airflow/dags
base_log_folder = /opt/airflow/logs
'
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Unauthorized
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Forbidden
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Not Found
'406':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Not Acceptable
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/public/dagWarnings:
get:
tags:
Expand Down Expand Up @@ -4593,6 +4754,55 @@ components:
- status
title: BaseInfoSchema
description: Base status field for metadatabase and scheduler.
Config:
properties:
sections:
items:
$ref: '#/components/schemas/ConfigSection'
type: array
title: Sections
type: object
required:
- sections
title: Config
description: List of config sections with their options.
ConfigOption:
properties:
key:
type: string
title: Key
value:
anyOf:
- type: string
- prefixItems:
- type: string
- type: string
type: array
maxItems: 2
minItems: 2
title: Value
type: object
required:
- key
- value
title: ConfigOption
description: Config option.
ConfigSection:
properties:
name:
type: string
title: Name
options:
items:
$ref: '#/components/schemas/ConfigOption'
type: array
title: Options
type: object
required:
- name
- options
title: ConfigSection
description: Config Section Schema.
ConnectionBody:
properties:
connection_id:
Expand Down
Loading

0 comments on commit 3b8f834

Please sign in to comment.