diff --git a/Dockerfile b/Dockerfile index 34d68e93a..b9f9074d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -106,6 +106,10 @@ RUN yarn build \ && jpegoptim static/img/**/*.jpg \ && optipng -clobber -quiet static/img/**/*.png +# Config app js compile +COPY config_app/ config_app/ +RUN yarn build-config-app + COPY . . RUN PYTHONPATH=$QUAYPATH venv/bin/python -m external_libraries @@ -139,4 +143,3 @@ RUN ./scripts/detect-config.sh EXPOSE 443 8443 80 ENTRYPOINT [ "/bin/bash", "./quay-entrypoint.sh"] - diff --git a/config_app/_init_config.py b/config_app/_init_config.py index 1ab66a338..a1628321a 100644 --- a/config_app/_init_config.py +++ b/config_app/_init_config.py @@ -3,8 +3,10 @@ import re import subprocess -# Note: this currently points to the directory above, since we're in the quay config_app dir. When extracting, revert +# Note: this currently points to the directory above, since we're in the quay config_app dir +# TODO(config_extract): revert to root directory rather than the one above ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + CONF_DIR = os.getenv("QUAYCONF", os.path.join(ROOT_DIR, "conf/")) STATIC_DIR = os.path.join(ROOT_DIR, 'static/') STATIC_LDN_DIR = os.path.join(STATIC_DIR, 'ldn/') diff --git a/config_app/c_app.py b/config_app/c_app.py index 6235701da..57155a5be 100644 --- a/config_app/c_app.py +++ b/config_app/c_app.py @@ -1,38 +1,40 @@ import os import logging + from flask import Flask -from _init_config import ROOT_DIR -from config_app.config_util.config import get_config_provider + +from data import database +from util.config.superusermanager import SuperUserManager from util.ipresolver import NoopIPResolver - -from util.config.superusermanager import SuperUserManager +from config_app._init_config import ROOT_DIR +from config_app.config_util.config import get_config_provider app = Flask(__name__) logger = logging.getLogger(__name__) -# OVERRIDE_CONFIG_DIRECTORY = os.path.join(ROOT_DIR, 'stack/') OVERRIDE_CONFIG_DIRECTORY = os.path.join(ROOT_DIR, 'config_app/conf/stack') - is_testing = 'TEST' in os.environ -is_kubernetes = 'KUBERNETES_SERVICE_HOST' in os.environ -config_provider = get_config_provider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config_app_config.py', - testing=is_testing, kubernetes=is_kubernetes) +# TODO(config kubernetes): reinstate when enabling kubernetes in config app +# is_kubernetes = 'KUBERNETES_SERVICE_HOST' in os.environ + +config_provider = get_config_provider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py', + testing=is_testing) if is_testing: - from config_app.config_test.testconfig import TestConfig + from test.testconfig import TestConfig logger.debug('Loading test config.') app.config.from_object(TestConfig()) else: - from config_app.config_app_config import DefaultConfig + from config import DefaultConfig logger.debug('Loading default config.') app.config.from_object(DefaultConfig()) - # app.teardown_request(database.close_db_filter) + app.teardown_request(database.close_db_filter) # Load the override config via the provider. config_provider.update_app_config(app.config) superusers = SuperUserManager(app) -ip_resolver = NoopIPResolver() \ No newline at end of file +ip_resolver = NoopIPResolver() diff --git a/config_app/conf/gunicorn_local.py b/config_app/conf/gunicorn_local.py index 377c1ba0f..d0ea0a758 100644 --- a/config_app/conf/gunicorn_local.py +++ b/config_app/conf/gunicorn_local.py @@ -6,12 +6,11 @@ import logging from Crypto import Random from config_app.config_util.log import logfile_path -from config_app.config_util.workers import get_worker_count logconfig = logfile_path(debug=True) bind = '0.0.0.0:5000' -workers = get_worker_count('local', 2, minimum=2, maximum=8) +workers = 1 worker_class = 'gevent' daemon = False pythonpath = '.' diff --git a/config_app/conf/gunicorn_web.py b/config_app/conf/gunicorn_web.py new file mode 100644 index 000000000..4c4e1a152 --- /dev/null +++ b/config_app/conf/gunicorn_web.py @@ -0,0 +1,26 @@ +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), "../")) + +import logging + +from Crypto import Random +from config_app.config_util.log import logfile_path + + +logconfig = logfile_path(debug=True) + +bind = '0.0.0.0:80' +workers = 1 +worker_class = 'gevent' +pythonpath = '.' +preload_app = True + +def post_fork(server, worker): + # Reset the Random library to ensure it won't raise the "PID check failed." error after + # gunicorn forks. + Random.atfork() + +def when_ready(server): + logger = logging.getLogger(__name__) + logger.debug('Starting local gunicorn with %s workers and %s worker class', workers, worker_class) diff --git a/config_app/config_app_config.py b/config_app/config_app_config.py deleted file mode 100644 index cd2d35be4..000000000 --- a/config_app/config_app_config.py +++ /dev/null @@ -1,544 +0,0 @@ -from uuid import uuid4 - -import os.path -import requests - -from _init_config import ROOT_DIR, CONF_DIR - - -def build_requests_session(): - sess = requests.Session() - adapter = requests.adapters.HTTPAdapter(pool_connections=100, - pool_maxsize=100) - sess.mount('http://', adapter) - sess.mount('https://', adapter) - return sess - - -# The set of configuration key names that will be accessible in the client. Since these -# values are sent to the frontend, DO NOT PLACE ANY SECRETS OR KEYS in this list. -CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY', - 'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', - 'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', - 'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER', 'DOCUMENTATION_LOCATION', - 'DOCUMENTATION_METADATA', 'SETUP_COMPLETE', 'DEBUG', 'MARKETO_MUNCHKIN_ID', - 'STATIC_SITE_BUCKET', 'RECAPTCHA_SITE_KEY', 'CHANNEL_COLORS', - 'TAG_EXPIRATION_OPTIONS', 'INTERNAL_OIDC_SERVICE_ID', - 'SEARCH_RESULTS_PER_PAGE', 'SEARCH_MAX_RESULT_PAGE_COUNT'] - - -def frontend_visible_config(config_dict): - visible_dict = {} - for name in CLIENT_WHITELIST: - if name.lower().find('secret') >= 0: - raise Exception('Cannot whitelist secrets: %s' % name) - - if name in config_dict: - visible_dict[name] = config_dict.get(name, None) - - return visible_dict - - -# Configuration that should not be changed by end users -class ImmutableConfig(object): - - # Requests based HTTP client with a large request pool - HTTPCLIENT = build_requests_session() - - # Status tag config - STATUS_TAGS = {} - for tag_name in ['building', 'failed', 'none', 'ready', 'cancelled']: - tag_path = os.path.join(ROOT_DIR, 'buildstatus', tag_name + '.svg') - with open(tag_path) as tag_svg: - STATUS_TAGS[tag_name] = tag_svg.read() - - # Reverse DNS prefixes that are reserved for internal use on labels and should not be allowable - # to be set via the API. - DEFAULT_LABEL_KEY_RESERVED_PREFIXES = ['com.docker.', 'io.docker.', 'org.dockerproject.', - 'org.opencontainers.', 'io.cncf.', - 'io.kubernetes.', 'io.k8s.', - 'io.quay', 'com.coreos', 'com.tectonic', - 'internal', 'quay'] - - # Colors for local avatars. - AVATAR_COLORS = ['#969696', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728', - '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2', - '#7f7f7f', '#c7c7c7', '#bcbd22', '#1f77b4', '#17becf', '#9edae5', '#393b79', - '#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b', - '#8c6d31', '#ad494a', '#e7ba52', '#a55194'] - - # Colors for channels. - CHANNEL_COLORS = ['#969696', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728', - '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2', - '#7f7f7f', '#c7c7c7', '#bcbd22', '#1f77b4', '#17becf', '#9edae5', '#393b79', - '#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b', - '#8c6d31', '#ad494a', '#e7ba52', '#a55194'] - - PROPAGATE_EXCEPTIONS = True - - -class DefaultConfig(ImmutableConfig): - # Flask config - JSONIFY_PRETTYPRINT_REGULAR = False - SESSION_COOKIE_SECURE = False - - LOGGING_LEVEL = 'DEBUG' - SEND_FILE_MAX_AGE_DEFAULT = 0 - PREFERRED_URL_SCHEME = 'http' - SERVER_HOSTNAME = 'localhost:5000' - - REGISTRY_TITLE = 'Quay Enterprise' - REGISTRY_TITLE_SHORT = 'Quay Enterprise' - - CONTACT_INFO = [ - 'mailto:support@quay.io', - 'irc://chat.freenode.net:6665/quay', - 'tel:+1-888-930-3475', - 'https://twitter.com/quayio', - ] - - # Mail config - MAIL_SERVER = '' - MAIL_USE_TLS = True - MAIL_PORT = 587 - MAIL_USERNAME = None - MAIL_PASSWORD = None - MAIL_DEFAULT_SENDER = 'support@quay.io' - MAIL_FAIL_SILENTLY = False - TESTING = True - - # DB config - DB_URI = 'sqlite:///test/data/test.db' - DB_CONNECTION_ARGS = { - 'threadlocals': True, - 'autorollback': True, - } - - @staticmethod - def create_transaction(db): - return db.transaction() - - DB_TRANSACTION_FACTORY = create_transaction - - # If set to true, TLS is used, but is terminated by an external service (such as a load balancer). - # Note that PREFERRED_URL_SCHEME must be `https` when this flag is set or it can lead to undefined - # behavior. - EXTERNAL_TLS_TERMINATION = False - - # If true, CDN URLs will be used for our external dependencies, rather than the local - # copies. - USE_CDN = False - - # Authentication - AUTHENTICATION_TYPE = 'Database' - - # Build logs - BUILDLOGS_REDIS = {'host': 'localhost'} - BUILDLOGS_OPTIONS = [] - - # Real-time user events - USER_EVENTS_REDIS = {'host': 'localhost'} - - # Stripe config - BILLING_TYPE = 'FakeStripe' - - # Analytics - ANALYTICS_TYPE = 'FakeAnalytics' - - # Build Queue Metrics - QUEUE_METRICS_TYPE = 'Null' - QUEUE_WORKER_METRICS_REFRESH_SECONDS = 300 - - # Exception logging - EXCEPTION_LOG_TYPE = 'FakeSentry' - SENTRY_DSN = None - SENTRY_PUBLIC_DSN = None - - # Github Config - GITHUB_LOGIN_CONFIG = None - GITHUB_TRIGGER_CONFIG = None - - # Google Config. - GOOGLE_LOGIN_CONFIG = None - - # Bitbucket Config. - BITBUCKET_TRIGGER_CONFIG = None - - # Gitlab Config. - GITLAB_TRIGGER_CONFIG = None - - NOTIFICATION_QUEUE_NAME = 'notification' - DOCKERFILE_BUILD_QUEUE_NAME = 'dockerfilebuild' - REPLICATION_QUEUE_NAME = 'imagestoragereplication' - SECSCAN_NOTIFICATION_QUEUE_NAME = 'security_notification' - CHUNK_CLEANUP_QUEUE_NAME = 'chunk_cleanup' - NAMESPACE_GC_QUEUE_NAME = 'namespacegc' - - # Super user config. Note: This MUST BE an empty list for the default config. - SUPER_USERS = [] - - # Feature Flag: Whether sessions are permanent. - FEATURE_PERMANENT_SESSIONS = True - - # Feature Flag: Whether super users are supported. - FEATURE_SUPER_USERS = True - - # Feature Flag: Whether to allow anonymous users to browse and pull public repositories. - FEATURE_ANONYMOUS_ACCESS = True - - # Feature Flag: Whether billing is required. - FEATURE_BILLING = False - - # Feature Flag: Whether user accounts automatically have usage log access. - FEATURE_USER_LOG_ACCESS = False - - # Feature Flag: Whether GitHub login is supported. - FEATURE_GITHUB_LOGIN = False - - # Feature Flag: Whether Google login is supported. - FEATURE_GOOGLE_LOGIN = False - - # Feature Flag: whether to enable support chat - FEATURE_SUPPORT_CHAT = False - - # Feature Flag: Whether to support GitHub build triggers. - FEATURE_GITHUB_BUILD = False - - # Feature Flag: Whether to support Bitbucket build triggers. - FEATURE_BITBUCKET_BUILD = False - - # Feature Flag: Whether to support GitLab build triggers. - FEATURE_GITLAB_BUILD = False - - # Feature Flag: Dockerfile build support. - FEATURE_BUILD_SUPPORT = True - - # Feature Flag: Whether emails are enabled. - FEATURE_MAILING = True - - # Feature Flag: Whether users can be created (by non-super users). - FEATURE_USER_CREATION = True - - # Feature Flag: Whether users being created must be invited by another user. If FEATURE_USER_CREATION is off, - # this flag has no effect. - FEATURE_INVITE_ONLY_USER_CREATION = False - - # Feature Flag: Whether users can be renamed - FEATURE_USER_RENAME = False - - # Feature Flag: Whether non-encrypted passwords (as opposed to encrypted tokens) can be used for - # basic auth. - FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = False - - # Feature Flag: Whether to automatically replicate between storage engines. - FEATURE_STORAGE_REPLICATION = False - - # Feature Flag: Whether users can directly login to the UI. - FEATURE_DIRECT_LOGIN = True - - # Feature Flag: Whether the v2/ endpoint is visible - FEATURE_ADVERTISE_V2 = True - - # Semver spec for which Docker versions we will blacklist - # Documentation: http://pythonhosted.org/semantic_version/reference.html#semantic_version.Spec - BLACKLIST_V2_SPEC = '<1.6.0' - - # Feature Flag: Whether or not to rotate old action logs to storage. - FEATURE_ACTION_LOG_ROTATION = False - - # Feature Flag: Whether to enable conversion to ACIs. - FEATURE_ACI_CONVERSION = False - - # Feature Flag: Whether to allow for "namespace-less" repositories when pulling and pushing from - # Docker. - FEATURE_LIBRARY_SUPPORT = True - - # Feature Flag: Whether to require invitations when adding a user to a team. - FEATURE_REQUIRE_TEAM_INVITE = True - - # Feature Flag: Whether to proxy all direct download URLs in storage via the registry's nginx. - FEATURE_PROXY_STORAGE = False - - # Feature Flag: Whether to collect and support user metadata. - FEATURE_USER_METADATA = False - - # Feature Flag: Whether to support signing - FEATURE_SIGNING = False - - # Feature Flag: Whether to enable support for App repositories. - FEATURE_APP_REGISTRY = False - - # Feature Flag: If set to true, the _catalog endpoint returns public repositories. Otherwise, - # only private repositories can be returned. - FEATURE_PUBLIC_CATALOG = False - - # Feature Flag: If set to true, build logs may be read by those with read access to the repo, - # rather than only write access or admin access. - FEATURE_READER_BUILD_LOGS = False - - # Feature Flag: If set to true, autocompletion will apply to partial usernames. - FEATURE_PARTIAL_USER_AUTOCOMPLETE = True - - # If a namespace is defined in the public namespace list, then it will appear on *all* - # user's repository list pages, regardless of whether that user is a member of the namespace. - # Typically, this is used by an enterprise customer in configuring a set of "well-known" - # namespaces. - PUBLIC_NAMESPACES = [] - - # The namespace to use for library repositories. - # Note: This must remain 'library' until Docker removes their hard-coded namespace for libraries. - # See: https://github.com/docker/docker/blob/master/registry/session.go#L320 - LIBRARY_NAMESPACE = 'library' - - BUILD_MANAGER = ('enterprise', {}) - - DISTRIBUTED_STORAGE_CONFIG = { - 'local_eu': ['LocalStorage', {'storage_path': 'test/data/registry/eu'}], - 'local_us': ['LocalStorage', {'storage_path': 'test/data/registry/us'}], - } - - DISTRIBUTED_STORAGE_PREFERENCE = ['local_us'] - DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS = ['local_us'] - - # Health checker. - HEALTH_CHECKER = ('LocalHealthCheck', {}) - - # Userfiles - USERFILES_LOCATION = 'local_us' - USERFILES_PATH = 'userfiles/' - - # Build logs archive - LOG_ARCHIVE_LOCATION = 'local_us' - LOG_ARCHIVE_PATH = 'logarchive/' - - # Action logs archive - ACTION_LOG_ARCHIVE_LOCATION = 'local_us' - ACTION_LOG_ARCHIVE_PATH = 'actionlogarchive/' - - # System logs. - SYSTEM_LOGS_PATH = "/var/log/" - SYSTEM_LOGS_FILE = "/var/log/syslog" - SYSTEM_SERVICES_PATH = os.path.join(CONF_DIR, "init/service/") - - # Allow registry pulls when unable to write to the audit log - ALLOW_PULLS_WITHOUT_STRICT_LOGGING = False - - # Services that should not be shown in the logs view. - SYSTEM_SERVICE_BLACKLIST = [] - - # Temporary tag expiration in seconds, this may actually be longer based on GC policy - PUSH_TEMP_TAG_EXPIRATION_SEC = 60 * 60 # One hour per layer - - # Signed registry grant token expiration in seconds - SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull - - # Registry v2 JWT Auth config - REGISTRY_JWT_AUTH_MAX_FRESH_S = 60 * 60 + 60 # At most signed one hour, accounting for clock skew - - # The URL endpoint to which we redirect OAuth when generating a token locally. - LOCAL_OAUTH_HANDLER = '/oauth/localapp' - - # The various avatar background colors. - AVATAR_KIND = 'local' - - # The location of the Quay documentation. - DOCUMENTATION_LOCATION = 'http://docs.quay.io' - DOCUMENTATION_METADATA = 'https://coreos.github.io/quay-docs/search.json' - - # How often the Garbage Collection worker runs. - GARBAGE_COLLECTION_FREQUENCY = 30 # seconds - - # How long notifications will try to send before timing out. - NOTIFICATION_SEND_TIMEOUT = 10 - - # Security scanner - FEATURE_SECURITY_SCANNER = False - FEATURE_SECURITY_NOTIFICATIONS = False - - # The endpoint for the security scanner. - SECURITY_SCANNER_ENDPOINT = 'http://192.168.99.101:6060' - - # The number of seconds between indexing intervals in the security scanner - SECURITY_SCANNER_INDEXING_INTERVAL = 30 - - # If specified, the security scanner will only index images newer than the provided ID. - SECURITY_SCANNER_INDEXING_MIN_ID = None - - # If specified, the endpoint to be used for all POST calls to the security scanner. - SECURITY_SCANNER_ENDPOINT_BATCH = None - - # If specified, GET requests that return non-200 will be retried at the following instances. - SECURITY_SCANNER_READONLY_FAILOVER_ENDPOINTS = [] - - # The indexing engine version running inside the security scanner. - SECURITY_SCANNER_ENGINE_VERSION_TARGET = 3 - - # The version of the API to use for the security scanner. - SECURITY_SCANNER_API_VERSION = 'v1' - - # API call timeout for the security scanner. - SECURITY_SCANNER_API_TIMEOUT_SECONDS = 10 - - # POST call timeout for the security scanner. - SECURITY_SCANNER_API_TIMEOUT_POST_SECONDS = 480 - - # The issuer name for the security scanner. - SECURITY_SCANNER_ISSUER_NAME = 'security_scanner' - - # JWTProxy Settings - # The address (sans schema) to proxy outgoing requests through the jwtproxy - # to be signed - JWTPROXY_SIGNER = 'localhost:8080' - - # The audience that jwtproxy should verify on incoming requests - # If None, will be calculated off of the SERVER_HOSTNAME (default) - JWTPROXY_AUDIENCE = None - - # Torrent management flags - FEATURE_BITTORRENT = False - BITTORRENT_PIECE_SIZE = 512 * 1024 - BITTORRENT_ANNOUNCE_URL = 'https://localhost:6881/announce' - BITTORRENT_FILENAME_PEPPER = str(uuid4()) - BITTORRENT_WEBSEED_LIFETIME = 3600 - - # "Secret" key for generating encrypted paging tokens. Only needed to be secret to - # hide the ID range for production (in which this value is overridden). Should *not* - # be relied upon for secure encryption otherwise. - # This value is a Fernet key and should be 32bytes URL-safe base64 encoded. - PAGE_TOKEN_KEY = '0OYrc16oBuksR8T3JGB-xxYSlZ2-7I_zzqrLzggBJ58=' - - # The timeout for service key approval. - UNAPPROVED_SERVICE_KEY_TTL_SEC = 60 * 60 * 24 # One day - - # How long to wait before GCing an expired service key. - EXPIRED_SERVICE_KEY_TTL_SEC = 60 * 60 * 24 * 7 # One week - - # The ID of the user account in the database to be used for service audit logs. If none, the - # lowest user in the database will be used. - SERVICE_LOG_ACCOUNT_ID = None - - # The service key ID for the instance service. - # NOTE: If changed, jwtproxy_conf.yaml.jnj must also be updated. - INSTANCE_SERVICE_KEY_SERVICE = 'quay' - - # The location of the key ID file generated for this instance. - INSTANCE_SERVICE_KEY_KID_LOCATION = os.path.join(CONF_DIR, 'quay.kid') - - # The location of the private key generated for this instance. - # NOTE: If changed, jwtproxy_conf.yaml.jnj must also be updated. - INSTANCE_SERVICE_KEY_LOCATION = os.path.join(CONF_DIR, 'quay.pem') - - # This instance's service key expiration in minutes. - INSTANCE_SERVICE_KEY_EXPIRATION = 120 - - # Number of minutes between expiration refresh in minutes. Should be the expiration / 2 minus - # some additional window time. - INSTANCE_SERVICE_KEY_REFRESH = 55 - - # The whitelist of client IDs for OAuth applications that allow for direct login. - DIRECT_OAUTH_CLIENTID_WHITELIST = [] - - # URL that specifies the location of the prometheus stats aggregator. - PROMETHEUS_AGGREGATOR_URL = 'http://localhost:9092' - - # Namespace prefix for all prometheus metrics. - PROMETHEUS_NAMESPACE = 'quay' - - # Overridable list of reverse DNS prefixes that are reserved for internal use on labels. - LABEL_KEY_RESERVED_PREFIXES = [] - - # Delays workers from starting until a random point in time between 0 and their regular interval. - STAGGER_WORKERS = True - - # Location of the static marketing site. - STATIC_SITE_BUCKET = None - - # Site key and secret key for using recaptcha. - FEATURE_RECAPTCHA = False - RECAPTCHA_SITE_KEY = None - RECAPTCHA_SECRET_KEY = None - - # Server where TUF metadata can be found - TUF_SERVER = None - - # Prefix to add to metadata e.g. // - TUF_GUN_PREFIX = None - - # Maximum size allowed for layers in the registry. - MAXIMUM_LAYER_SIZE = '20G' - - # Feature Flag: Whether team syncing from the backing auth is enabled. - FEATURE_TEAM_SYNCING = False - TEAM_RESYNC_STALE_TIME = '30m' - TEAM_SYNC_WORKER_FREQUENCY = 60 # seconds - - # Feature Flag: If enabled, non-superusers can setup team syncing. - FEATURE_NONSUPERUSER_TEAM_SYNCING_SETUP = False - - # The default configurable tag expiration time for time machine. - DEFAULT_TAG_EXPIRATION = '2w' - - # The options to present in namespace settings for the tag expiration. If empty, no option - # will be given and the default will be displayed read-only. - TAG_EXPIRATION_OPTIONS = ['0s', '1d', '1w', '2w', '4w'] - - # Feature Flag: Whether users can view and change their tag expiration. - FEATURE_CHANGE_TAG_EXPIRATION = True - - # Defines a secret for enabling the health-check endpoint's debug information. - ENABLE_HEALTH_DEBUG_SECRET = None - - # The lifetime for a user recovery token before it becomes invalid. - USER_RECOVERY_TOKEN_LIFETIME = '30m' - - # If specified, when app specific passwords expire by default. - APP_SPECIFIC_TOKEN_EXPIRATION = None - - # Feature Flag: If enabled, users can create and use app specific tokens to login via the CLI. - FEATURE_APP_SPECIFIC_TOKENS = True - - # How long expired app specific tokens should remain visible to users before being automatically - # deleted. Set to None to turn off garbage collection. - EXPIRED_APP_SPECIFIC_TOKEN_GC = '1d' - - # The size of pages returned by the Docker V2 API. - V2_PAGINATION_SIZE = 50 - - # If enabled, ensures that API calls are made with the X-Requested-With header - # when called from a browser. - BROWSER_API_CALLS_XHR_ONLY = True - - # If set to a non-None integer value, the default number of maximum builds for a namespace. - DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT = None - - # If set to a non-None integer value, the default number of maximum builds for a namespace whose - # creator IP is deemed a threat. - THREAT_NAMESPACE_MAXIMUM_BUILD_COUNT = None - - # For Billing Support Only: The number of allowed builds on a namespace that has been billed - # successfully. - BILLED_NAMESPACE_MAXIMUM_BUILD_COUNT = None - - # Configuration for the data model cache. - DATA_MODEL_CACHE_CONFIG = { - 'engine': 'memcached', - 'endpoint': ('127.0.0.1', 18080), - } - - # Defines the number of successive failures of a build trigger's build before the trigger is - # automatically disabled. - SUCCESSIVE_TRIGGER_FAILURE_DISABLE_THRESHOLD = 100 - - # Defines the number of successive internal errors of a build trigger's build before the - # trigger is automatically disabled. - SUCCESSIVE_TRIGGER_INTERNAL_ERROR_DISABLE_THRESHOLD = 5 - - # Defines the delay required (in seconds) before the last_accessed field of a user/robot or access - # token will be updated after the previous update. - LAST_ACCESSED_UPDATE_THRESHOLD_S = 60 - - # Defines the number of results per page used to show search results - SEARCH_RESULTS_PER_PAGE = 10 - - # Defines the maximum number of pages the user can paginate before they are limited - SEARCH_MAX_RESULT_PAGE_COUNT = 10 diff --git a/config_app/config_endpoints/api/__init__.py b/config_app/config_endpoints/api/__init__.py index fab34ffdd..d539639eb 100644 --- a/config_app/config_endpoints/api/__init__.py +++ b/config_app/config_endpoints/api/__init__.py @@ -22,7 +22,6 @@ class ApiExceptionHandlingApi(Api): @crossdomain(origin='*', headers=CROSS_DOMAIN_HEADERS) def handle_error(self, error): - print('HANDLING ERROR IN API') return super(ApiExceptionHandlingApi, self).handle_error(error) @@ -38,30 +37,12 @@ def format_date(date): return formatdate(timegm(date.utctimetuple())) -def verify_not_prod(func): - @add_method_metadata('enterprise_only', True) - @wraps(func) - def wrapped(*args, **kwargs): - # Verify that we are not running on a production (i.e. hosted) stack. If so, we fail. - # This should never happen (because of the feature-flag on SUPER_USERS), but we want to be - # absolutely sure. - # if app.config['SERVER_HOSTNAME'].find('quay.io') >= 0: - # TODO(config_port) fixme - if False: - logger.error('!!! Super user method called IN PRODUCTION !!!') - raise StandardError() - - return func(*args, **kwargs) - - return wrapped - def resource(*urls, **kwargs): def wrapper(api_resource): if not api_resource: return None - print('registering resource: ', urls) api_resource.registered = True api.add_resource(api_resource, *urls, **kwargs) return api_resource diff --git a/config_app/config_endpoints/api/discovery.py b/config_app/config_endpoints/api/discovery.py index 70246847c..2b0f0ceb3 100644 --- a/config_app/config_endpoints/api/discovery.py +++ b/config_app/config_endpoints/api/discovery.py @@ -1,3 +1,4 @@ +# TODO to extract the discovery stuff into a util at the top level and then use it both here and old discovery.py import logging import sys from collections import OrderedDict diff --git a/config_app/config_endpoints/api/suconfig.py b/config_app/config_endpoints/api/suconfig.py index 03cb895e4..29d617cfa 100644 --- a/config_app/config_endpoints/api/suconfig.py +++ b/config_app/config_endpoints/api/suconfig.py @@ -6,7 +6,7 @@ import signal from flask import abort, request from config_app.config_endpoints.api.suconfig_models_pre_oci import pre_oci_model as model -from config_app.config_endpoints.api import resource, ApiResource, verify_not_prod, nickname, validate_json_request +from config_app.config_endpoints.api import resource, ApiResource, nickname, validate_json_request from config_app.c_app import app, config_provider, superusers, OVERRIDE_CONFIG_DIRECTORY, ip_resolver from auth.auth_context import get_authenticated_user @@ -58,7 +58,6 @@ class SuperUserConfig(ApiResource): }, } - @verify_not_prod @nickname('scGetConfig') def get(self): """ Returns the currently defined configuration, if any. """ @@ -68,13 +67,11 @@ class SuperUserConfig(ApiResource): } @nickname('scUpdateConfig') - @verify_not_prod @validate_json_request('UpdateConfig') def put(self): """ Updates the config override file. """ # Note: This method is called to set the database configuration before super users exists, # so we also allow it to be called if there is no valid registry configuration setup. - # if not config_provider.config_exists() or SuperUserPermission().can(): if not config_provider.config_exists(): config_object = request.get_json()['config'] hostname = request.get_json()['hostname'] @@ -124,7 +121,6 @@ class SuperUserRegistryStatus(ApiResource): if a database is configured, and if it has any defined users. """ @nickname('scRegistryStatus') - @verify_not_prod def get(self): """ Returns the status of the registry. """ @@ -174,7 +170,6 @@ class _AlembicLogHandler(logging.Handler): @resource('/v1/superuser/setupdb') class SuperUserSetupDatabase(ApiResource): """ Resource for invoking alembic to setup the database. """ - @verify_not_prod @nickname('scSetupDatabase') def get(self): """ Invokes the alembic upgrade process. """ @@ -224,14 +219,12 @@ def get_process_id(name): class SuperUserShutdown(ApiResource): """ Resource for sending a shutdown signal to the container. """ - @verify_not_prod @nickname('scShutdownContainer') def post(self): """ Sends a signal to the phusion init system to shut down the container. """ # Note: This method is called to set the database configuration before super users exists, # so we also allow it to be called if there is no valid registry configuration setup. - # if app.config['TESTING'] or not database_has_users() or SuperUserPermission().can(): if app.config['TESTING'] or not database_has_users(): # Note: We skip if debugging locally. if app.config.get('DEBUGGING') == True: @@ -303,7 +296,7 @@ class SuperUserCreateInitialSuperUser(ApiResource): superusers.register_superuser(username) # Conduct login with that user. - # TODO(config): assuming we don't need to login the user + # TODO(config): figure out if we need validation for checking logged in user stuff # common_login(superuser_uuid) return { @@ -336,14 +329,12 @@ class SuperUserConfigValidate(ApiResource): } @nickname('scValidateConfig') - @verify_not_prod @validate_json_request('ValidateConfig') def post(self, service): """ Validates the given config for the given service. """ # Note: This method is called to validate the database configuration before super users exists, # so we also allow it to be called if there is no valid registry configuration setup. Note that # this is also safe since this method does not access any information not given in the request. - # if not config_provider.config_exists() or SuperUserPermission().can(): if not config_provider.config_exists(): config = request.get_json()['config'] validator_context = ValidatorContext.from_app(app, config, request.get_json().get('password', ''), diff --git a/config_app/config_endpoints/api/superuser.py b/config_app/config_endpoints/api/superuser.py index 5cb26fc9d..c061adacf 100644 --- a/config_app/config_endpoints/api/superuser.py +++ b/config_app/config_endpoints/api/superuser.py @@ -4,7 +4,7 @@ import pathvalidate from flask import request, jsonify from config_app.config_endpoints.exception import InvalidRequest -from config_app.config_endpoints.api import resource, ApiResource, verify_not_prod, nickname +from config_app.config_endpoints.api import resource, ApiResource, nickname from config_app.config_util.ssl import load_certificate, CertInvalidException from config_app.c_app import app, config_provider @@ -19,7 +19,6 @@ class SuperUserCustomCertificate(ApiResource): """ Resource for managing a custom certificate. """ @nickname('uploadCustomCertificate') - @verify_not_prod def post(self, certpath): uploaded_file = request.files['file'] if not uploaded_file: @@ -58,7 +57,6 @@ class SuperUserCustomCertificate(ApiResource): return '', 204 @nickname('deleteCustomCertificate') - @verify_not_prod def delete(self, certpath): cert_full_path = config_provider.get_volume_path(EXTRA_CA_DIRECTORY, certpath) config_provider.remove_volume_file(cert_full_path) @@ -70,7 +68,6 @@ class SuperUserCustomCertificates(ApiResource): """ Resource for managing custom certificates. """ @nickname('getCustomCertificates') - @verify_not_prod def get(self): has_extra_certs_path = config_provider.volume_file_exists(EXTRA_CA_DIRECTORY) extra_certs_found = config_provider.list_volume_directory(EXTRA_CA_DIRECTORY) @@ -107,7 +104,6 @@ class SuperUserCustomCertificates(ApiResource): } -# TODO(config) port this endpoint when (https://github.com/quay/quay/pull/3055) merged to ensure no conflicts @resource('/v1/superuser/keys') class SuperUserServiceKeyManagement(ApiResource): """ Resource for managing service keys.""" @@ -142,7 +138,6 @@ class SuperUserServiceKeyManagement(ApiResource): }, } - @verify_not_prod @nickname('listServiceKeys') def get(self): keys = pre_oci_model.list_all_service_keys() diff --git a/config_app/config_endpoints/api/superuser_models_interface.py b/config_app/config_endpoints/api/superuser_models_interface.py index 23d672f2b..f9971fdd1 100644 --- a/config_app/config_endpoints/api/superuser_models_interface.py +++ b/config_app/config_endpoints/api/superuser_models_interface.py @@ -1,17 +1,8 @@ -import json from abc import ABCMeta, abstractmethod from collections import namedtuple -from datetime import datetime - -from dateutil.relativedelta import relativedelta from six import add_metaclass -from tzlocal import get_localzone -# from app import avatar, superusers -# from buildtrigger.basehandler import BuildTriggerHandler -from data import model from config_app.config_endpoints.api import format_date -from util.morecollections import AttrDict def user_view(user): @@ -22,46 +13,6 @@ def user_view(user): } -# class BuildTrigger( -# namedtuple('BuildTrigger', ['uuid', 'service_name', 'pull_robot', 'can_read', 'can_admin', 'for_build'])): -# """ -# BuildTrigger represent a trigger that is associated with a build -# :type uuid: string -# :type service_name: string -# :type pull_robot: User -# :type can_read: boolean -# :type can_admin: boolean -# :type for_build: boolean -# """ -# -# def to_dict(self): -# if not self.uuid: -# return None -# -# build_trigger = BuildTriggerHandler.get_handler(self) -# build_source = build_trigger.config.get('build_source') -# -# repo_url = build_trigger.get_repository_url() if build_source else None -# can_read = self.can_read or self.can_admin -# -# trigger_data = { -# 'id': self.uuid, -# 'service': self.service_name, -# 'is_active': build_trigger.is_active(), -# -# 'build_source': build_source if can_read else None, -# 'repository_url': repo_url if can_read else None, -# -# 'config': build_trigger.config if self.can_admin else {}, -# 'can_invoke': self.can_admin, -# } -# -# if not self.for_build and self.can_admin and self.pull_robot: -# trigger_data['pull_robot'] = user_view(self.pull_robot) -# -# return trigger_data - - class RepositoryBuild(namedtuple('RepositoryBuild', ['uuid', 'logs_archived', 'repository_namespace_user_username', 'repository_name', 'can_write', 'can_read', 'pull_robot', 'resource_key', 'trigger', 'display_name', @@ -185,9 +136,6 @@ class User(namedtuple('User', ['username', 'email', 'verified', 'enabled', 'robo 'username': self.username, 'email': self.email, 'verified': self.verified, - # todo(config) remove or add these lines from app - # 'avatar': avatar.get_data_for_user(self), - # 'super_user': superusers.is_superuser(self.username), 'enabled': self.enabled, } @@ -205,108 +153,9 @@ class Organization(namedtuple('Organization', ['username', 'email'])): return { 'name': self.username, 'email': self.email, - # todo(config) remove or add these lines from app - # 'avatar': avatar.get_data_for_org(self), } -class LogEntry( - namedtuple('LogEntry', [ - 'metadata_json', 'ip', 'datetime', 'performer_email', 'performer_username', 'performer_robot', - 'account_organization', 'account_username', 'account_email', 'account_robot', 'kind', - ])): - """ - LogEntry a single log entry. - :type metadata_json: string - :type ip: string - :type datetime: string - :type performer_email: int - :type performer_username: string - :type performer_robot: boolean - :type account_organization: boolean - :type account_username: string - :type account_email: string - :type account_robot: boolean - :type kind_id: int - """ - - def to_dict(self): - view = { - 'kind': self.kind, - 'metadata': json.loads(self.metadata_json), - 'ip': self.ip, - 'datetime': format_date(self.datetime), - } - - if self.performer_username: - performer = AttrDict({'username': self.performer_username, 'email': self.performer_email}) - performer.robot = None - if self.performer_robot: - performer.robot = self.performer_robot - - view['performer'] = { - 'kind': 'user', - 'name': self.performer_username, - 'is_robot': self.performer_robot, - # todo(config) remove or add these lines from app - # 'avatar': avatar.get_data_for_user(performer), - } - - if self.account_username: - account = AttrDict({'username': self.account_username, 'email': self.account_email}) - if self.account_organization: - - view['namespace'] = { - 'kind': 'org', - 'name': self.account_username, - # todo(config) remove or add these lines from app - # 'avatar': avatar.get_data_for_org(account), - } - else: - account.robot = None - if self.account_robot: - account.robot = self.account_robot - view['namespace'] = { - 'kind': 'user', - 'name': self.account_username, - # todo(config) remove or add these lines from app - # 'avatar': avatar.get_data_for_user(account), - } - - return view - - -class LogEntryPage( - namedtuple('LogEntryPage', ['logs', 'next_page_token'])): - """ - LogEntryPage represents a single page of logs. - :type logs: [LogEntry] - :type next_page_token: {any -> any} - """ - - -class AggregatedLogEntry( - namedtuple('AggregatedLogEntry', ['count', 'kind_id', 'day', 'start_time'])): - """ - AggregatedLogEntry represents an aggregated view of logs. - :type count: int - :type kind_id: int - :type day: string - :type start_time: Date - """ - - def to_dict(self): - synthetic_date = datetime(self.start_time.year, self.start_time.month, int(self.day), tzinfo=get_localzone()) - if synthetic_date.day < self.start_time.day: - synthetic_date = synthetic_date + relativedelta(months=1) - kinds = model.log.get_log_entry_kinds() - view = { - 'kind': kinds[self.kind_id], - 'count': self.count, - 'datetime': format_date(synthetic_date), - } - - return view @add_metaclass(ABCMeta) @@ -314,135 +163,8 @@ class SuperuserDataInterface(object): """ Interface that represents all data store interactions required by a superuser api. """ - - @abstractmethod - def get_logs_query(self, start_time, end_time, page_token=None): - """ - Returns a LogEntryPage. - """ - - @abstractmethod - def get_aggregated_logs(self, start_time, end_time): - """ - Returns a list of AggregatedLogEntry - """ - - @abstractmethod - def get_organizations(self): - """ - Returns a list of Organization - """ - - @abstractmethod - def get_active_users(self): - """ - Returns a list of User - """ - - @abstractmethod - def create_install_user(self, username, password, email): - """ - Returns the created user and confirmation code for email confirmation - """ - - @abstractmethod - def get_nonrobot_user(self, username): - """ - Returns a User - """ - - @abstractmethod - def create_reset_password_email_code(self, email): - """ - Returns a recover password code - """ - - @abstractmethod - def mark_user_for_deletion(self, username): - """ - Returns None - """ - - @abstractmethod - def change_password(self, username, password): - """ - Returns None - """ - - @abstractmethod - def update_email(self, username, email, auto_verify): - """ - Returns None - """ - - @abstractmethod - def update_enabled(self, username, enabled): - """ - Returns None - """ - - @abstractmethod - def take_ownership(self, namespace, authed_user): - """ - Returns id of entity and whether the entity was a user - """ - - @abstractmethod - def mark_organization_for_deletion(self, name): - """ - Returns None - """ - - @abstractmethod - def change_organization_name(self, old_org_name, new_org_name): - """ - Returns updated Organization - """ - @abstractmethod def list_all_service_keys(self): """ Returns a list of service keys """ - - @abstractmethod - def generate_service_key(self, service, expiration_date, kid=None, name='', metadata=None, rotation_duration=None): - """ - Returns a tuple of private key and public key id - """ - - @abstractmethod - def approve_service_key(self, kid, approver, approval_type, notes=''): - """ - Returns the approved Key - """ - - @abstractmethod - def get_service_key(self, kid, service=None, alive_only=True, approved_only=True): - """ - Returns ServiceKey - """ - - @abstractmethod - def set_key_expiration(self, kid, expiration_date): - """ - Returns None - """ - - @abstractmethod - def update_service_key(self, kid, name=None, metadata=None): - """ - Returns None - """ - - @abstractmethod - def delete_service_key(self, kid): - """ - Returns deleted ServiceKey - """ - - @abstractmethod - def get_repository_build(self, uuid): - """ - Returns RepositoryBuild - """ diff --git a/config_app/config_endpoints/api/superuser_models_pre_oci.py b/config_app/config_endpoints/api/superuser_models_pre_oci.py index 352c8f38d..3002d5686 100644 --- a/config_app/config_endpoints/api/superuser_models_pre_oci.py +++ b/config_app/config_endpoints/api/superuser_models_pre_oci.py @@ -2,36 +2,6 @@ from data import model from config_app.config_endpoints.api.superuser_models_interface import SuperuserDataInterface, User, ServiceKey, Approval -# -# def _create_log(log, log_kind): -# account_organization = None -# account_username = None -# account_email = None -# account_robot = None -# try: -# account_organization = log.account.organization -# account_username = log.account.username -# account_email = log.account.email -# account_robot = log.account.robot -# except AttributeError: -# pass -# -# performer_robot = None -# performer_username = None -# performer_email = None -# -# try: -# performer_robot = log.performer.robot -# performer_username = log.performer.username -# performer_email = log.performer.email -# except AttributeError: -# pass -# -# return LogEntry(log.metadata_json, log.ip, log.datetime, performer_email, performer_username, -# performer_robot, account_organization, account_username, -# account_email, account_robot, log_kind[log.kind_id]) - - def _create_user(user): if user is None: return None @@ -46,18 +16,6 @@ def _create_key(key): return ServiceKey(key.name, key.kid, key.service, key.jwk, key.metadata, key.created_date, key.expiration_date, key.rotation_duration, approval) -# -# -# class ServiceKeyDoesNotExist(Exception): -# pass -# -# -# class ServiceKeyAlreadyApproved(Exception): -# pass -# -# -# class InvalidRepositoryBuildException(Exception): -# pass class PreOCIModel(SuperuserDataInterface): @@ -65,210 +23,9 @@ class PreOCIModel(SuperuserDataInterface): PreOCIModel implements the data model for the SuperUser using a database schema before it was changed to support the OCI specification. """ - def get_logs_query(self, start_time, end_time, page_token=None): - pass - - def get_aggregated_logs(self, start_time, end_time): - pass - - def get_organizations(self): - pass - - def get_active_users(self): - pass - - def create_install_user(self, username, password, email): - pass - - def get_nonrobot_user(self, username): - pass - - def create_reset_password_email_code(self, email): - pass - - def mark_user_for_deletion(self, username): - pass - - def change_password(self, username, password): - pass - - def update_email(self, username, email, auto_verify): - pass - - def update_enabled(self, username, enabled): - pass - - def take_ownership(self, namespace, authed_user): - pass - - def mark_organization_for_deletion(self, name): - pass - - def change_organization_name(self, old_org_name, new_org_name): - pass - - def generate_service_key(self, service, expiration_date, kid=None, name='', metadata=None, rotation_duration=None): - pass - - def approve_service_key(self, kid, approver, approval_type, notes=''): - pass - - def get_service_key(self, kid, service=None, alive_only=True, approved_only=True): - pass - - def set_key_expiration(self, kid, expiration_date): - pass - - def update_service_key(self, kid, name=None, metadata=None): - pass - - def delete_service_key(self, kid): - pass - - def get_repository_build(self, uuid): - pass - - # def get_repository_build(self, uuid): - # try: - # build = model.build.get_repository_build(uuid) - # except model.InvalidRepositoryBuildException as e: - # raise InvalidRepositoryBuildException(e.message) - # - # repo_namespace = build.repository_namespace_user_username - # repo_name = build.repository_name - # - # can_read = ReadRepositoryPermission(repo_namespace, repo_name).can() - # can_write = ModifyRepositoryPermission(repo_namespace, repo_name).can() - # can_admin = AdministerRepositoryPermission(repo_namespace, repo_name).can() - # job_config = get_job_config(build.job_config) - # phase, status, error = _get_build_status(build) - # url = userfiles.get_file_url(self.resource_key, request.remote_addr, requires_cors=True) - # - # return RepositoryBuild(build.uuid, build.logs_archived, repo_namespace, repo_name, can_write, can_read, - # _create_user(build.pull_robot), build.resource_key, - # BuildTrigger(build.trigger.uuid, build.trigger.service.name, - # _create_user(build.trigger.pull_robot), can_read, can_admin, True), - # build.display_name, build.display_name, build.started, job_config, phase, status, error, url) - # - # def delete_service_key(self, kid): - # try: - # key = model.service_keys.delete_service_key(kid) - # except model.ServiceKeyDoesNotExist: - # raise ServiceKeyDoesNotExist - # return _create_key(key) - # - # def update_service_key(self, kid, name=None, metadata=None): - # model.service_keys.update_service_key(kid, name, metadata) - # - # def set_key_expiration(self, kid, expiration_date): - # model.service_keys.set_key_expiration(kid, expiration_date) - # - # def get_service_key(self, kid, service=None, alive_only=True, approved_only=True): - # try: - # key = model.service_keys.get_service_key(kid, approved_only=approved_only, alive_only=alive_only) - # return _create_key(key) - # except model.ServiceKeyDoesNotExist: - # raise ServiceKeyDoesNotExist - # - # def approve_service_key(self, kid, approver, approval_type, notes=''): - # try: - # key = model.service_keys.approve_service_key(kid, approver, approval_type, notes=notes) - # return _create_key(key) - # except model.ServiceKeyDoesNotExist: - # raise ServiceKeyDoesNotExist - # except model.ServiceKeyAlreadyApproved: - # raise ServiceKeyAlreadyApproved - # - # def generate_service_key(self, service, expiration_date, kid=None, name='', metadata=None, rotation_duration=None): - # (private_key, key) = model.service_keys.generate_service_key(service, expiration_date, metadata=metadata, name=name) - # - # return private_key, key.kid - def list_all_service_keys(self): keys = model.service_keys.list_all_keys() return [_create_key(key) for key in keys] - # def change_organization_name(self, old_org_name, new_org_name): - # org = model.organization.get_organization(old_org_name) - # if new_org_name is not None: - # org = model.user.change_username(org.id, new_org_name) - # - # return Organization(org.username, org.email) - # - # def mark_organization_for_deletion(self, name): - # org = model.organization.get_organization(name) - # model.user.mark_namespace_for_deletion(org, all_queues, namespace_gc_queue, force=True) - # - # def take_ownership(self, namespace, authed_user): - # entity = model.user.get_user_or_org(namespace) - # if entity is None: - # return None, False - # - # was_user = not entity.organization - # if entity.organization: - # # Add the superuser as an admin to the owners team of the org. - # model.organization.add_user_as_admin(authed_user, entity) - # else: - # # If the entity is a user, convert it to an organization and add the current superuser - # # as the admin. - # model.organization.convert_user_to_organization(entity, authed_user) - # return entity.id, was_user - # - # def update_enabled(self, username, enabled): - # user = model.user.get_nonrobot_user(username) - # model.user.update_enabled(user, bool(enabled)) - # - # def update_email(self, username, email, auto_verify): - # user = model.user.get_nonrobot_user(username) - # model.user.update_email(user, email, auto_verify) - # - # def change_password(self, username, password): - # user = model.user.get_nonrobot_user(username) - # model.user.change_password(user, password) - # - # def mark_user_for_deletion(self, username): - # user = model.user.get_nonrobot_user(username) - # model.user.mark_namespace_for_deletion(user, all_queues, namespace_gc_queue, force=True) - # - # def create_reset_password_email_code(self, email): - # code = model.user.create_reset_password_email_code(email) - # return code.code - # - # def get_nonrobot_user(self, username): - # user = model.user.get_nonrobot_user(username) - # if user is None: - # return None - # return _create_user(user) - # - # def create_install_user(self, username, password, email): - # prompts = model.user.get_default_user_prompts(features) - # user = model.user.create_user(username, password, email, auto_verify=not features.MAILING, - # email_required=features.MAILING, prompts=prompts) - # - # return_user = _create_user(user) - # # If mailing is turned on, send the user a verification email. - # if features.MAILING: - # confirmation = model.user.create_confirm_email_code(user) - # return return_user, confirmation.code - # return return_user, '' - # - # def get_active_users(self, disabled=True): - # users = model.user.get_active_users(disabled=disabled) - # return [_create_user(user) for user in users] - # - # def get_organizations(self): - # return [Organization(org.username, org.email) for org in model.organization.get_organizations()] - # - # def get_aggregated_logs(self, start_time, end_time): - # aggregated_logs = model.log.get_aggregated_logs(start_time, end_time) - # return [AggregatedLogEntry(log.count, log.kind_id, log.day, start_time) for log in aggregated_logs] - # - # def get_logs_query(self, start_time, end_time, page_token=None): - # logs_query = model.log.get_logs_query(start_time, end_time) - # logs, next_page_token = model.modelutil.paginate(logs_query, database.LogEntry, descending=True, - # page_token=page_token, limit=20) - # kinds = model.log.get_log_entry_kinds() - # return LogEntryPage([_create_log(log, kinds) for log in logs], next_page_token) - pre_oci_model = PreOCIModel() diff --git a/config_app/config_endpoints/api/user.py b/config_app/config_endpoints/api/user.py index d8a6449c3..68d573873 100644 --- a/config_app/config_endpoints/api/user.py +++ b/config_app/config_endpoints/api/user.py @@ -1,4 +1,6 @@ +from auth.auth_context import get_authenticated_user from config_app.config_endpoints.api import resource, ApiResource, nickname +from config_app.config_endpoints.api.superuser_models_interface import user_view @resource('/v1/user/') @@ -8,11 +10,10 @@ class User(ApiResource): @nickname('getLoggedInUser') def get(self): """ Get user information for the authenticated user. """ - # user = get_authenticated_user() + user = get_authenticated_user() + # TODO(config): figure out if we need user validation + # if user is None or user.organization or not UserReadPermission(user.username).can(): + # raise InvalidToken("Requires authentication", payload={'session_required': False}) - # return user_view(user) - return { - 'anonymous': False, - # 'username': user.username, - } + return user_view(user) diff --git a/config_app/config_endpoints/common.py b/config_app/config_endpoints/common.py index 160cf7068..2cb1c2dc8 100644 --- a/config_app/config_endpoints/common.py +++ b/config_app/config_endpoints/common.py @@ -5,6 +5,8 @@ import re from flask import make_response, render_template from flask_restful import reqparse +from config_app._init_config import ROOT_DIR + def truthy_bool(param): return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'} @@ -30,9 +32,9 @@ def _list_files(path, extension, contains=""): def join_path(dp, f): # Remove the static/ prefix. It is added in the template. - return os.path.join(dp, f)[len('static/'):] + return os.path.join(dp, f)[len(ROOT_DIR) + 1 + len('config_app/static/'):] - filepath = os.path.join('static/', path) + filepath = os.path.join(os.path.join(ROOT_DIR, 'config_app/static/'), path) return [join_path(dp, f) for dp, _, files in os.walk(filepath) for f in files if matches(f)] diff --git a/config_app/config_endpoints/exception.py b/config_app/config_endpoints/exception.py index 20b0976b3..33cb161d2 100644 --- a/config_app/config_endpoints/exception.py +++ b/config_app/config_endpoints/exception.py @@ -5,30 +5,7 @@ from werkzeug.exceptions import HTTPException class ApiErrorType(Enum): - external_service_timeout = 'external_service_timeout' invalid_request = 'invalid_request' - invalid_response = 'invalid_response' - invalid_token = 'invalid_token' - expired_token = 'expired_token' - insufficient_scope = 'insufficient_scope' - fresh_login_required = 'fresh_login_required' - exceeds_license = 'exceeds_license' - not_found = 'not_found' - downstream_issue = 'downstream_issue' - - -ERROR_DESCRIPTION = { - ApiErrorType.external_service_timeout.value: "An external service timed out. Retrying the request may resolve the issue.", - ApiErrorType.invalid_request.value: "The request was invalid. It may have contained invalid values or was improperly formatted.", - ApiErrorType.invalid_response.value: "The response was invalid.", - ApiErrorType.invalid_token.value: "The access token provided was invalid.", - ApiErrorType.expired_token.value: "The access token provided has expired.", - ApiErrorType.insufficient_scope.value: "The access token did not have sufficient scope to access the requested resource.", - ApiErrorType.fresh_login_required.value: "The action requires a fresh login to succeed.", - ApiErrorType.exceeds_license.value: "The action was refused because the current license does not allow it.", - ApiErrorType.not_found.value: "The resource was not found.", - ApiErrorType.downstream_issue.value: "An error occurred in a downstream service.", -} class ApiException(HTTPException): @@ -79,10 +56,6 @@ class ApiException(HTTPException): return rv -class ExternalServiceError(ApiException): - def __init__(self, error_description, payload=None): - ApiException.__init__(self, ApiErrorType.external_service_timeout, 520, error_description, payload) - class InvalidRequest(ApiException): def __init__(self, error_description, payload=None): @@ -92,32 +65,3 @@ class InvalidRequest(ApiException): class InvalidResponse(ApiException): def __init__(self, error_description, payload=None): ApiException.__init__(self, ApiErrorType.invalid_response, 400, error_description, payload) - - -class InvalidToken(ApiException): - def __init__(self, error_description, payload=None): - ApiException.__init__(self, ApiErrorType.invalid_token, 401, error_description, payload) - -class ExpiredToken(ApiException): - def __init__(self, error_description, payload=None): - ApiException.__init__(self, ApiErrorType.expired_token, 401, error_description, payload) - - -class FreshLoginRequired(ApiException): - def __init__(self, payload=None): - ApiException.__init__(self, ApiErrorType.fresh_login_required, 401, "Requires fresh login", payload) - - -class ExceedsLicenseException(ApiException): - def __init__(self, payload=None): - ApiException.__init__(self, ApiErrorType.exceeds_license, 402, 'Payment Required', payload) - - -class NotFound(ApiException): - def __init__(self, payload=None): - ApiException.__init__(self, ApiErrorType.not_found, 404, 'Not Found', payload) - - -class DownstreamIssue(ApiException): - def __init__(self, error_description, payload=None): - ApiException.__init__(self, ApiErrorType.downstream_issue, 520, error_description, payload) diff --git a/config_app/config_endpoints/setup_web.py b/config_app/config_endpoints/setup_web.py index 541aa3df3..b9aba06c4 100644 --- a/config_app/config_endpoints/setup_web.py +++ b/config_app/config_endpoints/setup_web.py @@ -1,13 +1,15 @@ from flask import Blueprint +from cachetools import lru_cache + from config_app.config_endpoints.common import render_page_template from config_app.config_endpoints.api.discovery import generate_route_data -# from config_util.cache import no_cache +from config_app.config_endpoints.api import no_cache setup_web = Blueprint('setup_web', __name__, template_folder='templates') -# @lru_cache(maxsize=1) +@lru_cache(maxsize=1) def _get_route_data(): return generate_route_data() @@ -16,7 +18,7 @@ def render_page_template_with_routedata(name, *args, **kwargs): return render_page_template(name, _get_route_data(), *args, **kwargs) -# @no_cache +@no_cache @setup_web.route('/', methods=['GET'], defaults={'path': ''}) def index(path, **kwargs): return render_page_template_with_routedata('index.html', js_bundle_name='configapp', **kwargs) diff --git a/config_app/config_test/testconfig.py b/config_app/config_test/testconfig.py deleted file mode 100644 index 2ba731589..000000000 --- a/config_app/config_test/testconfig.py +++ /dev/null @@ -1,108 +0,0 @@ -import os - -from datetime import datetime, timedelta -from tempfile import NamedTemporaryFile - -from config import DefaultConfig - - -class FakeTransaction(object): - def __enter__(self): - return self - - def __exit__(self, exc_type, value, traceback): - pass - - -TEST_DB_FILE = NamedTemporaryFile(delete=True) - - -class TestConfig(DefaultConfig): - TESTING = True - SECRET_KEY = 'a36c9d7d-25a9-4d3f-a586-3d2f8dc40a83' - BILLING_TYPE = 'FakeStripe' - - TEST_DB_FILE = TEST_DB_FILE - DB_URI = os.environ.get('TEST_DATABASE_URI', 'sqlite:///{0}'.format(TEST_DB_FILE.name)) - DB_CONNECTION_ARGS = { - 'threadlocals': True, - 'autorollback': True, - } - - @staticmethod - def create_transaction(db): - return FakeTransaction() - - DB_TRANSACTION_FACTORY = create_transaction - - DISTRIBUTED_STORAGE_CONFIG = {'local_us': ['FakeStorage', {}], 'local_eu': ['FakeStorage', {}]} - DISTRIBUTED_STORAGE_PREFERENCE = ['local_us'] - - BUILDLOGS_MODULE_AND_CLASS = ('test.testlogs', 'testlogs.TestBuildLogs') - BUILDLOGS_OPTIONS = ['devtable', 'building', 'deadbeef-dead-beef-dead-beefdeadbeef', False] - - USERFILES_LOCATION = 'local_us' - - FEATURE_SUPER_USERS = True - FEATURE_BILLING = True - FEATURE_MAILING = True - SUPER_USERS = ['devtable'] - - LICENSE_USER_LIMIT = 500 - LICENSE_EXPIRATION = datetime.now() + timedelta(weeks=520) - LICENSE_EXPIRATION_WARNING = datetime.now() + timedelta(weeks=520) - - FEATURE_GITHUB_BUILD = True - FEATURE_BITTORRENT = True - FEATURE_ACI_CONVERSION = True - - CLOUDWATCH_NAMESPACE = None - - FEATURE_SECURITY_SCANNER = True - FEATURE_SECURITY_NOTIFICATIONS = True - SECURITY_SCANNER_ENDPOINT = 'http://fakesecurityscanner/' - SECURITY_SCANNER_API_VERSION = 'v1' - SECURITY_SCANNER_ENGINE_VERSION_TARGET = 1 - SECURITY_SCANNER_API_TIMEOUT_SECONDS = 1 - - FEATURE_SIGNING = True - - SIGNING_ENGINE = 'gpg2' - - GPG2_PRIVATE_KEY_NAME = 'EEB32221' - GPG2_PRIVATE_KEY_FILENAME = 'test/data/signing-private.gpg' - GPG2_PUBLIC_KEY_FILENAME = 'test/data/signing-public.gpg' - - INSTANCE_SERVICE_KEY_KID_LOCATION = 'test/data/test.kid' - INSTANCE_SERVICE_KEY_LOCATION = 'test/data/test.pem' - - PROMETHEUS_AGGREGATOR_URL = None - - GITHUB_LOGIN_CONFIG = {} - GOOGLE_LOGIN_CONFIG = {} - - FEATURE_GITHUB_LOGIN = True - FEATURE_GOOGLE_LOGIN = True - - TESTOIDC_LOGIN_CONFIG = { - 'CLIENT_ID': 'foo', - 'CLIENT_SECRET': 'bar', - 'OIDC_SERVER': 'http://fakeoidc', - 'DEBUGGING': True, - 'LOGIN_BINDING_FIELD': 'sub', - } - - RECAPTCHA_SITE_KEY = 'somekey' - RECAPTCHA_SECRET_KEY = 'somesecretkey' - - FEATURE_APP_REGISTRY = True - FEATURE_TEAM_SYNCING = True - FEATURE_CHANGE_TAG_EXPIRATION = True - - TAG_EXPIRATION_OPTIONS = ['0s', '1s', '1d', '1w', '2w', '4w'] - - DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT = None - - DATA_MODEL_CACHE_CONFIG = { - 'engine': 'inmemory', - } diff --git a/config_app/config_util/config/basefileprovider.py b/config_app/config_util/config/basefileprovider.py index 0ed1e9d35..8929845c8 100644 --- a/config_app/config_util/config/basefileprovider.py +++ b/config_app/config_util/config/basefileprovider.py @@ -2,7 +2,7 @@ import os import logging from config_app.config_util.config.baseprovider import (BaseProvider, import_yaml, export_yaml, - CannotWriteConfigException) + CannotWriteConfigException) logger = logging.getLogger(__name__) diff --git a/config_app/config_util/config/baseprovider.py b/config_app/config_util/config/baseprovider.py index 6fef3b870..5a616895f 100644 --- a/config_app/config_util/config/baseprovider.py +++ b/config_app/config_util/config/baseprovider.py @@ -6,7 +6,7 @@ from six import add_metaclass from jsonschema import validate, ValidationError -from config_app.config_util.config.schema import CONFIG_SCHEMA +from util.config.schema import CONFIG_SCHEMA logger = logging.getLogger(__name__) diff --git a/config_app/config_util/config/schema.py b/config_app/config_util/config/schema.py deleted file mode 100644 index db38b8872..000000000 --- a/config_app/config_util/config/schema.py +++ /dev/null @@ -1,914 +0,0 @@ -# INTERNAL_ONLY_PROPERTIES defines the properties in the config that, while settable, should -# not be documented for external users. These will generally be used for internal test or only -# given to customers when they have been briefed on the side effects of using them. -INTERNAL_ONLY_PROPERTIES = { - '__module__', - '__doc__', - 'create_transaction', - - 'TESTING', - 'SEND_FILE_MAX_AGE_DEFAULT', - - 'REPLICATION_QUEUE_NAME', - 'DOCKERFILE_BUILD_QUEUE_NAME', - 'CHUNK_CLEANUP_QUEUE_NAME', - 'SECSCAN_NOTIFICATION_QUEUE_NAME', - 'SECURITY_SCANNER_ISSUER_NAME', - 'NOTIFICATION_QUEUE_NAME', - 'NAMESPACE_GC_QUEUE_NAME', - - 'FEATURE_BILLING', - 'FEATURE_SUPPORT_CHAT', - 'BILLING_TYPE', - - 'INSTANCE_SERVICE_KEY_LOCATION', - 'INSTANCE_SERVICE_KEY_REFRESH', - 'INSTANCE_SERVICE_KEY_SERVICE', - 'INSTANCE_SERVICE_KEY_KID_LOCATION', - 'INSTANCE_SERVICE_KEY_EXPIRATION', - 'UNAPPROVED_SERVICE_KEY_TTL_SEC', - 'EXPIRED_SERVICE_KEY_TTL_SEC', - 'REGISTRY_JWT_AUTH_MAX_FRESH_S', - - 'BITTORRENT_FILENAME_PEPPER', - 'BITTORRENT_WEBSEED_LIFETIME', - - 'SERVICE_LOG_ACCOUNT_ID', - 'BUILDLOGS_OPTIONS', - 'LIBRARY_NAMESPACE', - 'STAGGER_WORKERS', - 'QUEUE_WORKER_METRICS_REFRESH_SECONDS', - 'PUSH_TEMP_TAG_EXPIRATION_SEC', - 'GARBAGE_COLLECTION_FREQUENCY', - 'PAGE_TOKEN_KEY', - 'BUILD_MANAGER', - 'JWTPROXY_AUDIENCE', - 'SYSTEM_SERVICE_BLACKLIST', - 'JWTPROXY_SIGNER', - 'SECURITY_SCANNER_INDEXING_MIN_ID', - 'STATIC_SITE_BUCKET', - 'LABEL_KEY_RESERVED_PREFIXES', - 'TEAM_SYNC_WORKER_FREQUENCY', - 'DOCUMENTATION_METADATA', - 'DOCUMENTATION_LOCATION', - 'JSONIFY_PRETTYPRINT_REGULAR', - 'SYSTEM_LOGS_FILE', - 'SYSTEM_LOGS_PATH', - 'SYSTEM_SERVICES_PATH', - 'TUF_GUN_PREFIX', - 'LOGGING_LEVEL', - 'SIGNED_GRANT_EXPIRATION_SEC', - 'PROMETHEUS_AGGREGATOR_URL', - 'DB_TRANSACTION_FACTORY', - 'NOTIFICATION_SEND_TIMEOUT', - 'QUEUE_METRICS_TYPE', - 'MAIL_FAIL_SILENTLY', - 'LOCAL_OAUTH_HANDLER', - 'USE_CDN', - 'ANALYTICS_TYPE', - 'LAST_ACCESSED_UPDATE_THRESHOLD_S', - - 'EXCEPTION_LOG_TYPE', - 'SENTRY_DSN', - 'SENTRY_PUBLIC_DSN', - - 'BILLED_NAMESPACE_MAXIMUM_BUILD_COUNT', - 'THREAT_NAMESPACE_MAXIMUM_BUILD_COUNT', - - 'SECURITY_SCANNER_ENDPOINT_BATCH', - 'SECURITY_SCANNER_API_TIMEOUT_SECONDS', - 'SECURITY_SCANNER_API_TIMEOUT_POST_SECONDS', - 'SECURITY_SCANNER_ENGINE_VERSION_TARGET', - 'SECURITY_SCANNER_READONLY_FAILOVER_ENDPOINTS', - 'SECURITY_SCANNER_API_VERSION', - - 'DATA_MODEL_CACHE_CONFIG', - - # TODO: move this into the schema once we support signing in QE. - 'FEATURE_SIGNING', - 'TUF_SERVER', -} - -CONFIG_SCHEMA = { - 'type': 'object', - 'description': 'Schema for Quay configuration', - 'required': [ - 'PREFERRED_URL_SCHEME', - 'SERVER_HOSTNAME', - 'DB_URI', - 'AUTHENTICATION_TYPE', - 'DISTRIBUTED_STORAGE_CONFIG', - 'BUILDLOGS_REDIS', - 'USER_EVENTS_REDIS', - 'DISTRIBUTED_STORAGE_PREFERENCE', - 'DEFAULT_TAG_EXPIRATION', - 'TAG_EXPIRATION_OPTIONS', - ], - 'properties': { - # Hosting. - 'PREFERRED_URL_SCHEME': { - 'type': 'string', - 'description': 'The URL scheme to use when hitting Quay. If Quay is behind SSL *at all*, this *must* be `https`', - 'enum': ['http', 'https'], - 'x-example': 'https', - }, - 'SERVER_HOSTNAME': { - 'type': 'string', - 'description': 'The URL at which Quay is accessible, without the scheme.', - 'x-example': 'quay.io', - }, - 'EXTERNAL_TLS_TERMINATION': { - 'type': 'boolean', - 'description': 'If TLS is supported, but terminated at a layer before Quay, must be true.', - 'x-example': True, - }, - - # User-visible configuration. - 'REGISTRY_TITLE': { - 'type': 'string', - 'description': 'If specified, the long-form title for the registry. Defaults to `Quay Enterprise`.', - 'x-example': 'Corp Container Service', - }, - 'REGISTRY_TITLE_SHORT': { - 'type': 'string', - 'description': 'If specified, the short-form title for the registry. Defaults to `Quay Enterprise`.', - 'x-example': 'CCS', - }, - 'CONTACT_INFO': { - 'type': 'array', - 'minItems': 1, - 'uniqueItems': True, - 'description': 'If specified, contact information to display on the contact page. ' + - 'If only a single piece of contact information is specified, the contact footer will link directly.', - 'items': [ - { - 'type': 'string', - 'pattern': '^mailto:(.)+$', - 'x-example': 'mailto:support@quay.io', - 'description': 'Adds a link to send an e-mail', - }, - { - 'type': 'string', - 'pattern': '^irc://(.)+$', - 'x-example': 'irc://chat.freenode.net:6665/quay', - 'description': 'Adds a link to visit an IRC chat room', - }, - { - 'type': 'string', - 'pattern': '^tel:(.)+$', - 'x-example': 'tel:+1-888-930-3475', - 'description': 'Adds a link to call a phone number', - }, - { - 'type': 'string', - 'pattern': '^http(s)?://(.)+$', - 'x-example': 'https://twitter.com/quayio', - 'description': 'Adds a link to a defined URL', - }, - ], - }, - 'SEARCH_RESULTS_PER_PAGE' : { - 'type': 'number', - 'description': 'Number of results returned per page by search page. Defaults to 10', - 'x-example': 10, - }, - 'SEARCH_MAX_RESULT_PAGE_COUNT' : { - 'type': 'number', - 'description': 'Maximum number of pages the user can paginate in search before they are limited. Defaults to 10', - 'x-example': 10, - }, - - # E-mail. - 'FEATURE_MAILING': { - 'type': 'boolean', - 'description': 'Whether emails are enabled. Defaults to True', - 'x-example': True, - }, - 'MAIL_SERVER': { - 'type': 'string', - 'description': 'The SMTP server to use for sending e-mails. Only required if FEATURE_MAILING is set to true.', - 'x-example': 'smtp.somedomain.com', - }, - 'MAIL_USE_TLS': { - 'type': 'boolean', - 'description': 'If specified, whether to use TLS for sending e-mails.', - 'x-example': True, - }, - 'MAIL_PORT': { - 'type': 'number', - 'description': 'The SMTP port to use. If not specified, defaults to 587.', - 'x-example': 588, - }, - 'MAIL_USERNAME': { - 'type': ['string', 'null'], - 'description': 'The SMTP username to use when sending e-mails.', - 'x-example': 'myuser', - }, - 'MAIL_PASSWORD': { - 'type': ['string', 'null'], - 'description': 'The SMTP password to use when sending e-mails.', - 'x-example': 'mypassword', - }, - 'MAIL_DEFAULT_SENDER': { - 'type': ['string', 'null'], - 'description': 'If specified, the e-mail address used as the `from` when Quay sends e-mails. If none, defaults to `support@quay.io`.', - 'x-example': 'support@myco.com', - }, - - # Database. - 'DB_URI': { - 'type': 'string', - 'description': 'The URI at which to access the database, including any credentials.', - 'x-example': 'mysql+pymysql://username:password@dns.of.database/quay', - 'x-reference': 'https://www.postgresql.org/docs/9.3/static/libpq-connect.html#AEN39495', - }, - 'DB_CONNECTION_ARGS': { - 'type': 'object', - 'description': 'If specified, connection arguments for the database such as timeouts and SSL.', - 'properties': { - 'threadlocals': { - 'type': 'boolean', - 'description': 'Whether to use thread-local connections. Should *ALWAYS* be `true`' - }, - 'autorollback': { - 'type': 'boolean', - 'description': 'Whether to use auto-rollback connections. Should *ALWAYS* be `true`' - }, - 'ssl': { - 'type': 'object', - 'description': 'SSL connection configuration', - 'properties': { - 'ca': { - 'type': 'string', - 'description': '*Absolute container path* to the CA certificate to use for SSL connections', - 'x-example': 'conf/stack/ssl-ca-cert.pem', - }, - }, - 'required': ['ca'], - }, - }, - 'required': ['threadlocals', 'autorollback'], - }, - 'ALLOW_PULLS_WITHOUT_STRICT_LOGGING': { - 'type': 'boolean', - 'description': 'If true, pulls in which the pull audit log entry cannot be written will ' + - 'still succeed. Useful if the database can fallback into a read-only state ' + - 'and it is desired for pulls to continue during that time. Defaults to False.', - 'x-example': True, - }, - - # Storage. - 'FEATURE_STORAGE_REPLICATION': { - 'type': 'boolean', - 'description': 'Whether to automatically replicate between storage engines. Defaults to False', - 'x-example': False, - }, - 'FEATURE_PROXY_STORAGE': { - 'type': 'boolean', - 'description': 'Whether to proxy all direct download URLs in storage via the registry nginx. Defaults to False', - 'x-example': False, - }, - 'MAXIMUM_LAYER_SIZE': { - 'type': 'string', - 'description': 'Maximum allowed size of an image layer. Defaults to 20G', - 'x-example': '100G', - 'pattern': '^[0-9]+(G|M)$', - }, - 'DISTRIBUTED_STORAGE_CONFIG': { - 'type': 'object', - 'description': 'Configuration for storage engine(s) to use in Quay. Each key is a unique ID' + - ' for a storage engine, with the value being a tuple of the type and ' + - ' configuration for that engine.', - 'x-example': { - 'local_storage': ['LocalStorage', {'storage_path': 'some/path/'}], - }, - 'items': { - 'type': 'array', - }, - }, - 'DISTRIBUTED_STORAGE_PREFERENCE': { - 'type': 'array', - 'description': 'The preferred storage engine(s) (by ID in DISTRIBUTED_STORAGE_CONFIG) to ' + - 'use. A preferred engine means it is first checked for pullig and images are ' + - 'pushed to it.', - 'items': { - 'type': 'string', - 'uniqueItems': True, - }, - 'x-example': ['s3_us_east', 's3_us_west'], - }, - 'DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS': { - 'type': 'array', - 'description': 'The list of storage engine(s) (by ID in DISTRIBUTED_STORAGE_CONFIG) whose ' + - 'images should be fully replicated, by default, to all other storage engines.', - 'items': { - 'type': 'string', - 'uniqueItems': True, - }, - 'x-example': ['s3_us_east', 's3_us_west'], - }, - 'USERFILES_LOCATION': { - 'type': 'string', - 'description': 'ID of the storage engine in which to place user-uploaded files', - 'x-example': 's3_us_east', - }, - 'USERFILES_PATH': { - 'type': 'string', - 'description': 'Path under storage in which to place user-uploaded files', - 'x-example': 'userfiles', - }, - 'ACTION_LOG_ARCHIVE_LOCATION': { - 'type': 'string', - 'description': 'If action log archiving is enabled, the storage engine in which to place the ' + - 'archived data.', - 'x-example': 's3_us_east', - }, - 'ACTION_LOG_ARCHIVE_PATH': { - 'type': 'string', - 'description': 'If action log archiving is enabled, the path in storage in which to place the ' + - 'archived data.', - 'x-example': 'archives/actionlogs', - }, - 'LOG_ARCHIVE_LOCATION': { - 'type': 'string', - 'description': 'If builds are enabled, the storage engine in which to place the ' + - 'archived build logs.', - 'x-example': 's3_us_east', - }, - 'LOG_ARCHIVE_PATH': { - 'type': 'string', - 'description': 'If builds are enabled, the path in storage in which to place the ' + - 'archived build logs.', - 'x-example': 'archives/buildlogs', - }, - - # Authentication. - 'AUTHENTICATION_TYPE': { - 'type': 'string', - 'description': 'The authentication engine to use for credential authentication.', - 'x-example': 'Database', - 'enum': ['Database', 'LDAP', 'JWT', 'Keystone', 'OIDC'], - }, - 'SUPER_USERS': { - 'type': 'array', - 'description': 'Quay usernames of those users to be granted superuser privileges', - 'uniqueItems': True, - 'items': { - 'type': 'string', - }, - }, - 'DIRECT_OAUTH_CLIENTID_WHITELIST': { - 'type': 'array', - 'description': 'A list of client IDs of *Quay-managed* applications that are allowed ' + - 'to perform direct OAuth approval without user approval.', - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/direct-oauth.html', - 'uniqueItems': True, - 'items': { - 'type': 'string', - }, - }, - - # Redis. - 'BUILDLOGS_REDIS': { - 'type': 'object', - 'description': 'Connection information for Redis for build logs caching', - 'required': ['host'], - 'properties': { - 'host': { - 'type': 'string', - 'description': 'The hostname at which Redis is accessible', - 'x-example': 'my.redis.cluster', - }, - 'port': { - 'type': 'number', - 'description': 'The port at which Redis is accessible', - 'x-example': 1234, - }, - 'password': { - 'type': 'string', - 'description': 'The password to connect to the Redis instance', - 'x-example': 'mypassword', - }, - }, - }, - 'USER_EVENTS_REDIS': { - 'type': 'object', - 'description': 'Connection information for Redis for user event handling', - 'required': ['host'], - 'properties': { - 'host': { - 'type': 'string', - 'description': 'The hostname at which Redis is accessible', - 'x-example': 'my.redis.cluster', - }, - 'port': { - 'type': 'number', - 'description': 'The port at which Redis is accessible', - 'x-example': 1234, - }, - 'password': { - 'type': 'string', - 'description': 'The password to connect to the Redis instance', - 'x-example': 'mypassword', - }, - }, - }, - - # OAuth configuration. - 'GITHUB_LOGIN_CONFIG': { - 'type': ['object', 'null'], - 'description': 'Configuration for using GitHub (Enterprise) as an external login provider', - 'required': ['CLIENT_ID', 'CLIENT_SECRET'], - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/github-auth.html', - 'properties': { - 'GITHUB_ENDPOINT': { - 'type': 'string', - 'description': 'The endpoint of the GitHub (Enterprise) being hit', - 'x-example': 'https://github.com/', - }, - 'API_ENDPOINT': { - 'type': 'string', - 'description': 'The endpoint of the GitHub (Enterprise) API to use. Must be overridden for github.com', - 'x-example': 'https://api.github.com/', - }, - 'CLIENT_ID': { - 'type': 'string', - 'description': 'The registered client ID for this Quay instance; cannot be shared with GITHUB_TRIGGER_CONFIG', - 'x-example': '0e8dbe15c4c7630b6780', - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/github-app.html', - }, - 'CLIENT_SECRET': { - 'type': 'string', - 'description': 'The registered client secret for this Quay instance', - 'x-example': 'e4a58ddd3d7408b7aec109e85564a0d153d3e846', - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/github-app.html', - }, - 'ORG_RESTRICT': { - 'type': 'boolean', - 'description': 'If true, only users within the organization whitelist can login using this provider', - 'x-example': True, - }, - 'ALLOWED_ORGANIZATIONS': { - 'type': 'array', - 'description': 'The names of the GitHub (Enterprise) organizations whitelisted to work with the ORG_RESTRICT option', - 'uniqueItems': True, - 'items': { - 'type': 'string', - }, - }, - }, - }, - 'BITBUCKET_TRIGGER_CONFIG': { - 'type': ['object', 'null'], - 'description': 'Configuration for using BitBucket for build triggers', - 'required': ['CONSUMER_KEY', 'CONSUMER_SECRET'], - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/bitbucket-build.html', - 'properties': { - 'CONSUMER_KEY': { - 'type': 'string', - 'description': 'The registered consumer key (client ID) for this Quay instance', - 'x-example': '0e8dbe15c4c7630b6780', - }, - 'CONSUMER_SECRET': { - 'type': 'string', - 'description': 'The registered consumer secret (client secret) for this Quay instance', - 'x-example': 'e4a58ddd3d7408b7aec109e85564a0d153d3e846', - }, - }, - }, - 'GITHUB_TRIGGER_CONFIG': { - 'type': ['object', 'null'], - 'description': 'Configuration for using GitHub (Enterprise) for build triggers', - 'required': ['GITHUB_ENDPOINT', 'CLIENT_ID', 'CLIENT_SECRET'], - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/github-build.html', - 'properties': { - 'GITHUB_ENDPOINT': { - 'type': 'string', - 'description': 'The endpoint of the GitHub (Enterprise) being hit', - 'x-example': 'https://github.com/', - }, - 'API_ENDPOINT': { - 'type': 'string', - 'description': 'The endpoint of the GitHub (Enterprise) API to use. Must be overridden for github.com', - 'x-example': 'https://api.github.com/', - }, - 'CLIENT_ID': { - 'type': 'string', - 'description': 'The registered client ID for this Quay instance; cannot be shared with GITHUB_LOGIN_CONFIG', - 'x-example': '0e8dbe15c4c7630b6780', - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/github-app.html', - }, - 'CLIENT_SECRET': { - 'type': 'string', - 'description': 'The registered client secret for this Quay instance', - 'x-example': 'e4a58ddd3d7408b7aec109e85564a0d153d3e846', - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/github-app.html', - }, - }, - }, - 'GOOGLE_LOGIN_CONFIG': { - 'type': ['object', 'null'], - 'description': 'Configuration for using Google for external authentication', - 'required': ['CLIENT_ID', 'CLIENT_SECRET'], - 'properties': { - 'CLIENT_ID': { - 'type': 'string', - 'description': 'The registered client ID for this Quay instance', - 'x-example': '0e8dbe15c4c7630b6780', - }, - 'CLIENT_SECRET': { - 'type': 'string', - 'description': 'The registered client secret for this Quay instance', - 'x-example': 'e4a58ddd3d7408b7aec109e85564a0d153d3e846', - }, - }, - }, - 'GITLAB_TRIGGER_CONFIG': { - 'type': ['object', 'null'], - 'description': 'Configuration for using Gitlab (Enterprise) for external authentication', - 'required': ['GITLAB_ENDPOINT', 'CLIENT_ID', 'CLIENT_SECRET'], - 'properties': { - 'GITLAB_ENDPOINT': { - 'type': 'string', - 'description': 'The endpoint at which Gitlab(Enterprise) is running', - 'x-example': 'https://gitlab.com', - }, - 'CLIENT_ID': { - 'type': 'string', - 'description': 'The registered client ID for this Quay instance', - 'x-example': '0e8dbe15c4c7630b6780', - }, - 'CLIENT_SECRET': { - 'type': 'string', - 'description': 'The registered client secret for this Quay instance', - 'x-example': 'e4a58ddd3d7408b7aec109e85564a0d153d3e846', - }, - }, - }, - - # Health. - 'HEALTH_CHECKER': { - 'description': 'The configured health check.', - 'x-example': ('RDSAwareHealthCheck', {'access_key': 'foo', 'secret_key': 'bar'}), - }, - - # Metrics. - 'PROMETHEUS_NAMESPACE': { - 'type': 'string', - 'description': 'The prefix applied to all exposed Prometheus metrics. Defaults to `quay`', - 'x-example': 'myregistry', - }, - - # Misc configuration. - 'BLACKLIST_V2_SPEC': { - 'type': 'string', - 'description': 'The Docker CLI versions to which Quay will respond that V2 is *unsupported*. Defaults to `<1.6.0`', - 'x-reference': 'http://pythonhosted.org/semantic_version/reference.html#semantic_version.Spec', - 'x-example': '<1.8.0', - }, - 'USER_RECOVERY_TOKEN_LIFETIME': { - 'type': 'string', - 'description': 'The length of time a token for recovering a user accounts is valid. Defaults to 30m.', - 'x-example': '10m', - 'pattern': '^[0-9]+(w|m|d|h|s)$', - }, - 'SESSION_COOKIE_SECURE': { - 'type': 'boolean', - 'description': 'Whether the `secure` property should be set on session cookies. ' + - 'Defaults to False. Recommended to be True for all installations using SSL.', - 'x-example': True, - 'x-reference': 'https://en.wikipedia.org/wiki/Secure_cookies', - }, - 'PUBLIC_NAMESPACES': { - 'type': 'array', - 'description': 'If a namespace is defined in the public namespace list, then it will appear on *all*' + - ' user\'s repository list pages, regardless of whether that user is a member of the namespace.' + - ' Typically, this is used by an enterprise customer in configuring a set of "well-known"' + - ' namespaces.', - 'uniqueItems': True, - 'items': { - 'type': 'string', - }, - }, - 'AVATAR_KIND': { - 'type': 'string', - 'description': 'The types of avatars to display, either generated inline (local) or Gravatar (gravatar)', - 'enum': ['local', 'gravatar'], - }, - 'V2_PAGINATION_SIZE': { - 'type': 'number', - 'description': 'The number of results returned per page in V2 registry APIs', - 'x-example': 100, - }, - 'ENABLE_HEALTH_DEBUG_SECRET': { - 'type': ['string', 'null'], - 'description': 'If specified, a secret that can be given to health endpoints to see full debug info when' + - 'not authenticated as a superuser', - 'x-example': 'somesecrethere', - }, - 'BROWSER_API_CALLS_XHR_ONLY': { - 'type': 'boolean', - 'description': 'If enabled, only API calls marked as being made by an XHR will be allowed from browsers. Defaults to True.', - 'x-example': False, - }, - - # Time machine and tag expiration settings. - 'FEATURE_CHANGE_TAG_EXPIRATION': { - 'type': 'boolean', - 'description': 'Whether users and organizations are allowed to change the tag expiration for tags in their namespace. Defaults to True.', - 'x-example': False, - }, - 'DEFAULT_TAG_EXPIRATION': { - 'type': 'string', - 'description': 'The default, configurable tag expiration time for time machine. Defaults to `2w`.', - 'pattern': '^[0-9]+(w|m|d|h|s)$', - }, - 'TAG_EXPIRATION_OPTIONS': { - 'type': 'array', - 'description': 'The options that users can select for expiration of tags in their namespace (if enabled)', - 'items': { - 'type': 'string', - 'pattern': '^[0-9]+(w|m|d|h|s)$', - }, - }, - - # Team syncing. - 'FEATURE_TEAM_SYNCING': { - 'type': 'boolean', - 'description': 'Whether to allow for team membership to be synced from a backing group in the authentication engine (LDAP or Keystone)', - 'x-example': True, - }, - 'TEAM_RESYNC_STALE_TIME': { - 'type': 'string', - 'description': 'If team syncing is enabled for a team, how often to check its membership and resync if necessary (Default: 30m)', - 'x-example': '2h', - 'pattern': '^[0-9]+(w|m|d|h|s)$', - }, - 'FEATURE_NONSUPERUSER_TEAM_SYNCING_SETUP': { - 'type': 'boolean', - 'description': 'If enabled, non-superusers can setup syncing on teams to backing LDAP or Keystone. Defaults To False.', - 'x-example': True, - }, - - # Security scanning. - 'FEATURE_SECURITY_SCANNER': { - 'type': 'boolean', - 'description': 'Whether to turn of/off the security scanner. Defaults to False', - 'x-example': False, - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/security-scanning.html', - }, - 'FEATURE_SECURITY_NOTIFICATIONS': { - 'type': 'boolean', - 'description': 'If the security scanner is enabled, whether to turn of/off security notificaitons. Defaults to False', - 'x-example': False, - }, - 'SECURITY_SCANNER_ENDPOINT' : { - 'type': 'string', - 'pattern': '^http(s)?://(.)+$', - 'description': 'The endpoint for the security scanner', - 'x-example': 'http://192.168.99.101:6060' , - }, - 'SECURITY_SCANNER_INDEXING_INTERVAL': { - 'type': 'number', - 'description': 'The number of seconds between indexing intervals in the security scanner. Defaults to 30.', - 'x-example': 30, - }, - - # Bittorrent support. - 'FEATURE_BITTORRENT': { - 'type': 'boolean', - 'description': 'Whether to allow using Bittorrent-based pulls. Defaults to False', - 'x-example': False, - 'x-reference': 'https://coreos.com/quay-enterprise/docs/latest/bittorrent.html', - }, - 'BITTORRENT_PIECE_SIZE': { - 'type': 'number', - 'description': 'The bittorent piece size to use. If not specified, defaults to 512 * 1024.', - 'x-example': 512 * 1024, - }, - 'BITTORRENT_ANNOUNCE_URL': { - 'type': 'string', - 'pattern': '^http(s)?://(.)+$', - 'description': 'The URL of the announce endpoint on the bittorrent tracker', - 'x-example': 'https://localhost:6881/announce', - }, - - # Build - 'FEATURE_GITHUB_BUILD': { - 'type': 'boolean', - 'description': 'Whether to support GitHub build triggers. Defaults to False', - 'x-example': False, - }, - 'FEATURE_BITBUCKET_BUILD': { - 'type': 'boolean', - 'description': 'Whether to support Bitbucket build triggers. Defaults to False', - 'x-example': False, - }, - 'FEATURE_GITLAB_BUILD': { - 'type': 'boolean', - 'description': 'Whether to support GitLab build triggers. Defaults to False', - 'x-example': False, - }, - 'FEATURE_BUILD_SUPPORT': { - 'type': 'boolean', - 'description': 'Whether to support Dockerfile build. Defaults to True', - 'x-example': True, - }, - 'DEFAULT_NAMESPACE_MAXIMUM_BUILD_COUNT': { - 'type': ['number', 'null'], - 'description': 'If not None, the default maximum number of builds that can be queued in a namespace.', - 'x-example': 20, - }, - 'SUCCESSIVE_TRIGGER_INTERNAL_ERROR_DISABLE_THRESHOLD': { - 'type': ['number', 'null'], - 'description': 'If not None, the number of successive internal errors that can occur before a build trigger is automatically disabled. Defaults to 5.', - 'x-example': 10, - }, - 'SUCCESSIVE_TRIGGER_FAILURE_DISABLE_THRESHOLD': { - 'type': ['number', 'null'], - 'description': 'If not None, the number of successive failures that can occur before a build trigger is automatically disabled. Defaults to 100.', - 'x-example': 50, - }, - - # Login - 'FEATURE_GITHUB_LOGIN': { - 'type': 'boolean', - 'description': 'Whether GitHub login is supported. Defaults to False', - 'x-example': False, - }, - 'FEATURE_GOOGLE_LOGIN': { - 'type': 'boolean', - 'description': 'Whether Google login is supported. Defaults to False', - 'x-example': False, - }, - - # Recaptcha - 'FEATURE_RECAPTCHA': { - 'type': 'boolean', - 'description': 'Whether Recaptcha is necessary for user login and recovery. Defaults to False', - 'x-example': False, - 'x-reference': 'https://www.google.com/recaptcha/intro/', - }, - 'RECAPTCHA_SITE_KEY': { - 'type': ['string', 'null'], - 'description': 'If recaptcha is enabled, the site key for the Recaptcha service', - }, - 'RECAPTCHA_SECRET_KEY': { - 'type': ['string', 'null'], - 'description': 'If recaptcha is enabled, the secret key for the Recaptcha service', - }, - - # External application tokens. - 'FEATURE_APP_SPECIFIC_TOKENS': { - 'type': 'boolean', - 'description': 'If enabled, users can create tokens for use by the Docker CLI. Defaults to True', - 'x-example': False, - }, - - 'APP_SPECIFIC_TOKEN_EXPIRATION': { - 'type': ['string', 'null'], - 'description': 'The expiration for external app tokens. Defaults to None.', - 'pattern': '^[0-9]+(w|m|d|h|s)$', - }, - - 'EXPIRED_APP_SPECIFIC_TOKEN_GC': { - 'type': ['string', 'null'], - 'description': 'Duration of time expired external app tokens will remain before being garbage collected. Defaults to 1d.', - 'pattern': '^[0-9]+(w|m|d|h|s)$', - }, - - # Feature Flag: Permanent Sessions. - 'FEATURE_PERMANENT_SESSIONS': { - 'type': 'boolean', - 'description': 'Whether sessions are permanent. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Super User Support. - 'FEATURE_SUPER_USERS': { - 'type': 'boolean', - 'description': 'Whether super users are supported. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Anonymous Users. - 'FEATURE_ANONYMOUS_ACCESS': { - 'type': 'boolean', - 'description': ' Whether to allow anonymous users to browse and pull public repositories. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: User Creation. - 'FEATURE_USER_CREATION': { - 'type': 'boolean', - 'description': 'Whether users can be created (by non-super users). Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Invite Only User Creation. - 'FEATURE_INVITE_ONLY_USER_CREATION': { - 'type': 'boolean', - 'description': 'Whether users being created must be invited by another user. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Encrypted Basic Auth. - 'FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH': { - 'type': 'boolean', - 'description': 'Whether non-encrypted passwords (as opposed to encrypted tokens) can be used for basic auth. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Direct Login. - 'FEATURE_DIRECT_LOGIN': { - 'type': 'boolean', - 'description': 'Whether users can directly login to the UI. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Advertising V2. - 'FEATURE_ADVERTISE_V2': { - 'type': 'boolean', - 'description': 'Whether the v2/ endpoint is visible. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Log Rotation. - 'FEATURE_ACTION_LOG_ROTATION': { - 'type': 'boolean', - 'description': 'Whether or not to rotate old action logs to storage. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: ACI Conversion. - 'FEATURE_ACI_CONVERSION': { - 'type': 'boolean', - 'description': 'Whether to enable conversion to ACIs. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Library Support. - 'FEATURE_LIBRARY_SUPPORT': { - 'type': 'boolean', - 'description': 'Whether to allow for "namespace-less" repositories when pulling and pushing from Docker. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Require Team Invite. - 'FEATURE_REQUIRE_TEAM_INVITE': { - 'type': 'boolean', - 'description': 'Whether to require invitations when adding a user to a team. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: Collecting and Supporting Metadata. - 'FEATURE_USER_METADATA': { - 'type': 'boolean', - 'description': 'Whether to collect and support user metadata. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Support App Registry. - 'FEATURE_APP_REGISTRY': { - 'type': 'boolean', - 'description': 'Whether to enable support for App repositories. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Public Reposiotires in _catalog Endpoint. - 'FEATURE_PUBLIC_CATALOG': { - 'type': 'boolean', - 'description': 'If set to true, the _catalog endpoint returns public repositories. Otherwise, only private repositories can be returned. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Reader Build Logs. - 'FEATURE_READER_BUILD_LOGS': { - 'type': 'boolean', - 'description': 'If set to true, build logs may be read by those with read access to the repo, rather than only write access or admin access. Defaults to False', - 'x-example': False, - }, - - # Feature Flag: Usernames Autocomplete. - 'FEATURE_PARTIAL_USER_AUTOCOMPLETE': { - 'type': 'boolean', - 'description': 'If set to true, autocompletion will apply to partial usernames. Defaults to True', - 'x-example': True, - }, - - # Feature Flag: User log access. - 'FEATURE_USER_LOG_ACCESS': { - 'type': 'boolean', - 'description': 'If set to true, users will have access to audit logs for their namespace. Defaults to False', - 'x-example': True, - }, - - # Feature Flag: User renaming. - 'FEATURE_USER_RENAME': { - 'type': 'boolean', - 'description': 'If set to true, users can rename their own namespace. Defaults to False', - 'x-example': True, - }, - }, -} - diff --git a/config_app/config_util/workers.py b/config_app/config_util/workers.py deleted file mode 100644 index f86f1d1bf..000000000 --- a/config_app/config_util/workers.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import psutil - - -def get_worker_count(worker_kind_name, multiplier, minimum=None, maximum=None): - """ Returns the number of gunicorn workers to run for the given worker kind, - based on a combination of environment variable, multiplier, minimum (if any), - and number of accessible CPU cores. - """ - minimum = minimum or multiplier - maximum = maximum or (multiplier * multiplier) - - # Check for an override via an environment variable. - override_value = os.environ.get('WORKER_COUNT_' + worker_kind_name.upper()) - if override_value is not None: - return max(override_value, minimum) - - override_value = os.environ.get('WORKER_COUNT') - if override_value is not None: - return max(override_value, minimum) - - # Load the number of CPU cores via affinity, and use that to calculate the - # number of workers to run. - p = psutil.Process(os.getpid()) - - try: - cpu_count = len(p.cpu_affinity()) - except AttributeError: - # cpu_affinity isn't supported on this platform. Assume 2. - cpu_count = 2 - - return min(max(cpu_count * multiplier, minimum), maximum) diff --git a/config_app/init/service/gunicorn_web/run b/config_app/init/service/gunicorn_web/run index e7564a2c9..1fdf1870e 100755 --- a/config_app/init/service/gunicorn_web/run +++ b/config_app/init/service/gunicorn_web/run @@ -6,6 +6,6 @@ QUAYPATH=${QUAYPATH:-"."} QUAYCONF=${QUAYCONF:-"$QUAYPATH/conf"} cd ${QUAYDIR:-"/"} -PYTHONPATH=$QUAYPATH venv/bin/gunicorn -c $QUAYDIR/config_app/conf/gunicorn_local.py config_application:application +PYTHONPATH=$QUAYPATH venv/bin/gunicorn -c $QUAYDIR/config_app/conf/gunicorn_web.py config_application:application echo 'Gunicorn exited' \ No newline at end of file diff --git a/config_app/js/services/api-service.js b/config_app/js/services/api-service.js index 09b88bcfa..5163f1cba 100644 --- a/config_app/js/services/api-service.js +++ b/config_app/js/services/api-service.js @@ -3,13 +3,12 @@ * callbacks. Any method defined on the server is exposed here as an equivalent method. Also * defines some helper functions for working with API responses. */ -// console.log(angular.module('quay-config').requires); angular.module('quay-config').factory('ApiService', ['Restangular', '$q', 'UtilService', function(Restangular, $q, UtilService) { var apiService = {}; - // if (!window.__endpoints) { - // return apiService; - // } + if (!window.__endpoints) { + return apiService; + } var getResource = function(getMethod, operation, opt_parameters, opt_background) { var resource = {}; diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index 001396888..66e7c74a3 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -1,3 +1,4 @@ +# TODO to extract the discovery stuff into a util at the top level and then use it both here and config_app discovery.py """ API discovery information. """ import re diff --git a/local-config-app.sh b/local-config-app.sh index e2d63562e..9c6192200 100755 --- a/local-config-app.sh +++ b/local-config-app.sh @@ -7,7 +7,7 @@ cat << "EOF" \ \ \ \ / / | |__| | | |__| | / ____ \ | | | |____ | |__| | | . ` | | __| _| |_ | |__| | \ \/ \ \/ / \_ ___/ \____/ /_/ \_\ |_| \_____| \____/ |_| \_| |_| |_____| \_____| \__/ \__/ \ \__ - \___\ by CoreOS + \___\ by Red Hat Build, Store, and Distribute your Containers diff --git a/package.json b/package.json index 1a084ab37..6462e2d76 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "clean": "rm -f static/build/*", "clean-config-app": "rm -f config_app/static/build/*", - "watch-config-app": "npm run clean-config-app && cd config_app && webpack --watch" + "watch-config-app": "npm run clean-config-app && cd config_app && webpack --watch", + "build-config-app": "npm run clean-config-app && cd config_app && NODE_ENV=production webpack --progress" }, "repository": { "type": "git",