Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2005c86
Added fixture waiting unti SaaS database is running
ckunki Apr 30, 2024
4e51522
fixed typo in changes file
ckunki Apr 30, 2024
6c274ef
Merge branch 'main' into feature/#14-fixture-operational_saas_databas…
ckunki Apr 30, 2024
7e1cb91
Fixed merge errors
ckunki Apr 30, 2024
de6a01f
Fixed review findings
ckunki May 6, 2024
dad0f11
Fixed review findings
ckunki May 6, 2024
ebef57c
Added user name to resources in Exasol Saas
ckunki May 6, 2024
97f44cc
fixed method call
ckunki May 6, 2024
8a400e4
fixed _timestamp_name()
ckunki May 6, 2024
0985a0d
replaced os.getlogin() by getpass.getuser()
ckunki May 6, 2024
bb93dda
shortened database name
ckunki May 6, 2024
6b328bf
Added log messages for deleting the database
ckunki May 6, 2024
b21c96a
Make pytest display log output of tests cases in CI build
ckunki May 6, 2024
2da0580
Added sleep before deleting the database
ckunki May 6, 2024
66026b0
Added log message for creating a database
ckunki May 6, 2024
26cf321
Excluded generated code from coverage
ckunki May 7, 2024
d822647
Fixed first batch of review findings
ckunki May 7, 2024
2b2b4a3
Refactored extracting minutes from timedelta for SaaS API
ckunki May 7, 2024
aace19e
Added parameter region for create_database()
ckunki May 7, 2024
5af0e1d
Moved limits into dedicated class
ckunki May 7, 2024
bd0fcc9
Use project short tag for saas resource
ckunki May 7, 2024
2534a4a
Renamed database limits to meet linter requirements
ckunki May 7, 2024
381e404
Fixed type check
ckunki May 7, 2024
fb30fa7
fixed import
ckunki May 7, 2024
db989b6
Made parameter project short tag for timestamp_name optional
ckunki May 7, 2024
3e23db0
Fixed review finding
ckunki May 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ jobs:
SAAS_HOST: ${{ secrets.INTEGRATION_TEAM_SAAS_STAGING_HOST }}
SAAS_ACCOUNT_ID: ${{ secrets.INTEGRATION_TEAM_SAAS_STAGING_ACCOUNT_ID }}
SAAS_PAT: ${{ secrets.INTEGRATION_TEAM_SAAS_STAGING_PAT }}
PYTEST_ADDOPTS: -o log_cli=true -o log_cli_level=INFO
run: poetry run nox -s coverage -- --

- name: Upload Artifacts
Expand Down
4 changes: 4 additions & 0 deletions doc/changes/changes_0.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ This release adds integration tests for the most important calls to SaaS API.

* #21: Added integration test for operation "create database"
* #23: Added integration test for operation "add IP to whitelist"

## Feature

* #14: Added fixture waiting until SaaS database is running
* #25: Fixed transitive dependencies required by generated API client
4 changes: 2 additions & 2 deletions doc/developer_guide/developer_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ openapi-python-client reads the JSON specification of the SaaS API and generates
The easiest way is to make openapi-python-client create a dedicated file `pyproject.toml` and copy the transitive dependencies from there to SAPIPY's file `pyproject.toml`.

In order to create file `pyproject.toml`
* In file `noxfile.py` you need to replace mode `update` by `generate`
* Additionally in file `openapi_config.yml` you need to specify a non-existing top-level directory as `name` and a package that does not contain slashes, e.g.
* In file `noxfile.py`, function `generate_api` you need to replace mode `update` by `generate`.
* Additionally, in file `openapi_config.yml` you need to specify a non-existing top-level directory as `project_name_override` and a package that does not contain slashes, e.g.

```yaml
project_name_override: "generate"
Expand Down
16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ python = ">=3.8.0,<4.0"
requests = "^2.31.0"
types-requests = "^2.31.0.6"
ifaddr = "^0.2.0"
tenacity = "^8.2.3"
# generated by openapi-python-client
httpx = ">=0.20.0,<0.28.0"
attrs = ">=21.3.0"
Expand Down Expand Up @@ -56,6 +57,10 @@ source = [
"exasol",
]

omit = [
'*/exasol/saas/client/openapi/*',
]

[tool.coverage.report]
fail_under = 15

Expand Down
100 changes: 92 additions & 8 deletions test/integration/api_access.py
Comment thread
ckunki marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,22 +1,52 @@
import getpass
import logging
import time

from typing import Iterable
from contextlib import contextmanager
from datetime import datetime
from datetime import datetime, timedelta
from tenacity.wait import wait_fixed
from tenacity.stop import stop_after_delay

from exasol.saas.client import openapi
from exasol.saas.client.openapi.models.status import Status
from exasol.saas.client.openapi.api.databases import (
create_database,
delete_database,
list_databases,
get_database,
)
from exasol.saas.client.openapi.api.security import (
list_allowed_i_ps,
add_allowed_ip,
delete_allowed_ip,
)
from tenacity import retry, TryAgain


LOG = logging.getLogger(__name__)
LOG.setLevel(logging.INFO)


# For auto-stopping idle database clusters
MINIMUM_IDLE_TIME = timedelta(minutes=15)
Comment thread
ckunki marked this conversation as resolved.
Outdated


# If deleting a database too early, then logging and accounting could be invalid.
MINIMUM_LIFETIME = timedelta(seconds=30)
Comment thread
ckunki marked this conversation as resolved.
Outdated


def _timestamp_name() -> str:
username = getpass.getuser()
timestamp = f'{datetime.now().timestamp():.0f}'
return f"{username}-{timestamp}"

@tkilias tkilias May 7, 2024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still can break for long user names, we need to shorten it differently. Furthermore, we still should mention from where the DB is from. So, mu suggestion would be something like the following

{Timestamp}{project_short_tag}_{username} and this overall string truncated to length

@tomuben what is the max length for the DB name in SaaS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    input_parameter_limits = {
        "max_database_name_length": 20,  # in characters
        "max_cluster_name_length": 40,  # in characters
        "autostop_min_idle_time": 15,  # in minutes
        "autostop_max_idle_time": 10000,  # in minutes
        "autostop_default_idle_time": 120,
    }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ckunki maybe we put the unshortened name as main cluster name

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, I wonder if the user name is helpful in CI builds.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, the user name was runner, this would indicate the CI



def timestamp() -> str:
return f'{datetime.now().timestamp():.0f}'
class DatabaseStartupFailure(Exception):
"""
If a SaaS database instance during startup reports a status other than
successful.
"""


def create_saas_client(
Expand Down Expand Up @@ -46,12 +76,18 @@ def create_database(self, cluster_size: str = "XS") -> openapi.models.database.D
cluster_spec = openapi.models.CreateCluster(
name="my-cluster",
size=cluster_size,
auto_stop=openapi.models.AutoStop(
enabled=True,
idle_time=int(MINIMUM_IDLE_TIME.seconds / 60),
Comment thread
ckunki marked this conversation as resolved.
Outdated
),
)
db_name = _timestamp_name()
LOG.info(f"Creating database {db_name}")
return create_database.sync(
self._account_id,
client=self._client,
body=openapi.models.CreateDatabase(
name=f"pytest-{timestamp()}",
name=db_name,
Comment thread
ckunki marked this conversation as resolved.
Outdated
initial_cluster=cluster_spec,
provider="aws",
region='us-east-1',
Expand Down Expand Up @@ -81,12 +117,52 @@ def database(
ignore_delete_failure: bool = False,
):
db = None
start = datetime.now()
try:
db = self.create_database()
yield db
self.wait_for_delete_clearance(start)
finally:
if not keep and db:
self.delete_database(db.id, ignore_delete_failure)
if db and not keep:
LOG.info(f"Deleting database {db.name}")
response = self.delete_database(db.id, ignore_delete_failure)
Comment thread
ckunki marked this conversation as resolved.
if response.status_code == 200:
LOG.info(f"Successfully deleted database {db.name}.")
else:
LOG.info(f"Ignoring status code {response.status_code}.")
Comment thread
ckunki marked this conversation as resolved.
Outdated
elif not db:
LOG.warning("Cannot delete db None")
else:
LOG.info(f"Keeping database {db.name} as keep = {keep}")

def get_database(self, database_id: str) -> openapi.models.database.Database:
return get_database.sync(
self._account_id,
database_id,
client=self._client,
)

def wait_until_running(
self,
database_id: str,
timeout: timedelta = timedelta(minutes=30),
interval: timedelta = timedelta(minutes=2),
) -> str:
success = [
Status.RUNNING,
]

@retry(wait=wait_fixed(interval), stop=stop_after_delay(timeout))
def poll_status():
Comment thread
ckunki marked this conversation as resolved.
db = self.get_database(database_id)
if db.status not in success:
print(f'status = {db.status}')
raise TryAgain
return db.status

if poll_status() not in success:
Comment thread
ckunki marked this conversation as resolved.
raise DatabaseStartupFailure()


def list_allowed_ip_ids(self) -> Iterable[openapi.models.allowed_ip.AllowedIP]:
ips = list_allowed_i_ps.sync(
Expand All @@ -95,6 +171,14 @@ def list_allowed_ip_ids(self) -> Iterable[openapi.models.allowed_ip.AllowedIP]:
)
return (x.id for x in ips)

def wait_for_delete_clearance(self, start: datetime.time):
Comment thread
ckunki marked this conversation as resolved.
Outdated
lifetime = datetime.now() - start
if lifetime < MINIMUM_LIFETIME:
wait = MINIMUM_LIFETIME - lifetime
LOG.info(f"Waiting {int(wait.seconds)} seconds"
" before deleting the database.")
time.sleep(wait.seconds)

def add_allowed_ip(self, cidr_ip: str = "0.0.0.0/0") -> openapi.models.allowed_ip.AllowedIP:
"""
Suggested values for cidr_ip:
Expand All @@ -103,7 +187,7 @@ def add_allowed_ip(self, cidr_ip: str = "0.0.0.0/0") -> openapi.models.allowed_i
* ::/0 = all ipv6
"""
rule = openapi.models.create_allowed_ip.CreateAllowedIP(
name=f"pytest-{timestamp()}",
name=_timestamp_name(),
cidr_ip=cidr_ip,
)
return add_allowed_ip.sync(
Expand All @@ -129,5 +213,5 @@ def allowed_ip(
ip = self.add_allowed_ip(cidr_ip)
yield ip
finally:
if not keep and ip:
if ip and not keep:
self.delete_allowed_ip(ip.id, ignore_delete_failure)
7 changes: 7 additions & 0 deletions test/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ def saas_database(api_access) -> openapi.models.database.Database:
"""
with api_access.database() as db:
yield db


@pytest.fixture(scope="session")
def operational_saas_database_id(api_access) -> str:
with api_access.database() as db:
api_access.wait_until_running(db.id)
yield db
17 changes: 17 additions & 0 deletions test/integration/databases_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import pytest

from exasol.saas.client import openapi
from tenacity import RetryError
from datetime import datetime, timedelta


def test_lifecycle(api_access):
Expand All @@ -14,6 +18,7 @@ def test_lifecycle(api_access):

testee = api_access
with testee.database(ignore_delete_failure=True) as db:
start = datetime.now()
# verify state and clusters of created database
assert db.status == openapi.models.Status.TOCREATE and \
Comment thread
ckunki marked this conversation as resolved.
Outdated
db.clusters.total == 1
Expand All @@ -22,5 +27,17 @@ def test_lifecycle(api_access):
assert db.id in testee.list_database_ids()

# delete database and verify database is not listed anymore
testee.wait_for_delete_clearance(start)
testee.delete_database(db.id)
assert db.id not in testee.list_database_ids()


def test_poll(api_access):
with api_access.database() as db:
print(f'{db.status}')
with pytest.raises(RetryError):
api_access.wait_until_running(
db.id,
timeout=timedelta(seconds=3),
interval=timedelta(seconds=1),
)