Skip to content

Commit

Permalink
Instance Identification
Browse files Browse the repository at this point in the history
  • Loading branch information
votdev committed Nov 15, 2023
1 parent 3273f48 commit facd3c7
Show file tree
Hide file tree
Showing 20 changed files with 267 additions and 90 deletions.
9 changes: 6 additions & 3 deletions src/backend/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@


class ConfigResponse(BaseModel):
endpoint: str
Endpoint: str
InstanceId: str
Delimiter: str
ApiPath: str


router = APIRouter(prefix="/config", tags=["config"])
Expand All @@ -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())
4 changes: 3 additions & 1 deletion src/backend/api/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
99 changes: 80 additions & 19 deletions src/backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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!")
Expand All @@ -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 "/"
Expand All @@ -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
Expand All @@ -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())
2 changes: 1 addition & 1 deletion src/backend/tests/unit/api/test_api_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
36 changes: 29 additions & 7 deletions src/backend/tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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")
7 changes: 7 additions & 0 deletions src/frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
alt="logo">
</div>
</div>

<div class="flex-fill"></div>

<div class="actions d-flex align-items-center ps-3">
<ng-container *ngIf="isAdmin">
<div class="form-check form-switch switch-danger me-2">
Expand All @@ -29,6 +31,12 @@
</div>
<div class="ms-3 vr"></div>
</ng-container>

<ng-container *ngIf="appMainConfigService.config.InstanceId">
<div class="m-3">{{ appMainConfigService.config.InstanceId }}</div>
<div class="vr"></div>
</ng-container>

<button class="btn btn-simple mx-2"
title="{{ 'Notifications' | transloco }}"
(click)="onToggleNotifications($event)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ViewMode } from '~/app/shared/enum/view-mode.enum';
import { NavigationConfig } from '~/app/shared/models/navigation-config.type';
import { 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 { AuthSessionService } from '~/app/shared/services/auth-session.service';
import { ModalDialogService } from '~/app/shared/services/modal-dialog.service';
import { NavigationConfigService } from '~/app/shared/services/navigation-config.service';
Expand All @@ -33,6 +34,7 @@ export class TopBarComponent implements OnDestroy {

constructor(
public appConfigService: AppConfigService,
public appMainConfigService: AppMainConfigService,
private authService: AuthService,
private authSessionService: AuthSessionService,
private modalDialogService: ModalDialogService,
Expand Down
12 changes: 6 additions & 6 deletions src/frontend/src/app/shared/forms/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import validator from 'validator';

import { format } from '~/app/functions.helper';
import { Constraint } from '~/app/shared/models/constraint.type';
import { AppMainConfigService } from '~/app/shared/services/app-main-config.service';
import { ConstraintService } from '~/app/shared/services/constraint.service';
import { S3gwConfigService } from '~/app/shared/services/s3gw-config.service';

const isEmptyInputValue = (value: any): boolean =>
value == null || ((typeof value === 'string' || Array.isArray(value)) && value.length === 0);
Expand Down Expand Up @@ -239,12 +239,12 @@ export class S3gwValidators {
return { custom: format(TEXT('Maximum length is {{ num }} characters.'), { num: 1024 }) };
}
const injector = Injector.create([
{ provide: S3gwConfigService, useClass: S3gwConfigService, deps: [] }
{ provide: AppMainConfigService, useClass: AppMainConfigService, deps: [] }
]);
const s3gwConfigService = injector.get(S3gwConfigService);
const appMainConfigService = injector.get(AppMainConfigService);
const parts: string[] = _.split(
_.trim(control.value, s3gwConfigService.config.delimiter),
s3gwConfigService.config.delimiter
_.trim(control.value, appMainConfigService.config.Delimiter),
appMainConfigService.config.Delimiter
);
_.remove(parts, _.isEmpty);
const valid = _.every(parts, (part: string) =>
Expand All @@ -258,7 +258,7 @@ export class S3gwValidators {
"The value contains invalid characters. Only letters, numbers, blanks, !_*'()&$@=;/:+,?.- and the delimiter {{ delimiter }} are allowed."
),
{
delimiter: s3gwConfigService.config.delimiter
delimiter: appMainConfigService.config.Delimiter
}
)
)
Expand Down
Loading

0 comments on commit facd3c7

Please sign in to comment.