Merge "Add image validation admin method"
This commit is contained in:
@@ -1743,6 +1743,85 @@ class TestWebProviders(LauncherBaseTestCase, WebMixin):
|
||||
if 'build_artifacts' not in data[1]:
|
||||
break
|
||||
|
||||
@simple_layout('layouts/nodepool-image-validate.yaml',
|
||||
enable_nodepool=True)
|
||||
@return_data(
|
||||
'build-debian-local-image',
|
||||
'refs/heads/master',
|
||||
LauncherBaseTestCase.debian_return_data,
|
||||
)
|
||||
@mock.patch('zuul.driver.aws.awsendpoint.AwsImageUploadJob.run',
|
||||
return_value="ami-785db401")
|
||||
def test_web_upload_validate(self, mock_image_upload_run):
|
||||
self.executor_server.hold_jobs_in_build = True
|
||||
self.startWebServer()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.executor_server.release('build-debian-local-image')
|
||||
self.waitUntilSettled()
|
||||
self.assertHistory([
|
||||
dict(name='build-debian-local-image', result='SUCCESS'),
|
||||
])
|
||||
|
||||
nodes = self.launcher.api.getProviderNodes()
|
||||
self.assertEqual(len(nodes), 2)
|
||||
for node in nodes:
|
||||
self.assertEqual(node.create_state['image_external_id'],
|
||||
mock_image_upload_run.return_value)
|
||||
|
||||
self.assertEqual(len(self.builds), 2)
|
||||
for build in self.builds:
|
||||
build.should_fail = True
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertHistory([
|
||||
dict(name='build-debian-local-image', result='SUCCESS'),
|
||||
dict(name='validate-debian-local-image', result='FAILURE'),
|
||||
dict(name='validate-debian-local-image', result='FAILURE'),
|
||||
])
|
||||
|
||||
resp = self.get_url('api/tenant/tenant-one/images')
|
||||
data = resp.json()
|
||||
self.assertEqual(1, len(data))
|
||||
self.assertEqual(1, len(data[0]['build_artifacts']))
|
||||
self.assertEqual(2, len(data[0]['build_artifacts'][0]['uploads']))
|
||||
upload = data[0]['build_artifacts'][0]['uploads'][0]
|
||||
self.assertFalse(upload['validated'])
|
||||
|
||||
# Now retrigger validation
|
||||
url = f"api/tenant/tenant-one/image-upload/{upload['uuid']}/validate"
|
||||
# Test that unauthenticated access fails
|
||||
resp = self.post_url(url)
|
||||
self.assertEqual(401, resp.status_code, resp.text)
|
||||
# Do it again with auth
|
||||
token = self._getToken(['tenant-one'])
|
||||
resp = self.post_url(
|
||||
url,
|
||||
headers={'Authorization': 'Bearer %s' % token})
|
||||
self.assertEqual(200, resp.status_code, resp.text)
|
||||
self.waitUntilSettled("image validate")
|
||||
self.assertHistory([
|
||||
dict(name='build-debian-local-image', result='SUCCESS'),
|
||||
dict(name='validate-debian-local-image', result='FAILURE'),
|
||||
dict(name='validate-debian-local-image', result='FAILURE'),
|
||||
dict(name='validate-debian-local-image', result='SUCCESS'),
|
||||
])
|
||||
|
||||
resp = self.get_url('api/tenant/tenant-one/images')
|
||||
data = resp.json()
|
||||
self.assertEqual(1, len(data))
|
||||
self.assertEqual(1, len(data[0]['build_artifacts']))
|
||||
self.assertEqual(2, len(data[0]['build_artifacts'][0]['uploads']))
|
||||
for new_upload in data[0]['build_artifacts'][0]['uploads']:
|
||||
if new_upload['uuid'] == upload['uuid']:
|
||||
break
|
||||
else:
|
||||
raise Exception("Upload not found")
|
||||
self.assertTrue(new_upload['validated'])
|
||||
|
||||
@return_data(
|
||||
'build-debian-local-image',
|
||||
'refs/heads/master',
|
||||
|
||||
@@ -262,6 +262,13 @@ function deleteImageUpload(apiPrefix, uploadId) {
|
||||
)
|
||||
}
|
||||
|
||||
function validateImageUpload(apiPrefix, uploadId) {
|
||||
return makeRequest(
|
||||
apiPrefix + 'image-upload/' + uploadId + '/validate',
|
||||
'post'
|
||||
)
|
||||
}
|
||||
|
||||
function fetchFlavors(apiPrefix) {
|
||||
return makeRequest(apiPrefix + 'flavors')
|
||||
}
|
||||
@@ -471,4 +478,5 @@ export {
|
||||
promote,
|
||||
setNodeState,
|
||||
setTenantState,
|
||||
validateImageUpload,
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { fetchImages } from '../../actions/images'
|
||||
import { fetchProviders } from '../../actions/providers'
|
||||
import { addNotification, addApiError } from '../../actions/notifications'
|
||||
import { deleteImageUpload } from '../../api'
|
||||
import { deleteImageUpload, validateImageUpload } from '../../api'
|
||||
|
||||
const STATE_STYLES = {
|
||||
ready: {
|
||||
@@ -54,7 +54,8 @@ const STATE_STYLES = {
|
||||
function ImageUploadTable(props) {
|
||||
const { build, uploads, fetching } = props
|
||||
const [showDeleteUploadModal, setShowDeleteUploadModal] = useState(false)
|
||||
const [pendingDeleteRow, setPendingDeleteRow] = useState(null)
|
||||
const [showValidateUploadModal, setShowValidateUploadModal] = useState(false)
|
||||
const [pendingActionRow, setPendingActionRow] = useState(null)
|
||||
const tenant = useSelector((state) => state.tenant)
|
||||
const user = useSelector((state) => state.user)
|
||||
const dispatch = useDispatch()
|
||||
@@ -94,7 +95,7 @@ function ImageUploadTable(props) {
|
||||
const state_style = STATE_STYLES[upload.state] || {}
|
||||
return {
|
||||
_uuid: upload.uuid,
|
||||
canDelete: build.build_tenant,
|
||||
canModify: build.build_tenant,
|
||||
cells: [
|
||||
{
|
||||
title: upload.uuid
|
||||
@@ -141,17 +142,24 @@ function ImageUploadTable(props) {
|
||||
}
|
||||
|
||||
const actionResolver = (rowData) => {
|
||||
if (rowData.canDelete &&
|
||||
if (rowData.canModify &&
|
||||
user.isAdmin &&
|
||||
user.scope.indexOf(tenant.name) !== -1) {
|
||||
return [
|
||||
{
|
||||
title: 'Delete upload',
|
||||
onClick: () => {
|
||||
setPendingDeleteRow(rowData)
|
||||
setPendingActionRow(rowData)
|
||||
setShowDeleteUploadModal(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Validate upload',
|
||||
onClick: () => {
|
||||
setPendingActionRow(rowData)
|
||||
setShowValidateUploadModal(true)
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
@@ -170,7 +178,7 @@ function ImageUploadTable(props) {
|
||||
onClick={() => {
|
||||
setShowDeleteUploadModal(false)
|
||||
deleteImageUpload(tenant.apiPrefix,
|
||||
pendingDeleteRow._uuid
|
||||
pendingActionRow._uuid
|
||||
).then(() => {
|
||||
dispatch(addNotification(
|
||||
{
|
||||
@@ -200,6 +208,49 @@ function ImageUploadTable(props) {
|
||||
)
|
||||
}
|
||||
|
||||
function renderValidateUploadModal() {
|
||||
const title = 'Validate image upload'
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
isOpen={showValidateUploadModal}
|
||||
title={title}
|
||||
onClose={() => { setShowValidateUploadModal(false) }}
|
||||
actions={[
|
||||
<Button key="confirm" variant="primary"
|
||||
onClick={() => {
|
||||
setShowValidateUploadModal(false)
|
||||
validateImageUpload(tenant.apiPrefix,
|
||||
pendingActionRow._uuid
|
||||
).then(() => {
|
||||
dispatch(addNotification(
|
||||
{
|
||||
text: 'Validation event submitted.',
|
||||
type: 'success',
|
||||
status: '',
|
||||
url: '',
|
||||
}))
|
||||
dispatch(fetchProviders(tenant))
|
||||
dispatch(fetchImages(tenant))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(addApiError(error))
|
||||
})
|
||||
}}>
|
||||
Confirm
|
||||
</Button>,
|
||||
<Button key="cancel" variant="link"
|
||||
onClick={() => {setShowValidateUploadModal(false) }}>
|
||||
Cancel
|
||||
</Button>,
|
||||
]}>
|
||||
<p>
|
||||
Please confirm that you want to validate this image upload.
|
||||
</p>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const haveUploads = uploads && uploads.length > 0
|
||||
|
||||
let rows = []
|
||||
@@ -239,6 +290,7 @@ function ImageUploadTable(props) {
|
||||
</EmptyState>
|
||||
)}
|
||||
{renderDeleteUploadModal()}
|
||||
{renderValidateUploadModal()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2407,6 +2407,34 @@ class ZuulWebAPI(object):
|
||||
upload.state = model.STATE_DELETING
|
||||
cherrypy.response.status = 204
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
@cherrypy.tools.handle_options(allowed_methods=['POST'])
|
||||
@cherrypy.tools.check_tenant_auth(require_admin=True)
|
||||
def image_upload_validate(self, tenant_name, tenant, auth, upload_id):
|
||||
upload = self.zuulweb.image_upload_registry.getItem(upload_id)
|
||||
if upload:
|
||||
iba = self.zuulweb.image_build_registry.getItem(
|
||||
upload.artifact_uuid)
|
||||
else:
|
||||
iba = None
|
||||
|
||||
if not iba or tenant.name != iba.build_tenant_name:
|
||||
raise cherrypy.HTTPError(
|
||||
404, "Image upload not found in tenant")
|
||||
|
||||
self.log.info(f'User {auth.uid} requesting image-upload-validate on '
|
||||
f'{upload_id}')
|
||||
|
||||
project_hostname, project_name = \
|
||||
iba.project_canonical_name.split('/', 1)
|
||||
driver = self.zuulweb.connections.drivers['zuul']
|
||||
event = driver.getImageValidateEvent(
|
||||
[iba.name], project_hostname, project_name, iba.project_branch,
|
||||
upload.uuid)
|
||||
self.zuulweb.trigger_events[tenant_name].put(
|
||||
event.trigger_name, event)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.save_params()
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
@@ -3246,6 +3274,12 @@ class ZuulWeb(object):
|
||||
controller=api,
|
||||
conditions=dict(method=['DELETE', 'OPTIONS']),
|
||||
action='image_upload_delete')
|
||||
route_map.connect('api',
|
||||
'/api/tenant/{tenant_name}/'
|
||||
'image-upload/{upload_id}/validate',
|
||||
controller=api,
|
||||
conditions=dict(method=['POST', 'OPTIONS']),
|
||||
action='image_upload_validate')
|
||||
route_map.connect('api', '/api/tenant/{tenant_name}/labels',
|
||||
controller=api, action='labels')
|
||||
route_map.connect('api', '/api/tenant/{tenant_name}/nodes',
|
||||
|
||||
Reference in New Issue
Block a user