Skip to content

Commit

Permalink
Allow user to attach issues to Job results
Browse files Browse the repository at this point in the history
In order to improve the overall quality of the product, when a job fails
due to a product issue, the bug should be reported and the issue
reported attached to the Job itself. This way DCI could be able to
report when the issue isn't there anymore for example, or be able to
tell how many remoteci/jobs are blocked due to a specific issue, etc...

Change-Id: I8bb1f993b1fe8a0367c7e6e01dd9ae1b43af8af9
  • Loading branch information
Spredzy committed Aug 1, 2016
1 parent adfcde6 commit 1dd56e7
Show file tree
Hide file tree
Showing 11 changed files with 688 additions and 0 deletions.
66 changes: 66 additions & 0 deletions dci/alembic/versions/f1940287976b_add_issue_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#
# Copyright (C) 2016 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""Add issue table
Revision ID: f1940287976b
Revises: 48c9af8ba5c3
Create Date: 2016-07-29 09:22:49.606519
"""

# revision identifiers, used by Alembic.
revision = 'f1940287976b'
down_revision = '48c9af8ba5c3'
branch_labels = None
depends_on = None

import datetime

from alembic import op
import sqlalchemy as sa

import dci.common.utils as utils


def upgrade():
trackers = sa.Enum('github', 'bugzilla', name='trackers')

op.create_table(
'issues',
sa.Column('id', sa.String(36), primary_key=True,
default=utils.gen_uuid),
sa.Column('created_at', sa.DateTime(),
default=datetime.datetime.utcnow, nullable=False),
sa.Column('updated_at', sa.DateTime(),
onupdate=datetime.datetime.utcnow,
default=datetime.datetime.utcnow, nullable=False),
sa.Column('url', sa.Text, unique=True),
sa.Column('tracker', trackers, nullable=False)
)

op.create_table(
'jobs_issues',
sa.Column('job_id', sa.String(36),
sa.ForeignKey('jobs.id', ondelete="CASCADE"),
nullable=False, primary_key=True),
sa.Column('issue_id', sa.String(36),
sa.ForeignKey('issues.id', ondelete="CASCADE"),
nullable=False, primary_key=True)
)


def downgrade():
pass
121 changes: 121 additions & 0 deletions dci/api/v1/issues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-2016 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import datetime
import flask

from flask import json
from sqlalchemy import sql
from sqlalchemy import exc as sa_exc
from dci.api.v1 import utils as v1_utils
from dci.common import exceptions as dci_exc
from dci.common import schemas
from dci.common import utils
from dci.db import models
from dci.trackers import github
from dci.trackers import bugzilla


_TABLE = models.ISSUES


def get_all_issues(job_id):
"""Get all issues for a specific job."""

v1_utils.verify_existence_and_get(job_id, models.JOBS)

JJI = models.JOIN_JOBS_ISSUES

query = (sql.select([_TABLE])
.select_from(JJI.join(_TABLE))
.where(JJI.c.job_id == job_id))
rows = flask.g.db_conn.execute(query)
rows = [dict(row) for row in rows]

for row in rows:
if row['tracker'] == 'github':
l_tracker = github.Github(row['url'])
elif row['tracker'] == 'bugzilla':
l_tracker = bugzilla.Bugzilla(row['url'])
row.update(l_tracker.dump())

return flask.jsonify({'issues': rows,
'_meta': {'count': len(rows)}})


def unattach_issue(job_id, issue_id):
"""Unattach an issue from a specific job."""

v1_utils.verify_existence_and_get(issue_id, _TABLE)
JJI = models.JOIN_JOBS_ISSUES
where_clause = sql.and_(JJI.c.job_id == job_id,
JJI.c.issue_id == issue_id)
query = JJI.delete().where(where_clause)
result = flask.g.db_conn.execute(query)

if not result.rowcount:
raise dci_exc.DCIConflict('Jobs_issues', issue_id)

return flask.Response(None, 204, content_type='application/json')


def attach_issue(job_id):
"""Attach an issue to a specific job."""

values = schemas.issue.post(flask.request.json)

if 'github.com' in values['url']:
type = 'github'
else:
type = 'bugzilla'

issue_id = utils.gen_uuid()
values.update({
'id': issue_id,
'created_at': datetime.datetime.utcnow().isoformat(),
'tracker': type,
})

# First, insert the issue if it doesn't already exist
# in the issues table. If it already exists, ignore the
# exceptions, and keep proceeding.
query = _TABLE.insert().values(**values)
try:
flask.g.db_conn.execute(query)
except sa_exc.IntegrityError:
# It is not a real failure it the issue have been tried
# to inserted a second time. As long as it is once, we are
# good to proceed
query = (sql.select([_TABLE])
.where(_TABLE.c.url == values['url']))
rows = list(flask.g.db_conn.execute(query))
issue_id = rows[0][0] # the 'id' field of the issues table.

# Second, insert a join record in the JOIN_JOBS_ISSUES
# database.
values = {
'job_id': job_id,
'issue_id': issue_id
}
query = models.JOIN_JOBS_ISSUES.insert().values(**values)
try:
flask.g.db_conn.execute(query)
except sa_exc.IntegrityError:
raise dci_exc.DCICreationConflict(models.JOIN_JOBS_ISSUES.name,
'job_id, issue_id')

result = json.dumps(values)
return flask.Response(result, 201, content_type='application/json')
25 changes: 25 additions & 0 deletions dci/api/v1/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from dci.db import models

from dci.api.v1 import files
from dci.api.v1 import issues
from dci.api.v1 import jobstates
from dci import dci_config

Expand Down Expand Up @@ -365,6 +366,9 @@ def get_job_by_id(user, jd_id):
raise dci_exc.DCINotFound('Job', jd_id)

job = v1_utils.group_embedded_resources(embed, row)
job['issues'] = (
json.loads(issues.get_all_issues(jd_id).response[0])['issues']
)
res = flask.jsonify({'job': job})
res.headers.add_header('ETag', job['etag'])
return res
Expand Down Expand Up @@ -459,6 +463,27 @@ def add_file_to_jobs(user, j_id):
return files.create_files(user, values)


@api.route('/jobs/<j_id>/issues', methods=['GET'])
@auth.requires_auth
def retrieve_issues_from_job(user, j_id):
"""Retrieve all issues attached to a job."""
return issues.get_all_issues(j_id)


@api.route('/jobs/<j_id>/issues', methods=['POST'])
@auth.requires_auth
def attach_issue_to_jobs(user, j_id):
"""Attach an issue to a job."""
return issues.attach_issue(j_id)


@api.route('/jobs/<j_id>/issues/<i_id>', methods=['DELETE'])
@auth.requires_auth
def unattach_issue_from_job(user, j_id, i_id):
"""Unattach an issue to a job."""
return issues.unattach_issue(j_id, i_id)


@api.route('/jobs/<j_id>/files', methods=['GET'])
@auth.requires_auth
def get_all_files_from_jobs(user, j_id):
Expand Down
12 changes: 12 additions & 0 deletions dci/common/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,15 @@ def schema_factory(schema):
}

audit = schema_factory(audit)

###############################################################################
# #
# Issues schemas #
# #
###############################################################################

issue = {
'url': six.text_type,
}

issue = schema_factory(issue)
23 changes: 23 additions & 0 deletions dci/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
'deployment-failure']
STATUSES = sa.Enum(*JOB_STATUSES, name='statuses')

ISSUE_TRACKERS = ['github', 'bugzilla']
TRACKERS = sa.Enum(*ISSUE_TRACKERS, name='trackers')

COMPONENTS = sa.Table(
'components', metadata,
sa.Column('id', sa.String(36), primary_key=True,
Expand Down Expand Up @@ -191,6 +194,14 @@
sa.ForeignKey('components.id', ondelete='CASCADE'),
nullable=False, primary_key=True))

JOIN_JOBS_ISSUES = sa.Table(
'jobs_issues', metadata,
sa.Column('job_id', sa.String(36),
sa.ForeignKey('jobs.id', ondelete='CASCADE'),
nullable=False, primary_key=True),
sa.Column('issue_id', sa.String(36),
sa.ForeignKey('issues.id', ondelete='CASCADE'),
nullable=False, primary_key=True))

JOBSTATES = sa.Table(
'jobstates', metadata,
Expand Down Expand Up @@ -268,3 +279,15 @@
sa.ForeignKey('teams.id', ondelete='CASCADE'),
nullable=False),
sa.Column('action', sa.Text, nullable=False))

ISSUES = sa.Table(
'issues', metadata,
sa.Column('id', sa.String(36), primary_key=True,
default=utils.gen_uuid),
sa.Column('created_at', sa.DateTime(),
default=datetime.datetime.utcnow, nullable=False),
sa.Column('updated_at', sa.DateTime(),
onupdate=datetime.datetime.utcnow,
default=datetime.datetime.utcnow, nullable=False),
sa.Column('url', sa.Text, unique=True),
sa.Column('tracker', TRACKERS, nullable=False))
54 changes: 54 additions & 0 deletions dci/trackers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.


class Tracker(object):

def __init__(self, url):
self.url = url
self.status_code = None
self.title = None
self.issue_id = None
self.reporter = None
self.assignee = None
self.status = None
self.product = None
self.component = None
self.created_at = None
self.updated_at = None
self.closed_at = None
self.retrieve_info()

def retrieve_info(self):
"""Retrieve informations for a specific issue in a tracker."""
raise Exception('Not Implemented')

def dump(self):
"""Return the object itself."""

return {
'title': self.title,
'issue_id': self.issue_id,
'reporter': self.reporter,
'assignee': self.assignee,
'status': self.status,
'product': self.product,
'component': self.component,
'created_at': self.created_at,
'updated_at': self.updated_at,
'closed_at': self.closed_at,
'status_code': self.status_code
}
Loading

0 comments on commit 1dd56e7

Please sign in to comment.