From 146fdcc5b3706b1ac6c8e85d1356d372e91862a2 Mon Sep 17 00:00:00 2001 From: Jan Kaluza Date: May 02 2017 06:42:45 +0000 Subject: Add database support and dummy Flask frontend --- diff --git a/conf/config.py b/conf/config.py index cff009e..89b7adc 100644 --- a/conf/config.py +++ b/conf/config.py @@ -9,6 +9,15 @@ dbdir = path.abspath(path.join(confdir, '..')) if confdir.endswith('conf') \ class BaseConfiguration(object): + # Make this random (used to generate session keys) + SECRET_KEY = '74d9e9f9cd40e66fc6c4c2e9987dce48df3ce98542529fd0' + SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(path.join( + dbdir, 'freshmaker.db')) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + HOST = '0.0.0.0' + PORT = 5001 + DEBUG = False # Global network-related values, in seconds NET_TIMEOUT = 120 @@ -63,6 +72,7 @@ class BaseConfiguration(object): # Settings for docker image rebuild handler KOJI_CONTAINER_SCRATCH_BUILD = False + SSL_ENABLED = False class DevConfiguration(BaseConfiguration): DEBUG = True @@ -82,6 +92,10 @@ class TestConfiguration(BaseConfiguration): LOG_BACKEND = 'console' LOG_LEVEL = 'debug' DEBUG = True + + SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format( + path.join(dbdir, 'tests', 'test_freshmaker.db')) + MESSAGING = 'in_memory' PDC_URL = 'http://pdc.fedoraproject.org/rest_api/v1' diff --git a/freshmaker/__init__.py b/freshmaker/__init__.py index 24a4bc9..1227bbb 100644 --- a/freshmaker/__init__.py +++ b/freshmaker/__init__.py @@ -24,9 +24,21 @@ # Jan Kaluza from logging import getLogger + +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + from freshmaker.logger import init_logging from freshmaker.config import init_config +from freshmaker.proxy import ReverseProxy + +app = Flask(__name__) +app.wsgi_app = ReverseProxy(app.wsgi_app) -conf = init_config() +db = SQLAlchemy(app) + +conf = init_config(app) init_logging(conf) log = getLogger(__name__) + +from freshmaker import views diff --git a/freshmaker/config.py b/freshmaker/config.py index eabb1c0..b504477 100644 --- a/freshmaker/config.py +++ b/freshmaker/config.py @@ -31,7 +31,7 @@ from os import sys from freshmaker import logger -def init_config(): +def init_config(app): """ Configure Freshmaker """ @@ -54,7 +54,7 @@ def init_config(): if 'FRESHMAKER_CONFIG_SECTION' in os.environ: config_section = os.environ['FRESHMAKER_CONFIG_SECTION'] # TestConfiguration shall only be used for running tests, otherwise... - if any(['py.test' in arg or 'pytest.py' in arg for arg in sys.argv]): + if any(['nosetests' in arg or 'noserunner.py' in arg or 'py.test' in arg or 'pytest.py' in arg for arg in sys.argv]): config_section = 'TestConfiguration' from conf import config config_module = config @@ -81,6 +81,7 @@ def init_config(): # finally configure Freshmaker config_section_obj = getattr(config_module, config_section) conf = Config(config_section_obj) + app.config.from_object(config_section_obj) return conf diff --git a/freshmaker/manage.py b/freshmaker/manage.py new file mode 100644 index 0000000..6b38a14 --- /dev/null +++ b/freshmaker/manage.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from flask_script import Manager +from functools import wraps +import flask_migrate +import logging +import os +import ssl + +import fedmsg.config +import moksha.hub +import moksha.hub.hub +import moksha.hub.reactor + +from freshmaker import app, conf, db +from freshmaker import models + + +manager = Manager(app) +help_args = ('-?', '--help') +manager.help_args = help_args +migrate = flask_migrate.Migrate(app, db) +manager.add_command('db', flask_migrate.MigrateCommand) + + +def console_script_help(f): + @wraps(f) + def wrapped(*args, **kwargs): + import sys + if any([arg in help_args for arg in sys.argv[1:]]): + command = os.path.basename(sys.argv[0]) + print("""{0} + +Usage: {0} [{1}] + +See also: + freshmaker-manager(1)""".format(command, + '|'.join(help_args))) + sys.exit(2) + r = f(*args, **kwargs) + return r + return wrapped + + +def _establish_ssl_context(): + if not conf.ssl_enabled: + return None + # First, do some validation of the configuration + attributes = ( + 'ssl_certificate_file', + 'ssl_certificate_key_file', + 'ssl_ca_certificate_file', + ) + + for attribute in attributes: + value = getattr(conf, attribute, None) + if not value: + raise ValueError("%r could not be found" % attribute) + if not os.path.exists(value): + raise OSError("%s: %s file not found." % (attribute, value)) + + # Then, establish the ssl context and return it + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + ssl_ctx.load_cert_chain(conf.ssl_certificate_file, + conf.ssl_certificate_key_file) + ssl_ctx.verify_mode = ssl.CERT_OPTIONAL + ssl_ctx.load_verify_locations(cafile=conf.ssl_ca_certificate_file) + return ssl_ctx + + +@console_script_help +@manager.command +def upgradedb(): + """ Upgrades the database schema to the latest revision + """ + app.config["SERVER_NAME"] = 'localhost' + migrations_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'migrations') + with app.app_context(): + flask_migrate.upgrade(directory=migrations_dir) + +@console_script_help +@manager.command +def cleardb(): + """ Clears the database + """ + models.Event.query.delete() + models.ArtifactBuild.query.delete() + db.session.commit() + +@manager.command +@console_script_help +def generatelocalhostcert(): + """ Creates a public/private key pair for message signing and the frontend + """ + from OpenSSL import crypto + cert_key = crypto.PKey() + cert_key.generate_key(crypto.TYPE_RSA, 2048) + + with open(conf.ssl_certificate_key_file, 'w') as cert_key_file: + os.chmod(conf.ssl_certificate_key_file, 0o600) + cert_key_file.write( + crypto.dump_privatekey(crypto.FILETYPE_PEM, cert_key)) + + cert = crypto.X509() + msg_cert_subject = cert.get_subject() + msg_cert_subject.C = 'US' + msg_cert_subject.ST = 'MA' + msg_cert_subject.L = 'Boston' + msg_cert_subject.O = 'Development' + msg_cert_subject.CN = 'localhost' + cert.set_serial_number(2) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(315360000) # 10 years + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(cert_key) + cert_extensions = [ + crypto.X509Extension( + 'keyUsage', True, + 'digitalSignature, keyEncipherment, nonRepudiation'), + crypto.X509Extension('extendedKeyUsage', True, 'serverAuth'), + ] + cert.add_extensions(cert_extensions) + cert.sign(cert_key, 'sha256') + + with open(conf.ssl_certificate_file, 'w') as cert_file: + cert_file.write( + crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + + +@console_script_help +@manager.command +def runssl(host=conf.host, port=conf.port, debug=conf.debug): + """ Runs the Flask app with the HTTPS settings configured in config.py + """ + logging.info('Starting Freshmaker frontend') + + ssl_ctx = _establish_ssl_context() + app.run( + host=host, + port=port, + ssl_context=ssl_ctx, + debug=debug + ) + +if __name__ == "__main__": + manager.run() diff --git a/freshmaker/migrations/README b/freshmaker/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/freshmaker/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/freshmaker/migrations/alembic.ini b/freshmaker/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/freshmaker/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/freshmaker/migrations/env.py b/freshmaker/migrations/env.py new file mode 100644 index 0000000..4593816 --- /dev/null +++ b/freshmaker/migrations/env.py @@ -0,0 +1,87 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.readthedocs.org/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/freshmaker/migrations/script.py.mako b/freshmaker/migrations/script.py.mako new file mode 100644 index 0000000..9570201 --- /dev/null +++ b/freshmaker/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/freshmaker/migrations/versions/1529069af28e_initial_db.py b/freshmaker/migrations/versions/1529069af28e_initial_db.py new file mode 100644 index 0000000..865b259 --- /dev/null +++ b/freshmaker/migrations/versions/1529069af28e_initial_db.py @@ -0,0 +1,41 @@ +"""Initial database + +Revision ID: 1529069af28e +Revises: None +Create Date: 2017-04-28 13:43:23.340055 + +""" + +# revision identifiers, used by Alembic. +revision = '1529069af28e' +down_revision = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('events', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('message_id', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('artifact_builds', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('type', sa.Integer(), nullable=True), + sa.Column('state', sa.Integer(), nullable=False), + sa.Column('time_submitted', sa.DateTime(), nullable=False), + sa.Column('time_completed', sa.DateTime(), nullable=True), + sa.Column('dep_of_id', sa.Integer(), nullable=True), + sa.Column('event_id', sa.Integer(), nullable=True), + sa.Column('build_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['dep_of_id'], ['artifact_builds.id'], ), + sa.ForeignKeyConstraint(['event_id'], ['events.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table('artifact_builds') + op.drop_table('events') diff --git a/freshmaker/models.py b/freshmaker/models.py new file mode 100644 index 0000000..cfbdc82 --- /dev/null +++ b/freshmaker/models.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Jan Kaluza + +""" SQLAlchemy Database models for the Flask app +""" + +import contextlib + +from datetime import datetime +from sqlalchemy import engine_from_config +from sqlalchemy.orm import (validates, scoped_session, sessionmaker, + relationship) +from sqlalchemy.ext.declarative import declarative_base + +from freshmaker import db, log + +# BUILD_STATES for the builds submitted by Freshmaker +BUILD_STATES = { + # Artifact is building. + "build": 0, + # Artifact build is sucessfuly done. + "done": 1, + # Artifact build has failed. + "failed": 2, + # Artifact build is canceled. + "canceled": 3, +} + +INVERSE_BUILD_STATES = {v: k for k, v in BUILD_STATES.items()} + +ARTIFACT_TYPES = { + "rpm": 0, + "image": 1, + "module": 2, + } + +INVERSE_ARTIFACT_TYPES = {v: k for k, v in ARTIFACT_TYPES.items()} + +class FreshmakerBase(db.Model): + __abstract__ = True + +class Event(FreshmakerBase): + __tablename__ = "events" + id = db.Column(db.Integer, primary_key=True) + message_id = db.Column(db.String, nullable=False) + + # List of builds associated with this Event. + builds = relationship("ArtifactBuild", back_populates="event") + + @classmethod + def create(cls, session, message_id): + event = cls( + message_id=message_id, + ) + session.add(event) + return event + +class ArtifactBuild(FreshmakerBase): + __tablename__ = "artifact_builds" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False) + type = db.Column(db.Integer) + state = db.Column(db.Integer, nullable=False) + time_submitted = db.Column(db.DateTime, nullable=False) + time_completed = db.Column(db.DateTime) + + # Link to the Artifact on which this one depends and which triggered + # the rebuild of this Artifact. + dep_of_id = db.Column(db.Integer, db.ForeignKey('artifact_builds.id')) + dep_of = relationship('ArtifactBuild', remote_side=[id]) + + # Event associated with this Build + event_id = db.Column(db.Integer, db.ForeignKey('events.id')) + event = relationship("Event", back_populates="builds") + + # Id of a build in the build system + build_id = db.Column(db.Integer) + + @classmethod + def create(cls, session, event, name, type, build_id, dep_of=None): + now = datetime.utcnow() + build = cls( + name=name, + type=type, + event=event, + state="build", + build_id=build_id, + time_submitted=now, + dep_of=dep_of + ) + session.add(build) + return build + + @validates('state') + def validate_state(self, key, field): + if field in BUILD_STATES.values(): + return field + if field in BUILD_STATES: + return BUILD_STATES[field] + raise ValueError("%s: %s, not in %r" % (key, field, BUILD_STATES)) + + @validates('type') + def validate_type(self, key, field): + if field in ARTIFACT_TYPES.values(): + return field + if field in ARTIFACT_TYPES: + return ARTIFACT_TYPES[field] + raise ValueError("%s: %s, not in %r" % (key, field, ARTIFACT_TYPES)) + + def __repr__(self): + return "" % ( + self.name, INVERSE_ARTIFACT_TYPES[self.type], + INVERSE_BUILD_STATES[self.state], self.event.message_id) + diff --git a/freshmaker/proxy.py b/freshmaker/proxy.py new file mode 100644 index 0000000..753f6a9 --- /dev/null +++ b/freshmaker/proxy.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Ralph Bean + +""" +Allows Freshmaker to run behind reverse proxy and to ensure redirects work +with https. + +WSGI Middleware!! + +Source: http://flask.pocoo.org/snippets/35/ by Peter Hansen +""" + + +class ReverseProxy(object): + '''Wrap the application in this middleware and configure the + front-end server to add these headers, to let you quietly bind + this to a URL other than / and to an HTTP scheme that is + different than what is used locally. + + :param app: the WSGI application + ''' + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + script_name = environ.get('HTTP_X_SCRIPT_NAME', '') + if script_name: + environ['SCRIPT_NAME'] = script_name + path_info = environ['PATH_INFO'] + if path_info.startswith(script_name): + environ['PATH_INFO'] = path_info[len(script_name):] + + server = environ.get('HTTP_X_FORWARDED_HOST', '') + if server: + environ['HTTP_HOST'] = server + + scheme = environ.get('HTTP_X_SCHEME', '') + if scheme: + environ['wsgi.url_scheme'] = scheme + return self.app(environ, start_response) diff --git a/freshmaker/views.py b/freshmaker/views.py new file mode 100644 index 0000000..4eed73e --- /dev/null +++ b/freshmaker/views.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Jan Kaluza + +import json +from flask import request, jsonify +from flask.views import MethodView + +from freshmaker import app, conf, log +from freshmaker import models, db + +api_v1 = { + 'freshmaker': { + 'url': '/freshmaker/1/events/', + 'options': { + 'defaults': {'id': None}, + 'methods': ['GET'], + } + }, +} + + +class FreshmakerAPI(MethodView): + + def get(self, id): + return "Done", 200 + +def register_api_v1(): + """ Registers version 1 of MBS API. """ + module_view = FreshmakerAPI.as_view('freshmaker') + for key, val in api_v1.items(): + app.add_url_rule(val['url'], + endpoint=key, + view_func=module_view, + **val['options']) + +register_api_v1() diff --git a/requirements.txt b/requirements.txt index 6901882..47f8305 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,7 @@ python-fedora six sqlalchemy futures # Python 2 only +Flask +Flask-Migrate +Flask-SQLAlchemy +Flask-Script diff --git a/start_backend_from_here b/start_backend_from_here new file mode 100755 index 0000000..0018c66 --- /dev/null +++ b/start_backend_from_here @@ -0,0 +1,12 @@ +#!/usr/bin/env python +__requires__ = 'fedmsg' +import sys, os +from pkg_resources import load_entry_point + +sys.path.append(os.path.join(os.path.dirname(__file__))) +os.environ["FRESHMAKER_DEVELOPER_ENV"] = "1" + +if __name__ == '__main__': + sys.exit( + load_entry_point('fedmsg', 'console_scripts', 'fedmsg-hub')() + ) diff --git a/start_from_here b/start_from_here deleted file mode 100755 index 0018c66..0000000 --- a/start_from_here +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python -__requires__ = 'fedmsg' -import sys, os -from pkg_resources import load_entry_point - -sys.path.append(os.path.join(os.path.dirname(__file__))) -os.environ["FRESHMAKER_DEVELOPER_ENV"] = "1" - -if __name__ == '__main__': - sys.exit( - load_entry_point('fedmsg', 'console_scripts', 'fedmsg-hub')() - ) diff --git a/start_frontend_from_here b/start_frontend_from_here new file mode 100755 index 0000000..73c74f6 --- /dev/null +++ b/start_frontend_from_here @@ -0,0 +1,3 @@ +#!/bin/bash + +FRESHMAKER_DEVELOPER_ENV=1 python freshmaker/manage.py runssl diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..e1d7e0f --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,62 @@ +# Copyright (c) 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Jan Kaluza + +import unittest + +from freshmaker import conf, db +from freshmaker.models import Event, ArtifactBuild + + +class TestModels(unittest.TestCase): + def setUp(self): + db.session.remove() + db.drop_all() + db.create_all() + db.session.commit() + + def tearDown(self): + db.session.remove() + db.drop_all() + db.session.commit() + + def test_creating_event_and_builds(self): + event = Event.create(db.session, "test_msg_id") + build = ArtifactBuild.create(db.session, event, "ed", "module", 1234) + build2 = ArtifactBuild.create(db.session, event, "mksh", "module", 1235, build) + db.session.commit() + db.session.expire_all() + + e = db.session.query(Event).filter(event.id == 1).one() + self.assertEquals(e.message_id, "test_msg_id") + self.assertEquals(len(e.builds), 2) + + self.assertEquals(e.builds[0].name, "ed") + self.assertEquals(e.builds[0].type, 2) + self.assertEquals(e.builds[0].state, 0) + self.assertEquals(e.builds[0].build_id, 1234) + self.assertEquals(e.builds[0].dep_of, None) + + self.assertEquals(e.builds[1].name, "mksh") + self.assertEquals(e.builds[1].type, 2) + self.assertEquals(e.builds[1].state, 0) + self.assertEquals(e.builds[1].build_id, 1235) + self.assertEquals(e.builds[1].dep_of.name, "ed")