diff --git a/src/backend/api/config.py b/src/backend/api/config.py index 86b1cf3d..ce58f0a2 100644 --- a/src/backend/api/config.py +++ b/src/backend/api/config.py @@ -20,7 +20,10 @@ class ConfigResponse(BaseModel): - endpoint: str + Endpoint: str + InstanceId: str + Delimiter: str + ApiPath: str router = APIRouter(prefix="/config", tags=["config"]) @@ -31,5 +34,5 @@ class ConfigResponse(BaseModel): response_model=ConfigResponse, ) async def get_config(req: Request) -> ConfigResponse: - cfg: Config = req.app.state.config - return ConfigResponse(endpoint=cfg.s3gw_addr) + config: Config = req.app.state.config + return ConfigResponse.parse_obj(config.to_dict()) diff --git a/src/backend/api/objects.py b/src/backend/api/objects.py index 74b9e05b..9be11820 100644 --- a/src/backend/api/objects.py +++ b/src/backend/api/objects.py @@ -93,7 +93,9 @@ class ObjectBodyStreamingResponse(StreamingResponse): Helper class to stream the object body. """ - def __init__(self, conn: S3GWClientDep, bucket: str, params: ObjectRequest): + def __init__( + self, conn: S3GWClientDep, bucket: str, params: ObjectRequest + ): # noqa # Note, do not call the parent class constructor which does some # initializations that we don't want at the moment. These are # done at a later stage, e.g. in the `stream_response` method. diff --git a/src/backend/config.py b/src/backend/config.py index d8ac2d3f..c458b9dd 100644 --- a/src/backend/config.py +++ b/src/backend/config.py @@ -12,10 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import os import re from enum import Enum, EnumMeta -from typing import Any +from typing import Any, Dict, Type, TypeVar from fastapi.logger import logger @@ -31,9 +32,45 @@ class S3AddressingStyle(Enum, metaclass=S3AddressingStyleEnumMeta): PATH = "path" -def get_s3gw_address() -> str: - """Obtain s3gw service address from environment, and validate format.""" +ET = TypeVar("ET", bound=Enum) # Enum type. + + +def get_environ_str(key: str, default: str = "") -> str: + """ + Helper function to obtain a string value from an environment variable. + :param key: The name of the environment variable. + :param default: The default value if the variable does not exist. + Defaults to an empty string. + :return: The content of the specified environment variable as string. + """ + value: str = os.environ.get(key, default) + logger.info(f"Using {key}={value}") + return value + + +def get_environ_enum(enum_cls: Type[ET], key: str, default: Any = None) -> ET: + """ + Helper function to obtain an enum value from an environment variable. + :param enum_cls: The enum class to be used. + Note for Python < 3.12: Make sure the `Enum` metaclass overrides + the __contains__ dunder method to do not throw an exception if + the checked value does not exist. + :param key: The name of the environment variable. + :param default: The default value if the variable does not exist. + Defaults to an empty string. + :return: The content of the specified environment variable as enum. + """ + value: str = os.environ.get(key, default).lower() + if value not in enum_cls: + value = default + logger.info(f"Using {key}={value}") + return enum_cls(value) + +def get_s3gw_address() -> str: + """ + Obtain s3gw service address from environment, and validate format. + """ url = os.environ.get("S3GW_SERVICE_URL") if url is None: logger.error("S3GW_SERVICE_URL env variable not set!") @@ -42,12 +79,13 @@ def get_s3gw_address() -> str: if m is None: logger.error(f"Malformed s3gw URL: {url}") raise Exception("Malformed URL") - return url def get_ui_path() -> str: - """Obtain the path under which the UI should be served, e.g. /ui""" + """ + Obtain the path under which the UI should be served, e.g. `/ui`. + """ path = os.environ.get("S3GW_UI_PATH") if path is None: return "/" @@ -67,33 +105,30 @@ def get_api_path(ui_path: str) -> str: return f"{ui_path.rstrip('/')}/api" -def get_s3_addressing_style() -> S3AddressingStyle: +class Config: """ - Obtain the S3 addressing style. Defaults to `auto`. + Keeps config relevant for the backend's operation. """ - addressing_style: str = os.environ.get( - "S3GW_S3_ADDRESSING_STYLE", "auto" - ).lower() - if addressing_style not in S3AddressingStyle: - addressing_style = S3AddressingStyle.AUTO.value - logger.info(f"Using '{addressing_style}' S3 addressing style") - return S3AddressingStyle(addressing_style) - - -class Config: - """Keeps config relevant for the backend's operation.""" # Address for the s3gw instance we're servicing. _s3gw_addr: str _s3_addressing_style: S3AddressingStyle + _s3_prefix_delimiter: str _ui_path: str _api_path: str + _instance_id: str def __init__(self) -> None: self._s3gw_addr = get_s3gw_address() - self._s3_addressing_style = get_s3_addressing_style() + self._s3_addressing_style = get_environ_enum( + S3AddressingStyle, "S3GW_S3_ADDRESSING_STYLE", "auto" + ) + self._s3_prefix_delimiter = get_environ_str( + "S3GW_S3_PREFIX_DELIMITER", "/" + ) self._ui_path = get_ui_path() self._api_path = get_api_path(self._ui_path) + self._instance_id = get_environ_str("S3GW_INSTANCE_ID") logger.info(f"Servicing s3gw at {self._s3gw_addr}") @property @@ -117,3 +152,29 @@ def s3_addressing_style(self) -> S3AddressingStyle: Obtain the S3 addressing style. """ return self._s3_addressing_style + + @property + def s3_prefix_delimiter(self) -> str: + """ + The prefix delimiter. Defaults to `/`. See + https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-prefixes.html + """ + return self._s3_prefix_delimiter + + @property + def instance_id(self) -> str: + """ + Obtain the instance identifier. Defaults to an empty string. + """ + return self._instance_id + + def to_dict(self) -> Dict[str, Any]: + return { + "ApiPath": self.api_path, + "Delimiter": self.s3_prefix_delimiter, + "Endpoint": self.s3gw_addr, + "InstanceId": self.instance_id, + } + + def to_json(self) -> str: + return json.dumps(self.to_dict()) diff --git a/src/backend/tests/unit/api/test_api_config.py b/src/backend/tests/unit/api/test_api_config.py index c7291c40..9e5690fb 100644 --- a/src/backend/tests/unit/api/test_api_config.py +++ b/src/backend/tests/unit/api/test_api_config.py @@ -38,4 +38,4 @@ class MockApp: req.app.state.config = test_config res = await api_config.get_config(req) assert isinstance(res, api_config.ConfigResponse) - assert res.endpoint == "http://foo.bar:123" + assert res.Endpoint == "http://foo.bar:123" diff --git a/src/backend/tests/unit/test_config.py b/src/backend/tests/unit/test_config.py index 6a130025..acca6ad0 100644 --- a/src/backend/tests/unit/test_config.py +++ b/src/backend/tests/unit/test_config.py @@ -22,7 +22,8 @@ from backend.config import ( Config, S3AddressingStyle, - get_s3_addressing_style, + get_environ_enum, + get_environ_str, get_s3gw_address, get_ui_path, ) @@ -139,16 +140,37 @@ def test_api_path_with_trailing_slash() -> None: pytest.fail(str(e)) -def test_get_s3_addressing_style_1() -> None: +def test_get_environ_enum_1() -> None: os.environ["S3GW_S3_ADDRESSING_STYLE"] = "foo" - assert S3AddressingStyle.AUTO == get_s3_addressing_style() + assert S3AddressingStyle.AUTO == get_environ_enum( + S3AddressingStyle, "S3GW_S3_ADDRESSING_STYLE", "auto" + ) -def test_get_s3_addressing_style_2() -> None: +def test_get_environ_enum_2() -> None: os.environ["S3GW_S3_ADDRESSING_STYLE"] = "VIRTUAL" - assert S3AddressingStyle.VIRTUAL == get_s3_addressing_style() + assert S3AddressingStyle.VIRTUAL == get_environ_enum( + S3AddressingStyle, "S3GW_S3_ADDRESSING_STYLE", "auto" + ) -def test_get_s3_addressing_style_3() -> None: +def test_get_environ_enum_3() -> None: os.environ.pop("S3GW_S3_ADDRESSING_STYLE", None) - assert S3AddressingStyle.AUTO == get_s3_addressing_style() + assert S3AddressingStyle.AUTO == get_environ_enum( + S3AddressingStyle, "S3GW_S3_ADDRESSING_STYLE", "auto" + ) + + +def test_get_environ_str_1() -> None: + os.environ["S3GW_S3_PREFIX_DELIMITER"] = "|" + assert "|" == get_environ_str("S3GW_S3_PREFIX_DELIMITER") + + +def test_get_environ_str_2() -> None: + os.environ.pop("S3GW_S3_PREFIX_DELIMITER", None) + assert "&" == get_environ_str("S3GW_S3_PREFIX_DELIMITER", "&") + + +def test_get_environ_str_3() -> None: + os.environ.pop("S3GW_INSTANCE_ID", None) + assert "" == get_environ_str("S3GW_INSTANCE_ID") diff --git a/src/frontend/src/app/app.module.ts b/src/frontend/src/app/app.module.ts index 8fca103b..df466626 100644 --- a/src/frontend/src/app/app.module.ts +++ b/src/frontend/src/app/app.module.ts @@ -10,6 +10,7 @@ import { AppRoutingModule } from '~/app/app-routing.module'; import { getCurrentLanguage, setTranslationService } from '~/app/i18n.helper'; import { PagesModule } from '~/app/pages/pages.module'; import { AppConfigService } from '~/app/shared/services/app-config.service'; +import { AppMainConfigService } from '~/app/shared/services/app-main-config.service'; import { HttpErrorInterceptorService } from '~/app/shared/services/http-error-interceptor.service'; import { SharedModule } from '~/app/shared/shared.module'; import { TranslocoRootModule } from '~/app/transloco-root.module'; @@ -37,6 +38,12 @@ import { TranslocoRootModule } from '~/app/transloco-root.module'; multi: true, deps: [AppConfigService] }, + { + provide: APP_INITIALIZER, + useFactory: (appMainConfigService: AppMainConfigService) => () => appMainConfigService.load(), + multi: true, + deps: [AppMainConfigService] + }, { provide: APP_INITIALIZER, useFactory: (translocoService: TranslocoService) => () => { diff --git a/src/frontend/src/app/pages/shared/login-page/login-page.component.ts b/src/frontend/src/app/pages/shared/login-page/login-page.component.ts index f2d5ae32..922bc4e4 100644 --- a/src/frontend/src/app/pages/shared/login-page/login-page.component.ts +++ b/src/frontend/src/app/pages/shared/login-page/login-page.component.ts @@ -9,9 +9,9 @@ import { DeclarativeFormComponent } from '~/app/shared/components/declarative-fo import { DeclarativeFormConfig } from '~/app/shared/models/declarative-form-config.type'; import { AuthResponse, AuthService } from '~/app/shared/services/api/auth.service'; import { AppConfigService } from '~/app/shared/services/app-config.service'; +import { AppMainConfigService } from '~/app/shared/services/app-main-config.service'; import { BlockUiService } from '~/app/shared/services/block-ui.service'; import { DialogService } from '~/app/shared/services/dialog.service'; -import { NotificationService } from '~/app/shared/services/notification.service'; @Component({ selector: 's3gw-login-page', @@ -57,19 +57,31 @@ export class LoginPageComponent implements OnInit { constructor( private appConfigService: AppConfigService, + private appMainConfigService: AppMainConfigService, private authService: AuthService, private blockUiService: BlockUiService, private dialogService: DialogService, - private notificationService: NotificationService, private router: Router ) { - this.welcomeMessage = translate(TEXT('Welcome to {{ name }}'), this.appConfigService.config); + this.welcomeMessage = translate(TEXT('Welcome to {{ title }}'), this.appConfigService.config); } ngOnInit(): void { this.blockUiService.resetGlobal(); // Ensure all open modal dialogs are closed. this.dialogService.dismissAll(); + // Add the additional `Instance` field to the form if the + // `instanceId` is available. + if (this.appMainConfigService.config.InstanceId) { + this.config.fields.unshift({ + name: 'instanceId', + type: 'text', + label: TEXT('Instance'), + value: this.appMainConfigService.config.InstanceId, + readonly: true, + submitValue: false + }); + } } onLogin(): void { diff --git a/src/frontend/src/app/shared/components/top-bar/top-bar.component.html b/src/frontend/src/app/shared/components/top-bar/top-bar.component.html index 6680b2e3..49a43561 100644 --- a/src/frontend/src/app/shared/components/top-bar/top-bar.component.html +++ b/src/frontend/src/app/shared/components/top-bar/top-bar.component.html @@ -11,7 +11,9 @@ alt="logo"> +
+
@@ -29,6 +31,12 @@
+ + +
{{ appMainConfigService.config.InstanceId }}
+
+
+