Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@
* Added workload description and external link to docs
* 0.3.2: Backend dependencies are bumped
* 0.3.3: Removed limit of returned DB objects in DB List API
* 0.4.0: Enhancements
* DB Owner is shown in Databases API and Administrative UI
* Max number of allowed DB per account can now be set via `DB_MAX_ALLOWED_NUMBER_PER_ACCOUNT` env variable
* Frontend changes:
* 400 and 422 server errors are better handled
6 changes: 3 additions & 3 deletions dbaas/extension.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "DBaaS",
"description": "On-demand provisioning of cloud-based database storages as a service.",
"version": "0.3.3",
"version": "0.4.0",
"audience": ["reseller", "distributor", "vendor"],
"readme_url": "https://github.com/cloudblue/connect-extension-dbaas/blob/0.3.3/README.md",
"changelog_url": "https://github.com/cloudblue/connect-extension-dbaas/blob/0.3.3/CHANGELOG.md",
"readme_url": "https://github.com/cloudblue/connect-extension-dbaas/blob/0.4.0/README.md",
"changelog_url": "https://github.com/cloudblue/connect-extension-dbaas/blob/0.4.0/CHANGELOG.md",
"icon": "googleExtensionBaseline"
}
1 change: 1 addition & 0 deletions dbaas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class DatabaseOutList(DatabaseInCreate):
region: RefOut
tech_contact: RefOut
status: str
owner: RefIn
case: Optional[RefIn]
events: Optional[dict]

Expand Down
24 changes: 23 additions & 1 deletion dbaas/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ async def create(
if tech_contact['id'] != context.user_id:
actor = await cls._get_actor(context, client)

await cls._validate_allowed_db_number_per_account(db, context, config)

prepared_db_doc = cls._prepare_db_document(data, context, region_doc, tech_contact, actor)
inserted_db_doc = await cls._create_db_document(
prepared_db_doc, db, context, client, config,
Expand Down Expand Up @@ -304,6 +306,7 @@ def _default_query(cls, context: Context) -> dict:
@classmethod
def _db_document_repr(cls, db_document: dict, config: dict = None) -> dict:
document = copy(db_document)
document['owner'] = {'id': document.get('account_id')}

case = cls._get_last_db_document_case(document)
if case:
Expand Down Expand Up @@ -354,6 +357,25 @@ async def _get_validated_tech_contact(

return tech_contact

@classmethod
async def _validate_allowed_db_number_per_account(
cls,
db: AsyncIOMotorDatabase,
context: Context,
config: dict,
):
if is_admin_context(context):
return

max_allowed_number_of_db = int(config.get('DB_MAX_ALLOWED_NUMBER_PER_ACCOUNT', 50))

db_coll = db[cls.COLLECTION]
current_number_of_db = await db_coll.count_documents(cls._default_query(context))
if current_number_of_db + 1 > max_allowed_number_of_db:
raise ValueError(
f'Max allowed number of databases is reached: {max_allowed_number_of_db}.',
)

@classmethod
async def _get_actor(cls, context: Context, client: AsyncConnectClient) -> dict:
actor = await ConnectAccountUser.retrieve(context.account_id, context.user_id, client)
Expand Down Expand Up @@ -460,7 +482,7 @@ def _db_collection_from_db_session(cls, db_session, config):

@staticmethod
def _generate_id(config: dict) -> str:
id_random_length = config.get('DB_ID_RANDOM_LENGTH', 5)
id_random_length = int(config.get('DB_ID_RANDOM_LENGTH', 5))
id_prefix = config.get('DB_ID_PREFIX', 'DBPG')

random_part = ''.join(random.choice(string.digits) for _ in range(id_random_length))
Expand Down
2 changes: 2 additions & 0 deletions dbaas/static/7cace99224d3d55c5b00.js

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions dbaas/static/e362c49b3047272f792b.js

This file was deleted.

2 changes: 1 addition & 1 deletion dbaas/static/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<html><head><title>Lorem ipsum</title><link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Roboto+Mono:400,500|Material+Icons" rel="stylesheet"><link id="mock-favicon" rel="shortcut icon" href="#"><script defer="defer" src="e362c49b3047272f792b.js"></script><link href="main.css" rel="stylesheet"></head><body><div id="app"></div></body></html>
<html><head><title>Lorem ipsum</title><link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Roboto+Mono:400,500|Material+Icons" rel="stylesheet"><link id="mock-favicon" rel="shortcut icon" href="#"><script defer="defer" src="7cace99224d3d55c5b00.js"></script><link href="main.css" rel="stylesheet"></head><body><div id="app"></div></body></html>
10 changes: 5 additions & 5 deletions dbaas/static/main.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cloudblueconnect/eaas-database-extension",
"version": "0.3.2",
"version": "0.4.0",
"description": "On-demand provisioning of cloud-based database storages as a service.",
"author": "Ingram Micro",
"license": "Apache Software License 2.0",
Expand Down
4 changes: 4 additions & 0 deletions tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class DBFactory(factory.DictFactory):

account_id = factory.Sequence(lambda n: f'VA-{n:05}')

@factory.post_generation
def owner(obj, *a, **kw):
obj['owner'] = {'id': obj['account_id']}


class InstallationFactory(factory.DictFactory):
id = factory.Sequence(lambda n: f'EIN-{n:05}')
Expand Down
52 changes: 48 additions & 4 deletions tests/services/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,28 @@ async def test_list_no_account_dbs(db, admin_context):
),
))
def test__db_document_repr(in_doc, out_doc):
assert DB._db_document_repr(in_doc) == out_doc
result = DB._db_document_repr(in_doc)
del result['owner']

assert result == out_doc


@pytest.mark.parametrize('in_doc, out_doc', (
({}, {}),
({1: True, 'a': 'key'}, {1: True, 'a': 'key'}),
({}, {'owner': {'id': None}}),
({1: True, 'a': 'key'}, {1: True, 'a': 'key', 'owner': {'id': None}}),
(
{
'status': DBStatus.ACTIVE,
'credentials': b'gAAAAABkEdPYFZffrdrEU5_jwzsBO-GstLDA2IYs8uAN7jGyQ4KRKw_'
b'CoxytmSLMdTi_NQ49Oe15RWgVtkbEFM2PAZ4wQI9sLQ==',
'account_id': 'VA-123',
},
{
'status': DBStatus.ACTIVE,
'credentials': {'1': 1},
'account_id': 'VA-123',
'owner': {'id': 'VA-123'},
},
{'status': DBStatus.ACTIVE, 'credentials': {'1': 1}},
),
))
def test__db_document_repr_with_config(in_doc, out_doc, config):
Expand Down Expand Up @@ -259,6 +268,36 @@ async def test__get_actor_ok(async_client_mocker_factory, mocker):
p.assert_called_once_with('PA-123', user['id'], 'client')


@pytest.mark.asyncio
async def test__validate_allowed_db_number_per_account_is_admin(admin_context):
assert await DB._validate_allowed_db_number_per_account('db', admin_context, 'config') is None


@pytest.mark.asyncio
@pytest.mark.parametrize('max_num, error', (
(0, 'Max allowed number of databases is reached: 0.'),
(1, 'Max allowed number of databases is reached: 1.'),
))
async def test__validate_allowed_db_number_per_account_exceeds_max(
db, common_context, max_num, error,
):
account_id = 'VA-234'
common_context.account_id = account_id
await db[Collections.DB].insert_one({'id': 'DB-200', 'account_id': account_id})

config = {'DB_MAX_ALLOWED_NUMBER_PER_ACCOUNT': max_num}

with pytest.raises(ValueError) as e:
await DB._validate_allowed_db_number_per_account(db, common_context, config)

assert str(e.value) == error


@pytest.mark.asyncio
async def test__validate_allowed_db_number_per_account_ok(db, common_context, config):
assert await DB._validate_allowed_db_number_per_account(db, common_context, config) is None


def test__prepare_db_document(mocker):
data = {'name': 'DB-1'}
context = Context(account_id='VA-123')
Expand Down Expand Up @@ -520,6 +559,10 @@ async def test_create_ok(mocker, context_uid, am_call_count, actor):
'dbaas.services.DB._get_actor',
AsyncMock(return_value=actor),
)
vn_p = mocker.patch(
'dbaas.services.DB._validate_allowed_db_number_per_account',
AsyncMock(),
)
pdd_p = mocker.patch('dbaas.services.DB._prepare_db_document', return_value='prepared_db_doc')
cdd_p = mocker.patch(
'dbaas.services.DB._create_db_document',
Expand All @@ -539,6 +582,7 @@ async def test_create_ok(mocker, context_uid, am_call_count, actor):

gvrd_p.assert_called_once_with(data, 'db')
gvtc_p.assert_called_once_with(data, context, 'client')
vn_p.assert_called_once_with('db', context, 'config')
assert ga_p.call_count == am_call_count
pdd_p.assert_called_once_with(
data, context, 'region_doc', {'id': 'UR-123-456'}, actor,
Expand Down
4 changes: 4 additions & 0 deletions tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def test_database_out_list():
tech_contact__name='user',
cases=CaseFactory.create_batch(2),
events={'happened': {'at': 1}},
account_id='VA-123',
)

assert jsonable_encoder(DatabaseOutList(**db)) == {
Expand All @@ -229,6 +230,7 @@ def test_database_out_list():
'status': 'reviewing',
'case': None,
'events': {'happened': {'at': 1}},
'owner': {'id': 'VA-123'},
}


Expand All @@ -250,6 +252,7 @@ def test_database_out_detail():
'host': 'some',
'password': 'qwerty',
},
account_id='PA-123',
)

assert jsonable_encoder(DatabaseOutDetail(case=CaseFactory(id='CS-100'), **db)) == {
Expand All @@ -268,6 +271,7 @@ def test_database_out_detail():
'password': 'qwerty',
'name': None,
},
'owner': {'id': 'PA-123'},
}


Expand Down
9 changes: 8 additions & 1 deletion ui/app/tools/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,17 @@ async function request(
data = responseData;
}

let errorMsg = `Server responded with non-ok code: ${response.status}`;
if (response.status === 422) {
errorMsg = 'An input error occurred. Please fill all required fields';
} else if (response.status === 400 && 'message' in data) {
errorMsg = data.message;
}

throw new ApiError(
data,
response,
`Server responded with non-ok code: ${response.status}`,
errorMsg,
);
}

Expand Down
9 changes: 3 additions & 6 deletions ui/app/views/ActivateDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,7 @@ export default {
this.$emit('saved');
this.close();
} catch (e) {
if (e.status === 422) {
this.errorText = 'An input error occurred. Please fill all required fields';
} else {
this.errorText = `#${e.status} ${e.message}`;
}

this.errorText = e.message;
this.emit({ name: 'snackbar:error', value: e });
}

Expand All @@ -158,6 +153,8 @@ export default {
value: {
immediate: true,
handler(v) {
this.errorText = null;

const data = clone(this.item);
if (!data?.credentials) data.credentials = initialForm().credentials;
this.form = data;
Expand Down
8 changes: 2 additions & 6 deletions ui/app/views/CreateEditDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,7 @@ export default {
this.$emit('saved');
this.close();
} catch (e) {
if (e.status === 422) {
this.errorText = 'An input error occurred. Please fill all required fields';
} else {
this.errorText = `#${e.status} ${e.message}`;
}

this.errorText = e.message;
this.emit({ name: 'snackbar:error', value: e });
}

Expand All @@ -235,6 +230,7 @@ export default {
immediate: true,
async handler(v) {
if (v) {
this.errorText = null;
if (this.item) this.form = clone(this.item);

try {
Expand Down
4 changes: 4 additions & 0 deletions ui/app/views/ItemDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ div
.item-label Workload Type
.item-value.capitalize {{ localItem.workload }}

.item-row(v-if="installationContext.isAdmin")
.item-label Owner ID
.item-value.capitalize {{ localItem.owner.id }}

.divider._mt_24._mb_24

.item-row
Expand Down
10 changes: 9 additions & 1 deletion ui/app/views/ItemsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ div
template(#name="{ item }")
.detail-item
a.detail-item__text(@click="$emit('item-clicked', item)") {{ item.name }}
.detail-item__assistive-text {{ item.id }}
.detail-item__assistive-text
span {{ item.id }}
span(v-if="installationContext.isAdmin") • {{ item.owner.id }}

template(#description="{ value }")
.assistive-text {{ value }}
Expand All @@ -56,6 +58,10 @@ div
</template>

<script>
import {
mapState,
} from 'vuex';

import {
googleStorageBaseline,
} from '@cloudblueconnect/material-svg/baseline';
Expand Down Expand Up @@ -94,6 +100,8 @@ export default {
}),

computed: {
...mapState(['installationContext']),

placeholderIcon: () => googleStorageBaseline,

showPlaceholder: ({ list, loading }) => !loading && isNilOrEmpty(list),
Expand Down
1 change: 1 addition & 0 deletions ui/app/views/ReconfDialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('ReconfDialog', () => {
describe('#data', () => {
it('should provide initial data', () => {
expect(cmp.data()).toEqual({
errorText: null,
dialogOpened: false,
acceptTermsAndConds: false,
saving: false,
Expand Down
4 changes: 4 additions & 0 deletions ui/app/views/ReconfDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ ez-dialog(
v-model="dialogOpened",
width="800",
title="Request Reconfiguration",
:error-text="errorText",
)
ui-card(title="Type")
.two-columns
Expand Down Expand Up @@ -106,6 +107,7 @@ export default {
},

data: () => ({
errorText: null,
dialogOpened: false,
acceptTermsAndConds: false,
form: initialForm(),
Expand Down Expand Up @@ -138,6 +140,7 @@ export default {
this.$emit('saved', item);
this.close();
} catch (e) {
this.errorText = e.message;
this.emit({ name: 'snackbar:error', value: e });
/* eslint-disable no-console */
console.error(e);
Expand All @@ -151,6 +154,7 @@ export default {
value: {
immediate: true,
handler(v) {
this.errorText = null;
this.dialogOpened = v;
},
},
Expand Down