diff --git a/Dockerfile b/Dockerfile index 7ba5e7501..d1307b102 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,7 @@ RUN mkdir /usr/local/nginx/logs/ # Run the tests RUN TEST=true venv/bin/python -m unittest discover -f +RUN TEST=true venv/bin/python -m test.registry_tests -f VOLUME ["/conf/stack", "/var/log", "/datastorage", "/tmp", "/conf/etcd"] diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 000000000..d94d81588 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,38 @@ +# Quay.io Roadmap + +**work in progress** + +### Short Term +- Framework for microservice decomposition +- Improve documentation + - Ability to answer 80% of tickets with a link to the docs + - Eliminate old UI screenshots/references +- Auth provider as a service + - Registry v2 compatible + +### Medium Term +- Registry v2 support + - Forward and backward compatible with registry v1 +- Support ACI push spec + - Translate between ACI and docker images transparently +- Integrate docs with the search bar + - Full text search? +- Running on top of Tectonic +- BitTorrent distribution support +- Fully launch our API + - Versioned and backward compatible + - Adequate documentation + +### Long Term +- Become the Tectonic app store + - Pods/apps as top level concept +- Builds as top level concept + - Multiple Quay.io repos from a single git push + - Multi-step builds + - build artifact + - bundle artifact + - test bundle +- Immediately consistent multi-region data availability + - Cockroach? +- 2 factor auth + - How to integrate with Docker CLI? \ No newline at end of file diff --git a/buildman/component/buildcomponent.py b/buildman/component/buildcomponent.py index f5703bb65..a59cb421a 100644 --- a/buildman/component/buildcomponent.py +++ b/buildman/component/buildcomponent.py @@ -55,6 +55,7 @@ class BuildComponent(BaseComponent): def onConnect(self): self.join(self.builder_realm) + @trollius.coroutine def onJoin(self, details): logger.debug('Registering methods and listeners for component %s', self.builder_realm) yield trollius.From(self.register(self._on_ready, u'io.quay.buildworker.ready')) @@ -277,6 +278,9 @@ class BuildComponent(BaseComponent): # Send the notification that the build has completed successfully. self._current_job.send_notification('build_success', image_id=kwargs.get('image_id')) except ApplicationError as aex: + build_id = self._current_job.repo_build.uuid + logger.exception('Got remote exception for build: %s', build_id) + worker_error = WorkerError(aex.error, aex.kwargs.get('base_error')) # Write the error to the log. @@ -310,6 +314,7 @@ class BuildComponent(BaseComponent): @trollius.coroutine def _on_ready(self, token, version): + logger.debug('On ready called (token "%s")', token) self._worker_version = version if not version in SUPPORTED_WORKER_VERSIONS: @@ -343,6 +348,10 @@ class BuildComponent(BaseComponent): def _on_heartbeat(self): """ Updates the last known heartbeat. """ + if not self._current_job or self._component_status == ComponentStatus.TIMED_OUT: + return + + logger.debug('Got heartbeat for build %s', self._current_job.repo_build.uuid) self._last_heartbeat = datetime.datetime.utcnow() @trollius.coroutine @@ -374,9 +383,15 @@ class BuildComponent(BaseComponent): logger.debug('Checking heartbeat on realm %s', self.builder_realm) if (self._last_heartbeat and self._last_heartbeat < datetime.datetime.utcnow() - HEARTBEAT_DELTA): + logger.debug('Heartbeat on realm %s has expired: %s', self.builder_realm, + self._last_heartbeat) + yield trollius.From(self._timeout()) raise trollius.Return() + logger.debug('Heartbeat on realm %s is valid: %s.', self.builder_realm, + self._last_heartbeat) + yield trollius.From(trollius.sleep(HEARTBEAT_TIMEOUT)) @trollius.coroutine diff --git a/buildman/templates/cloudconfig.yaml b/buildman/templates/cloudconfig.yaml index 24b7bafe7..053f1f617 100644 --- a/buildman/templates/cloudconfig.yaml +++ b/buildman/templates/cloudconfig.yaml @@ -47,6 +47,7 @@ coreos: {{ dockersystemd('builder-logs', 'quay.io/kelseyhightower/journal-2-logentries', extra_args='--env-file /root/overrides.list -v /run/journald.sock:/run/journald.sock', + flattened=True, after_units=['quay-builder.service'] ) | indent(4) }} {%- endif %} diff --git a/conf/nginx.conf b/conf/nginx.conf index 77a78f70e..5e49b1977 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -7,18 +7,26 @@ http { include hosted-http-base.conf; include rate-limiting.conf; + ssl_certificate ./stack/ssl.cert; + ssl_certificate_key ./stack/ssl.key; + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + ssl_stapling on; + ssl_stapling_verify on; + ssl_prefer_server_ciphers on; + server { include server-base.conf; listen 443 default; ssl on; - ssl_certificate ./stack/ssl.cert; - ssl_certificate_key ./stack/ssl.key; - ssl_session_timeout 5m; - ssl_protocols SSLv3 TLSv1; - ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; - ssl_prefer_server_ciphers on; + + # This header must be set only for HTTPS + add_header Strict-Transport-Security "max-age=63072000; preload"; + } server { @@ -28,11 +36,8 @@ http { listen 8443 default proxy_protocol; ssl on; - ssl_certificate ./stack/ssl.cert; - ssl_certificate_key ./stack/ssl.key; - ssl_session_timeout 5m; - ssl_protocols SSLv3 TLSv1; - ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; - ssl_prefer_server_ciphers on; + + # This header must be set only for HTTPS + add_header Strict-Transport-Security "max-age=63072000; preload"; } } diff --git a/conf/server-base.conf b/conf/server-base.conf index 3853fbccf..bfa6c012f 100644 --- a/conf/server-base.conf +++ b/conf/server-base.conf @@ -8,6 +8,11 @@ if ($args ~ "_escaped_fragment_") { rewrite ^ /snapshot$uri; } +# Disable the ability to be embedded into iframes +add_header X-Frame-Options DENY; + + +# Proxy Headers proxy_set_header X-Forwarded-For $proper_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; diff --git a/data/model/legacy.py b/data/model/legacy.py index 67daaa540..464131f55 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -566,6 +566,12 @@ def list_federated_logins(user): FederatedLogin.user == user) +def lookup_federated_login(user, service_name): + try: + return list_federated_logins(user).where(LoginService.name == service_name).get() + except FederatedLogin.DoesNotExist: + return None + def create_confirm_email_code(user, new_email=None): if new_email: if not validate_email(new_email): @@ -636,6 +642,13 @@ def find_user_by_email(email): return None +def get_nonrobot_user(username): + try: + return User.get(User.username == username, User.organization == False, User.robot == False) + except User.DoesNotExist: + return None + + def get_user(username): try: return User.get(User.username == username, User.organization == False) diff --git a/data/users.py b/data/users.py index a50d462b9..f528694a3 100644 --- a/data/users.py +++ b/data/users.py @@ -9,6 +9,7 @@ import os from util.aes import AESCipher from util.validation import generate_valid_usernames from data import model +from collections import namedtuple logger = logging.getLogger(__name__) if os.environ.get('LDAP_DEBUG') == '1': @@ -28,6 +29,8 @@ class DatabaseUsers(object): return (result, None) + def confirm_existing_user(self, username, password): + return self.verify_user(username, password) def user_exists(self, username): return model.get_user(username) is not None @@ -43,6 +46,7 @@ class LDAPConnection(object): def __enter__(self): trace_level = 2 if os.environ.get('LDAP_DEBUG') == '1' else 0 self._conn = ldap.initialize(self._ldap_uri, trace_level=trace_level) + self._conn.set_option(ldap.OPT_REFERRALS, 1) self._conn.simple_bind_s(self._user_dn, self._user_pw) return self._conn @@ -52,6 +56,8 @@ class LDAPConnection(object): class LDAPUsers(object): + _LDAPResult = namedtuple('LDAPResult', ['dn', 'attrs']) + def __init__(self, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr): self._ldap_conn = LDAPConnection(ldap_uri, admin_dn, admin_passwd) self._ldap_uri = ldap_uri @@ -60,6 +66,25 @@ class LDAPUsers(object): self._uid_attr = uid_attr self._email_attr = email_attr + def _get_ldap_referral_dn(self, referral_exception): + logger.debug('Got referral: %s', referral_exception.args[0]) + if not referral_exception.args[0] or not referral_exception.args[0].get('info'): + logger.debug('LDAP referral missing info block') + return None + + referral_info = referral_exception.args[0]['info'] + if not referral_info.startswith('Referral:\n'): + logger.debug('LDAP referral missing Referral header') + return None + + referral_uri = referral_info[len('Referral:\n'):] + if not referral_uri.startswith('ldap:///'): + logger.debug('LDAP referral URI does not start with ldap:///') + return None + + referral_dn = referral_uri[len('ldap:///'):] + return referral_dn + def _ldap_user_search(self, username_or_email): with self._ldap_conn as conn: logger.debug('Incoming username or email param: %s', username_or_email.__repr__()) @@ -70,22 +95,56 @@ class LDAPUsers(object): logger.debug('Conducting user search: %s under %s', query, user_search_dn) try: pairs = conn.search_s(user_search_dn, ldap.SCOPE_SUBTREE, query.encode('utf-8')) + except ldap.REFERRAL as re: + referral_dn = self._get_ldap_referral_dn(re) + if not referral_dn: + return None + + try: + subquery = u'(%s=%s)' % (self._uid_attr, username_or_email) + pairs = conn.search_s(referral_dn, ldap.SCOPE_BASE, subquery) + except ldap.LDAPError: + logger.exception('LDAP referral search exception') + return None + except ldap.LDAPError: logger.exception('LDAP search exception') return None logger.debug('Found matching pairs: %s', pairs) - if len(pairs) < 1: + + results = [LDAPUsers._LDAPResult(*pair) for pair in pairs] + + # Filter out pairs without DNs. Some LDAP impls will return such + # pairs. + with_dns = [result for result in results if result.dn] + if len(with_dns) < 1: return None - for pair in pairs: - if pair[0] is not None: - logger.debug('Found user: %s', pair) - return pair + # If we have found a single pair, then return it. + if len(with_dns) == 1: + return with_dns[0] - return None + # Otherwise, there are multiple pairs with DNs, so find the one with the mail + # attribute (if any). + with_mail = [result for result in results if result.attrs.get(self._email_attr)] + return with_mail[0] if with_mail else with_dns[0] - def verify_user(self, username_or_email, password): + def confirm_existing_user(self, username, password): + """ Verify the username and password by looking up the *LDAP* username and confirming the + password. + """ + db_user = model.get_user(username) + if not db_user: + return (None, 'Invalid user') + + federated_login = model.lookup_federated_login(db_user, 'ldap') + if not federated_login: + return (None, 'Invalid user') + + return self.verify_user(federated_login.service_ident, password, create_new_user=False) + + def verify_user(self, username_or_email, password, create_new_user=True): """ Verify the credentials with LDAP and if they are valid, create or update the user in our database. """ @@ -94,17 +153,29 @@ class LDAPUsers(object): return (None, 'Anonymous binding not allowed') found_user = self._ldap_user_search(username_or_email) - if found_user is None: return (None, 'Username not found') found_dn, found_response = found_user + logger.debug('Found user for LDAP username %s; validating password', username_or_email) + logger.debug('DN %s found: %s', found_dn, found_response) # First validate the password by binding as the user - logger.debug('Found user %s; validating password', username_or_email) try: with LDAPConnection(self._ldap_uri, found_dn, password.encode('utf-8')): pass + except ldap.REFERRAL as re: + referral_dn = self._get_ldap_referral_dn(re) + if not referral_dn: + return (None, 'Invalid username') + + try: + with LDAPConnection(self._ldap_uri, referral_dn, password.encode('utf-8')): + pass + except ldap.INVALID_CREDENTIALS: + logger.exception('Invalid LDAP credentials') + return (None, 'Invalid password') + except ldap.INVALID_CREDENTIALS: logger.exception('Invalid LDAP credentials') return (None, 'Invalid password') @@ -121,6 +192,9 @@ class LDAPUsers(object): db_user = model.verify_federated_login('ldap', username) if not db_user: + if not create_new_user: + return (None, 'Invalid user') + # We must create the user in our db valid_username = None for valid_username in generate_valid_usernames(username): @@ -232,6 +306,13 @@ class UserAuthentication(object): return data.get('password', encrypted) + def confirm_existing_user(self, username, password): + """ Verifies that the given password matches to the given DB username. Unlike verify_user, this + call first translates the DB user via the FederatedLogin table (where applicable). + """ + return self.state.confirm_existing_user(username, password) + + def verify_user(self, username_or_email, password, basic_auth=False): # First try to decode the password as a signed token. if basic_auth: diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index c1406dcb8..947b2a8db 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -238,8 +238,8 @@ class SuperUserSendRecoveryEmail(ApiResource): @nickname('sendInstallUserRecoveryEmail') def post(self, username): if SuperUserPermission().can(): - user = model.get_user(username) - if not user or user.organization or user.robot: + user = model.get_nonrobot_user(username) + if not user: abort(404) if superusers.is_superuser(username): @@ -288,8 +288,8 @@ class SuperUserManagement(ApiResource): def get(self, username): """ Returns information about the specified user. """ if SuperUserPermission().can(): - user = model.get_user(username) - if not user or user.organization or user.robot: + user = model.get_nonrobot_user(username) + if not user: abort(404) return user_view(user) @@ -302,8 +302,8 @@ class SuperUserManagement(ApiResource): def delete(self, username): """ Deletes the specified user. """ if SuperUserPermission().can(): - user = model.get_user(username) - if not user or user.organization or user.robot: + user = model.get_nonrobot_user(username) + if not user: abort(404) if superusers.is_superuser(username): @@ -321,8 +321,8 @@ class SuperUserManagement(ApiResource): def put(self, username): """ Updates information about the specified user. """ if SuperUserPermission().can(): - user = model.get_user(username) - if not user or user.organization or user.robot: + user = model.get_nonrobot_user(username) + if not user: abort(404) if superusers.is_superuser(username): diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 92199ec68..6a0567963 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -283,7 +283,7 @@ class User(ApiResource): user_data = request.get_json() invite_code = user_data.get('invite_code', '') - existing_user = model.get_user(user_data['username']) + existing_user = model.get_nonrobot_user(user_data['username']) if existing_user: raise request_error(message='The username already exists') @@ -372,8 +372,7 @@ class ClientKey(ApiResource): """ Return's the user's private client key. """ username = get_authenticated_user().username password = request.get_json()['password'] - - (result, error_message) = authentication.verify_user(username, password) + (result, error_message) = authentication.confirm_existing_user(username, password) if not result: raise request_error(message=error_message) @@ -541,7 +540,17 @@ class VerifyUser(ApiResource): """ Verifies the signed in the user with the specified credentials. """ signin_data = request.get_json() password = signin_data['password'] - return conduct_signin(get_authenticated_user().username, password) + + username = get_authenticated_user().username + (result, error_message) = authentication.confirm_existing_user(username, password) + if not result: + return { + 'message': error_message, + 'invalidCredentials': True, + }, 403 + + common_login(result) + return {'success': True} @resource('/v1/signout') @@ -815,8 +824,8 @@ class Users(ApiResource): @nickname('getUserInformation') def get(self, username): """ Get user information for the specified user. """ - user = model.get_user(username) - if user is None or user.organization or user.robot: + user = model.get_nonrobot_user(username) + if user is None: abort(404) return user_view(user) diff --git a/endpoints/notificationmethod.py b/endpoints/notificationmethod.py index 4d2d685f8..8626fd68f 100644 --- a/endpoints/notificationmethod.py +++ b/endpoints/notificationmethod.py @@ -71,7 +71,7 @@ class QuayNotificationMethod(NotificationMethod): target_info = config_data['target'] if target_info['kind'] == 'user': - target = model.get_user(target_info['name']) + target = model.get_nonrobot_user(target_info['name']) if not target: # Just to be safe. return (True, 'Unknown user %s' % target_info['name'], []) diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 1a07a357a..692c23990 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -244,7 +244,11 @@ class BitbucketBuildTrigger(BuildTriggerHandler): def _get_authorized_client(self): base_client = self._get_client() auth_token = self.auth_token or 'invalid:invalid' - (access_token, access_token_secret) = auth_token.split(':') + token_parts = auth_token.split(':') + if len(token_parts) != 2: + token_parts = ['invalid', 'invalid'] + + (access_token, access_token_secret) = token_parts return base_client.get_authorized_client(access_token, access_token_secret) def _get_repository_client(self): @@ -253,6 +257,13 @@ class BitbucketBuildTrigger(BuildTriggerHandler): bitbucket_client = self._get_authorized_client() return bitbucket_client.for_namespace(namespace).repositories().get(name) + def _get_default_branch(self, repository, default_value='master'): + (result, data, _) = repository.get_main_branch() + if result: + return data['name'] + + return default_value + def get_oauth_url(self): bitbucket_client = self._get_client() (result, data, err_msg) = bitbucket_client.get_authorization_url() @@ -372,6 +383,9 @@ class BitbucketBuildTrigger(BuildTriggerHandler): # Find the first matching branch. repo_branches = self.list_field_values('branch_name') or [] branches = find_matching_branches(config, repo_branches) + if not branches: + branches = [self._get_default_branch(repository)] + (result, data, err_msg) = repository.get_path_contents('', revision=branches[0]) if not result: raise RepositoryReadException(err_msg) @@ -432,10 +446,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): # Lookup the default branch associated with the repository. We use this when building # the tags. - default_branch = '' - (result, data, _) = repository.get_main_branch() - if result: - default_branch = data['name'] + default_branch = self._get_default_branch(repository) # Lookup the commit sha. (result, data, _) = repository.changesets().get(commit_sha) @@ -488,30 +499,36 @@ class BitbucketBuildTrigger(BuildTriggerHandler): # Parse the JSON payload. payload_json = request.form.get('payload') if not payload_json: + logger.debug('Skipping BitBucket request due to missing payload') raise SkipRequestException() try: payload = json.loads(payload_json) except ValueError: + logger.debug('Skipping BitBucket request due to invalid payload') raise SkipRequestException() logger.debug('BitBucket trigger payload %s', payload) # Make sure we have a commit in the payload. if not payload.get('commits'): + logger.debug('Skipping BitBucket request due to missing commits block') raise SkipRequestException() # Check if this build should be skipped by commit message. commit = payload['commits'][0] commit_message = commit['message'] if should_skip_commit(commit_message): + logger.debug('Skipping BitBucket request due to commit message request') raise SkipRequestException() # Check to see if this build should be skipped by ref. if not commit.get('branch') and not commit.get('tag'): + logger.debug('Skipping BitBucket request due to missing branch and tag') raise SkipRequestException() ref = 'refs/heads/' + commit['branch'] if commit.get('branch') else 'refs/tags/' + commit['tag'] + logger.debug('Checking BitBucket request: %s', ref) raise_if_skipped(self.config, ref) commit_sha = commit['node'] @@ -523,10 +540,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): repository = self._get_repository_client() # Find the branch to build. - branch_name = run_parameters.get('branch_name') - (result, data, _) = repository.get_main_branch() - if result: - branch_name = branch_name or data['name'] + branch_name = run_parameters.get('branch_name') or self._get_default_branch(repository) # Lookup the commit SHA for the branch. (result, data, _) = repository.get_branches() @@ -1048,7 +1062,7 @@ class CustomBuildTrigger(BuildTriggerHandler): } prepared = PreparedBuild(self.trigger) - prepared.tags = [commit_sha] + prepared.tags = [commit_sha[:7]] prepared.name_from_sha(commit_sha) prepared.subdirectory = config['subdir'] prepared.metadata = metadata diff --git a/endpoints/web.py b/endpoints/web.py index db12a0e65..b526790dc 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -9,10 +9,11 @@ from health.healthcheck import get_healthchecker from data import model from data.model.oauth import DatabaseAuthorizationProvider -from app import app, billing as stripe, build_logs, avatar, signer +from app import app, billing as stripe, build_logs, avatar, signer, log_archive from auth.auth import require_session_login, process_oauth from auth.permissions import (AdministerOrganizationPermission, ReadRepositoryPermission, - SuperUserPermission, AdministerRepositoryPermission) + SuperUserPermission, AdministerRepositoryPermission, + ModifyRepositoryPermission) from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot @@ -250,6 +251,31 @@ def robots(): return send_from_directory('static', 'robots.txt') +@web.route('/buildlogs/', methods=['GET']) +@route_show_if(features.BUILD_SUPPORT) +@require_session_login +def buildlogs(build_uuid): + build = model.get_repository_build(build_uuid) + if not build: + abort(403) + + repo = build.repository + if not ModifyRepositoryPermission(repo.namespace_user.username, repo.name).can(): + abort(403) + + # If the logs have been archived, just return a URL of the completed archive + if build.logs_archived: + return redirect(log_archive.get_file_url(build.uuid)) + + _, logs = build_logs.get_log_entries(build.uuid, 0) + response = jsonify({ + 'logs': [log for log in logs] + }) + + response.headers["Content-Disposition"] = "attachment;filename=" + build.uuid + ".json" + return response + + @web.route('/receipt', methods=['GET']) @route_show_if(features.BILLING) @require_session_login diff --git a/health/healthcheck.py b/health/healthcheck.py index 11a365e34..98de22435 100644 --- a/health/healthcheck.py +++ b/health/healthcheck.py @@ -77,10 +77,11 @@ class LocalHealthCheck(HealthCheck): class ProductionHealthCheck(HealthCheck): - def __init__(self, app, access_key, secret_key): + def __init__(self, app, access_key, secret_key, db_instance='quay'): super(ProductionHealthCheck, self).__init__(app) self.access_key = access_key self.secret_key = secret_key + self.db_instance = db_instance @classmethod def check_name(cls): @@ -115,7 +116,10 @@ class ProductionHealthCheck(HealthCheck): aws_access_key_id=self.access_key, aws_secret_access_key=self.secret_key) response = region.describe_db_instances()['DescribeDBInstancesResponse'] result = response['DescribeDBInstancesResult'] - instances = result['DBInstances'] + instances = [i for i in result['DBInstances'] if i['DBInstanceIdentifier'] == self.db_instance] + if not instances: + return 'error' + status = instances[0]['DBInstanceStatus'] return status except: diff --git a/requirements-nover.txt b/requirements-nover.txt index d76e671b5..8cbb82cf8 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -21,7 +21,6 @@ paramiko xhtml2pdf redis hiredis -docker-py flask-restful==0.2.12 jsonschema git+https://github.com/NateFerrero/oauth2lib.git @@ -38,12 +37,12 @@ psycopg2 pyyaml git+https://github.com/DevTable/aniso8601-fake.git git+https://github.com/DevTable/anunidecode.git -git+https://github.com/DevTable/avatar-generator.git git+https://github.com/DevTable/pygithub.git git+https://github.com/DevTable/container-cloud-config.git git+https://github.com/DevTable/python-etcd.git git+https://github.com/coreos/py-bitbucket.git git+https://github.com/coreos/pyapi-gitlab.git +git+https://github.com/coreos/mockldap.git gipc pyOpenSSL pygpgme @@ -51,4 +50,6 @@ cachetools mock psutil stringscore -mockldap +python-swiftclient +python-keystoneclient +Flask-Testing \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 08eddb442..4531f5227 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,76 +1,99 @@ -APScheduler==3.0.1 +APScheduler==3.0.3 +Babel==1.3 Flask==0.10.1 Flask-Login==0.2.11 Flask-Mail==0.9.1 Flask-Principal==0.4.0 Flask-RESTful==0.2.12 +Flask-Testing==0.4.2 Jinja2==2.7.3 -LogentriesLogger==0.2.1 -Mako==1.0.0 +Logentries==0.7 +Mako==1.0.1 MarkupSafe==0.23 -Pillow==2.7.0 -PyMySQL==0.6.3 +Pillow==2.8.1 +PyMySQL==0.6.6 PyPDF2==1.24 PyYAML==3.11 -SQLAlchemy==0.9.8 -WebOb==1.4 -Werkzeug==0.9.6 -aiowsgi==0.3 -alembic==0.7.4 +SQLAlchemy==1.0.3 +WebOb==1.4.1 +Werkzeug==0.10.4 +aiowsgi==0.5 +alembic==0.7.5.post2 +argparse==1.3.0 autobahn==0.9.3-3 backports.ssl-match-hostname==3.4.0.2 beautifulsoup4==4.3.2 blinker==1.3 -boto==2.35.1 +boto==2.38.0 cachetools==1.0.0 -docker-py==0.7.1 -ecdsa==0.11 +certifi==2015.04.28 +cffi==0.9.2 +cryptography==0.8.2 +ecdsa==0.13 +enum34==1.0.4 +funcparserlib==0.3.6 futures==2.2.0 gevent==1.0.1 gipc==0.5.0 greenlet==0.4.5 gunicorn==18.0 -hiredis==0.1.5 -html5lib==0.999 +hiredis==0.2.0 +html5lib==0.99999 +iso8601==0.1.10 itsdangerous==0.24 jsonschema==2.4.0 -marisa-trie==0.7 -mixpanel-py==3.2.1 +marisa-trie==0.7.2 +mixpanel-py==4.0.2 mock==1.0.1 -mockldap==0.2.4 +msgpack-python==0.4.6 +netaddr==0.7.14 +netifaces==0.10.4 +oauthlib==0.7.2 +oslo.config==1.11.0 +oslo.i18n==1.6.0 +oslo.serialization==1.5.0 +oslo.utils==1.5.0 paramiko==1.15.2 -peewee==2.4.7 +pbr==0.11.0 +peewee==2.6.0 +prettytable==0.7.2 psutil==2.2.1 -psycopg2==2.5.4 +psycopg2==2.6 py-bcrypt==0.4 +pyOpenSSL==0.15.1 +pyasn1==0.1.7 +pycparser==2.12 pycrypto==2.6.1 -python-dateutil==2.4.0 +pygpgme==0.3 +python-dateutil==2.4.2 +python-keystoneclient==1.4.0 python-ldap==2.4.19 python-magic==0.4.6 -pygpgme==0.3 -pytz==2014.10 -pyOpenSSL==0.14 -raven==5.1.1 +python-swiftclient==2.4.0 +pytz==2015.2 +raven==5.3.0 redis==2.10.3 reportlab==2.7 -requests==2.5.1 +requests==2.6.2 requests-oauthlib==0.4.2 +simplejson==3.7.1 six==1.9.0 +stevedore==1.4.0 stringscore==0.1.0 -stripe==1.20.1 +stripe==1.22.2 trollius==1.0.4 -tzlocal==1.1.2 -urllib3==1.10.2 +tzlocal==1.1.3 +urllib3==1.10.3 waitress==0.8.9 -websocket-client==0.23.0 +websocket-client==0.30.0 wsgiref==0.1.2 xhtml2pdf==0.0.6 git+https://github.com/DevTable/aniso8601-fake.git git+https://github.com/DevTable/anunidecode.git -git+https://github.com/DevTable/avatar-generator.git git+https://github.com/DevTable/pygithub.git git+https://github.com/DevTable/container-cloud-config.git git+https://github.com/DevTable/python-etcd.git git+https://github.com/NateFerrero/oauth2lib.git git+https://github.com/coreos/py-bitbucket.git git+https://github.com/coreos/pyapi-gitlab.git +git+https://github.com/coreos/mockldap.git \ No newline at end of file diff --git a/static/css/core-ui.css b/static/css/core-ui.css index 58ab72b81..adf6bc084 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -388,6 +388,29 @@ a:focus { width: 400px; } +.config-map-field-element table { + margin-bottom: 10px; +} + +.config-map-field-element .form-control-container { + border-top: 1px solid #eee; + padding-top: 10px; +} + +.config-map-field-element .form-control-container select, .config-map-field-element .form-control-container input { + margin-bottom: 10px; +} + +.config-map-field-element .empty { + color: #ccc; + margin-bottom: 10px; + display: block; +} + +.config-map-field-element .item-title { + font-weight: bold; +} + .config-contact-field { margin-bottom: 4px; } diff --git a/static/css/directives/ui/build-logs-view.css b/static/css/directives/ui/build-logs-view.css index e69a3ad57..746a6c1d7 100644 --- a/static/css/directives/ui/build-logs-view.css +++ b/static/css/directives/ui/build-logs-view.css @@ -157,6 +157,24 @@ transition: all 0.15s ease-in-out; } +.build-logs-view .download-button i.fa { + margin-right: 10px; +} + +.build-logs-view .download-button { + position: absolute; + top: 6px; + right: 124px; + z-index: 2; + transition: all 0.15s ease-in-out; +} + +.build-logs-view .download-button:not(:hover) { + background: transparent; + border: 1px solid transparent; + color: #ddd; +} + .build-logs-view .copy-button:not(.zeroclipboard-is-hover) { background: transparent; border: 1px solid transparent; diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html index f99a7eb00..aa79cec8a 100644 --- a/static/directives/build-logs-view.html +++ b/static/directives/build-logs-view.html @@ -3,6 +3,12 @@ Copy Logs + + Download Logs + + diff --git a/static/directives/config/config-map-field.html b/static/directives/config/config-map-field.html new file mode 100644 index 000000000..7089e2010 --- /dev/null +++ b/static/directives/config/config-map-field.html @@ -0,0 +1,20 @@ +
+ + + + + + +
{{ key }}{{ value }} + Remove +
+ No entries defined +
+ Add Key-Value: + + + +
+
diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 6e9458f61..3e935e756 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -112,6 +112,11 @@ A valid SSL certificate and private key files are required to use this option. +
+ Enabling SSL also enables HTTP Strict Transport Security.
+ This prevents downgrade attacks and cookie theft, but browsers will reject all future insecure connections on this hostname. +
+ @@ -198,6 +203,7 @@ + @@ -206,10 +212,15 @@
Certificate:
{{ field.title }}: + + ng-if="field.kind == 'text'" + is-optional="field.optional">
@@ -849,4 +860,4 @@
- \ No newline at end of file + diff --git a/static/directives/config/config-string-field.html b/static/directives/config/config-string-field.html index 7714fd541..703891f89 100644 --- a/static/directives/config/config-string-field.html +++ b/static/directives/config/config-string-field.html @@ -2,7 +2,7 @@
+ ng-pattern="getRegexp(pattern)" ng-required="!isOptional">
{{ errorMessage }}
diff --git a/static/directives/repo-view/repo-panel-settings.html b/static/directives/repo-view/repo-panel-settings.html index 04e128579..720a96772 100644 --- a/static/directives/repo-view/repo-panel-settings.html +++ b/static/directives/repo-view/repo-panel-settings.html @@ -63,6 +63,12 @@