applications: mark elements with a not-installed status at first (#81783)
gitea/hobo/pipeline/head This commit looks good Details

This commit is contained in:
Frédéric Péters 2023-09-29 12:10:40 +02:00
parent 5c2b51f671
commit 3447df418f
4 changed files with 77 additions and 37 deletions

View File

@ -24,7 +24,9 @@ class Migration(migrations.Migration):
model_name='relation',
name='error_status',
field=models.CharField(
choices=[('notfound', 'Not Found'), ('error', 'Error')], max_length=100, null=True
choices=[('notfound', 'Not Found'), ('error', 'Error'), ('not-installed', 'Not installed')],
max_length=100,
null=True,
),
),
]

View File

@ -140,6 +140,7 @@ class Application(models.Model):
if not remote_elements[element.type].get(element.slug):
continue
remote_element = remote_elements[element.type][element.slug]
relation.reset_error()
if cache_only:
if element.cache == remote_element:
continue
@ -166,7 +167,7 @@ class Application(models.Model):
continue
response = requests.get(dependencies_url)
if not response.ok:
rel.set_error(response.status_code)
rel.set_error_from_http_code(response.status_code)
raise ScanError(
_('Failed to scan "%s" (type %s, slug %s) dependencies (%s)')
% (el.name, el.type, el.slug, response.status_code)
@ -259,6 +260,7 @@ class Relation(models.Model):
choices=[
('notfound', _('Not Found')),
('error', _('Error')),
('not-installed', _('Not installed')),
],
null=True,
)
@ -266,11 +268,14 @@ class Relation(models.Model):
def __repr__(self):
return '<Relation %s - %s/%s>' % (self.application.slug, self.element.type, self.element.slug)
def set_error(self, http_status_code):
def set_error(self, status):
self.error = True
self.error_status = 'notfound' if http_status_code == 404 else 'error'
self.error_status = status
self.save()
def set_error_from_http_code(self, http_status_code):
self.set_error('notfound' if http_status_code == 404 else 'error')
def reset_error(self):
self.error = False
self.error_status = None

View File

@ -462,6 +462,7 @@ class ConfirmInstall(TemplateView):
element=element,
auto_dependency=element_dict['auto-dependency'],
)
relation.set_error('not-installed')
relation.save()
self.version.bundle.seek(0)

View File

@ -1,4 +1,5 @@
import base64
import copy
import io
import json
import random
@ -26,6 +27,7 @@ from hobo.environment.models import Authentic, Variable, Wcs
pytestmark = pytest.mark.django_db
WCS_AVAILABLE_OBJECTS = {
'data': [
{
@ -160,6 +162,16 @@ WCS_FORM_DEPENDENCIES = {
}
class StatefulHTTMock(HTTMock):
def __enter__(self):
self.orig_wcs_available_forms = copy.deepcopy(WCS_AVAILABLE_FORMS)
return super().__enter__()
def __exit__(self, *args, **kwargs):
WCS_AVAILABLE_FORMS['data'] = self.orig_wcs_available_forms['data']
return super().__exit__(*args, **kwargs)
def mocked_http(url, request):
assert url.query.count('&signature=') == 1
@ -195,6 +207,18 @@ def mocked_http(url, request):
return {'content': '<carddef/>', 'status_code': 200, 'headers': {'content-length': '10'}}
if url.path == '/api/export-import/bundle-import/':
# alter WCS_AVAILABLE_FORMS response with newly installed forms
with tarfile.open(mode='r', fileobj=io.BytesIO(request.body)) as tar:
manifest_json = json.load(tar.extractfile('manifest.json'))
for element in manifest_json.get('elements'):
if element['type'] not in [x['id'] for x in WCS_AVAILABLE_OBJECTS['data']]:
continue
if element['type'] == 'forms' and element['slug'] not in [
x['id'] for x in WCS_AVAILABLE_FORMS['data']
]:
WCS_AVAILABLE_FORMS['data'].append(
{'id': element['slug'], 'text': element['name'], 'type': 'forms'}
)
return {
'content': json.dumps({'err': 0, 'url': 'https://wcs.example.invalid/api/jobs/job-uuid/'}),
'status_code': 200,
@ -246,7 +270,7 @@ def test_create_application(app, admin_user, settings, analyze):
resp.form['name'] = 'Test'
resp = resp.form.submit()
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.follow()
assert 'You should now assemble the different parts of your application.' in resp.text
@ -417,7 +441,7 @@ def test_create_application(app, admin_user, settings, analyze):
return mocked_http(url, request)
resp = app.get('/applications/manifest/test/generate/')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(DeploymentError) as e:
resp.form.submit()
assert str(e.value) == 'Failed to declare elements for module wcs (500)'
@ -476,7 +500,7 @@ def test_manifest_ordering(app, admin_user, settings):
Relation.objects.create(application=application, element=element, auto_dependency=auto_dependency)
login(app)
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = app.get('/applications/manifest/test/')
assert resp.pyquery('.application-content li a').text() == (
'Baaaar - Form remove '
@ -528,7 +552,7 @@ def test_scandeps_on_unknown_element(app, admin_user, settings):
login(app)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(ScanError) as e:
app.get('/applications/manifest/test/scandeps/').follow()
assert str(e.value) == 'Failed to scan "Unknown" (type forms, slug unknown) dependencies (404)'
@ -544,7 +568,7 @@ def test_scandeps_on_unknown_element(app, admin_user, settings):
return {'status_code': 500}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(ScanError) as e:
app.get('/applications/manifest/test/scandeps/').follow()
assert str(e.value) == 'Failed to scan "Unknown" (type forms, slug unknown) dependencies (500)'
@ -555,7 +579,7 @@ def test_scandeps_on_unknown_element(app, admin_user, settings):
assert relation.error is True
assert relation.error_status == 'error'
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
app.get('/applications/manifest/test/scandeps/').follow()
job = AsyncJob.objects.latest('pk')
assert job.status == 'completed'
@ -639,7 +663,7 @@ def test_scandeps_on_renamed_element(app, admin_user, settings):
return mocked_http(url, request)
login(app)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
app.get('/applications/manifest/test/scandeps/').follow()
job = AsyncJob.objects.latest('pk')
assert job.status == 'completed'
@ -671,7 +695,7 @@ def test_scandeps_on_renamed_element(app, admin_user, settings):
return {'status_code': 404}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(ScanError) as e:
app.get('/applications/manifest/test/scandeps/').follow()
assert str(e.value) == 'Failed to get elements of type forms (404)'
@ -684,7 +708,7 @@ def test_scandeps_on_renamed_element(app, admin_user, settings):
return {'status_code': 500}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(ScanError) as e:
app.get('/applications/manifest/test/scandeps/').follow()
assert str(e.value) == 'Failed to get elements of type forms (500)'
@ -742,7 +766,7 @@ def test_redirect_application_element(app, admin_user, settings, editable):
login(app)
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = app.get('/applications/manifest/test/')
assert 'https://wcs.example.invalid/api/export-import/forms/test-form/redirect/' in resp
assert (
@ -777,7 +801,7 @@ def test_delete_application(app, admin_user, settings):
assert Application.objects.count() == 2
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = app.get('/applications/manifest/app_to_delete/')
resp = resp.click(re.compile('^Delete$'))
resp = resp.forms[0].submit()
@ -794,7 +818,7 @@ def test_delete_application(app, admin_user, settings):
return {'status_code': 500}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = app.get('/applications/manifest/other_app/')
resp = resp.click(re.compile('^Delete$'))
resp = resp.forms[0].submit()
@ -885,7 +909,7 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
def install(resp, bundle):
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', bundle, 'application/x-tar')
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.form.submit().follow()
assert Application.objects.count() == 1
@ -908,11 +932,14 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
assert job.progression_urls == {'wcs': {'Foobar': 'https://wcs.example.invalid/api/jobs/job-uuid/'}}
assert Parameter.objects.filter(application=application).count() == 2
assert Variable.objects.filter(name__startswith='app_').count() == 2
# check forms are not marked as error (the other object types are not altered in the mocked responses
# and would be marked as errors)
assert [x.error for x in application.relation_set.filter(element__type='forms')] == [False]
return version
resp = app.get('/applications/')
if action == 'Update':
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.click(href='/manifest/test/')
version1 = install(resp, app_bundle)
if action == 'Update':
@ -950,14 +977,19 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
resp = app.get('/applications/')
if action == 'Update':
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.click(href='manifest/test/')
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(DeploymentError) as e:
resp.form.submit().follow()
assert str(e.value) == 'Failed to deploy module wcs (500)'
application = Application.objects.get(slug='test')
# check elements are marked as not installed if there was a failure
assert [
(x.error, x.error_status) for x in application.relation_set.filter(element__type='forms')
] == [(True, 'not-installed')]
job = AsyncJob.objects.latest('pk')
assert job.status == 'failed'
assert job.exception == 'Failed to deploy module wcs (500)'
@ -983,11 +1015,11 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
resp = app.get('/applications/')
if action == 'Update':
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.click(href='manifest/test/')
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp.form.submit().follow()
application = Application.objects.get(slug='test')
elements = application.elements.all().order_by('type')
@ -1004,11 +1036,11 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
resp = app.get('/applications/')
if action == 'Update':
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.click(href='manifest/test/')
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(ScanError) as e:
resp.form.submit().follow()
assert str(e.value) == 'Failed to get elements of type forms (500)'
@ -1019,7 +1051,7 @@ def test_deploy_application(app, admin_user, settings, app_bundle, app_bundle_wi
# bad file format
resp = app.get('/applications/')
if action == 'Update':
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.click(href='manifest/test/')
resp = resp.click(action)
resp.form['bundle'] = Upload('app.tar', b'garbage', 'application/x-tar')
@ -1061,7 +1093,7 @@ def test_update_application(app, admin_user, settings, app_bundle):
application.save()
resp = app.get('/applications/manifest/test/update/')
resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.form.submit()
# only legacy elements, do the update and redirect to manifest
last_version = Version.objects.latest('pk')
@ -1092,7 +1124,7 @@ def test_update_application(app, admin_user, settings, app_bundle):
Version.objects.filter(pk=last_version.pk).update(number='1.0')
resp = app.get('/applications/manifest/test/update/')
resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = resp.form.submit()
last_version = Version.objects.latest('pk')
assert resp.location.endswith('/applications/manifest/test/confirm/%s/' % last_version.pk)
@ -1138,7 +1170,7 @@ def test_update_application(app, admin_user, settings, app_bundle):
Version.objects.filter(pk=last_version.pk).update(number='2.0')
resp = app.get('/applications/manifest/test/update/')
resp.form['bundle'] = Upload('app.tar', app_bundle, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = resp.form.submit()
assert resp.location.endswith(
'/applications/manifest/test/confirm/%s/' % Version.objects.latest('pk').pk
@ -1277,7 +1309,7 @@ def test_refresh_application(app, admin_user, settings):
}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = app.get('/applications/manifest/test/refresh/')
application = Application.objects.get(slug='test')
element.refresh_from_db()
@ -1291,7 +1323,7 @@ def test_refresh_application(app, admin_user, settings):
element.cache = {'foo': 'bar'}
element.save()
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = app.get('/applications/manifest/test/refresh/')
application = Application.objects.get(slug='test')
element.refresh_from_db()
@ -1363,7 +1395,7 @@ def test_deploy_application_roles(app, admin_user, settings, app_bundle_roles):
resp = app.get('/applications/')
resp = resp.click('Install')
resp.form['bundle'] = Upload('app.tar', app_bundle_roles, 'application/x-tar')
with HTTMock(httmock.remember_called(mocked_http)):
with StatefulHTTMock(httmock.remember_called(mocked_http)):
resp = resp.form.submit().follow()
# roles
assert mocked_http.call['requests'][0].url.startswith(
@ -1401,7 +1433,7 @@ def test_deploy_application_roles(app, admin_user, settings, app_bundle_roles):
resp = app.get('/applications/install/')
resp.form['bundle'] = Upload('app.tar', app_bundle_roles, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(DeploymentError) as e:
resp.form.submit().follow()
assert str(e.value) == 'Failed to create role test-role (500)'
@ -1416,7 +1448,7 @@ def test_deploy_application_roles(app, admin_user, settings, app_bundle_roles):
resp = app.get('/applications/install/')
resp.form['bundle'] = Upload('app.tar', app_bundle_roles, 'application/x-tar')
with HTTMock(response_content):
with StatefulHTTMock(response_content):
with pytest.raises(DeploymentError) as e:
resp.form.submit().follow()
assert str(e.value) == 'Failed to provision role test-role (500)'
@ -1479,7 +1511,7 @@ def test_job_status_page(app, admin_user, settings):
}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = app.get('/applications/manifest/test/job/%s/' % job.pk)
assert 'Please wait…' in resp
assert 'Running: 42%' in resp
@ -1496,7 +1528,7 @@ def test_job_status_page(app, admin_user, settings):
}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = app.get('/applications/manifest/test/job/%s/' % job.pk)
assert 'Please wait…' in resp
assert 'Completed: 100%' in resp
@ -1524,7 +1556,7 @@ def test_job_status_page(app, admin_user, settings):
}
return mocked_http(url, request)
with HTTMock(response_content):
with StatefulHTTMock(response_content):
resp = app.get('/applications/manifest/test/job/%s/' % job.pk)
assert 'Error running the job.' in resp
assert 'Please wait…' not in resp
@ -1556,7 +1588,7 @@ def test_create_application_parameters(app, admin_user, settings):
resp.form['name'] = 'Test'
resp = resp.form.submit()
with HTTMock(mocked_http):
with StatefulHTTMock(mocked_http):
resp = resp.follow()
# add a form (an element is required to have the generate app button)