Exemple #1
0
def patch_request(request_id):
    """
    Modify the given request.

    :param int request_id: the request ID from the URL
    :return: a Flask JSON response
    :rtype: flask.Response
    :raise NotFound: if the request is not found
    :raise ValidationError: if the JSON is invalid
    """
    payload = flask.request.get_json()
    if not isinstance(payload, dict):
        raise ValidationError("The input data must be a JSON object")

    if not payload:
        raise ValidationError(
            "At least one key must be specified to update the request")

    valid_keys = {
        "dependencies",
        "environment_variables",
        "package",
        "package_subpath",
        "state",
        "state_reason",
    }
    invalid_keys = set(payload.keys()) - valid_keys
    if invalid_keys:
        raise ValidationError("The following keys are not allowed: {}".format(
            ", ".join(invalid_keys)))

    for key, value in payload.items():
        if key == "dependencies":
            if not isinstance(value, list):
                raise ValidationError(
                    'The value for "dependencies" must be an array')
            if "package" not in payload:
                raise ValidationError(
                    'The "package" object must also be provided if the "dependencies" array is '
                    "provided")
            for dep in value:
                Dependency.validate_json(dep, for_update=True)
        elif key == "package":
            Package.validate_json(value)
        elif key == "environment_variables":
            if not isinstance(value, dict):
                raise ValidationError(
                    'The value for "{}" must be an object'.format(key))
            for env_var_name, env_var_info in value.items():
                EnvironmentVariable.validate_json(env_var_name, env_var_info)
        elif not isinstance(value, str):
            raise ValidationError(
                'The value for "{}" must be a string'.format(key))

    if "package_subpath" in payload and "package" not in payload:
        raise ValidationError(
            'The "package" object must also be provided if "package_subpath" is provided'
        )

    if "state" in payload and "state_reason" not in payload:
        raise ValidationError(
            'The "state_reason" key is required when "state" is supplied')
    elif "state_reason" in payload and "state" not in payload:
        raise ValidationError(
            'The "state" key is required when "state_reason" is supplied')

    request = Request.query.get_or_404(request_id)
    delete_bundle = False
    delete_bundle_temp = False
    cleanup_nexus = []
    delete_logs = False
    if "state" in payload and "state_reason" in payload:
        new_state = payload["state"]
        delete_bundle = new_state == "stale" and request.state.state_name != "failed"
        if new_state in ("stale", "failed"):
            for pkg_manager in ["npm", "pip", "yarn"]:
                if any(p.name == pkg_manager for p in request.pkg_managers):
                    cleanup_nexus.append(pkg_manager)
        delete_bundle_temp = new_state in ("complete", "failed")
        delete_logs = new_state == "stale"
        new_state_reason = payload["state_reason"]
        # This is to protect against a Celery task getting executed twice and setting the
        # state each time
        if request.state.state_name == new_state and request.state.state_reason == new_state_reason:
            flask.current_app.logger.info(
                "Not adding a new state since it matches the last state")
        else:
            request.add_state(new_state, new_state_reason)

    package_object = None
    if "package" in payload:
        package_object = Package.get_or_create(payload["package"])

        package_attrs = {}
        # The presence of "package_subpath" in payload indicates whether to modify the subpath.
        # This is only allowed when creating a new package, so when the PATCH API is used to
        # modify an existing package, the user must make sure to use the same subpath (or no
        # subpath).
        if "package_subpath" in payload:
            package_attrs["subpath"] = payload["package_subpath"]

        request.add_package(package_object, **package_attrs)

    for dep_and_replaces in payload.get("dependencies", []):
        dep = copy.deepcopy(dep_and_replaces)
        replaces = dep.pop("replaces", None)

        dep_object = Dependency.get_or_create(dep)
        replaces_object = None
        if replaces:
            replaces_object = Dependency.get_or_create(replaces)
        request.add_dependency(package_object, dep_object, replaces_object)

    for env_var_name, env_var_info in payload.get("environment_variables",
                                                  {}).items():
        env_var_obj = EnvironmentVariable.query.filter_by(
            name=env_var_name, **env_var_info).first()
        if not env_var_obj:
            env_var_obj = EnvironmentVariable.from_json(
                env_var_name, env_var_info)
            db.session.add(env_var_obj)

        if env_var_obj not in request.environment_variables:
            request.environment_variables.append(env_var_obj)

    db.session.commit()

    bundle_dir = RequestBundleDir(
        request.id, root=flask.current_app.config["CACHITO_BUNDLES_DIR"])

    if delete_bundle and bundle_dir.bundle_archive_file.exists():
        flask.current_app.logger.info("Deleting the bundle archive %s",
                                      bundle_dir.bundle_archive_file)
        try:
            bundle_dir.bundle_archive_file.unlink()
        except:  # noqa E722
            flask.current_app.logger.exception(
                "Failed to delete the bundle archive %s",
                bundle_dir.bundle_archive_file)

    if delete_bundle_temp and bundle_dir.exists():
        flask.current_app.logger.debug(
            "Deleting the temporary files used to create the bundle at %s",
            bundle_dir)
        try:
            bundle_dir.rmtree()
        except:  # noqa E722
            flask.current_app.logger.exception(
                "Failed to delete the temporary files at %s", bundle_dir)

    if delete_logs:
        request_log_dir = flask.current_app.config[
            "CACHITO_REQUEST_FILE_LOGS_DIR"]
        path_to_file = os.path.join(request_log_dir, f"{request_id}.log")
        try:
            os.remove(path_to_file)
        except:  # noqa E722
            flask.current_app.logger.exception(
                "Failed to delete the log file %s", path_to_file)

    for pkg_mgr in cleanup_nexus:
        flask.current_app.logger.info(
            "Cleaning up the Nexus %s content for request %d", pkg_mgr,
            request_id)
        cleanup_task = getattr(tasks, f"cleanup_{pkg_mgr}_request")
        try:
            cleanup_task.delay(request_id)
        except kombu.exceptions.OperationalError:
            flask.current_app.logger.exception(
                "Failed to schedule the cleanup_%s_request task for request %d. An administrator "
                "must clean this up manually.",
                pkg_mgr,
                request.id,
            )

    if current_user.is_authenticated:
        flask.current_app.logger.info("The user %s patched request %d",
                                      current_user.username, request.id)
    else:
        flask.current_app.logger.info("An anonymous user patched request %d",
                                      request.id)

    return flask.jsonify(request.to_json()), 200
Exemple #2
0
def patch_request(request_id):
    """
    Modify the given request.

    :param int request_id: the request ID from the URL
    :return: a Flask JSON response
    :rtype: flask.Response
    :raise NotFound: if the request is not found
    :raise ValidationError: if the JSON is invalid
    """
    allowed_users = flask.current_app.config['CACHITO_WORKER_USERNAMES']
    # current_user.is_authenticated is only ever False when auth is disabled
    if current_user.is_authenticated and current_user.username not in allowed_users:
        raise Unauthorized(
            'This API endpoint is restricted to Cachito workers')

    payload = flask.request.get_json()
    if not isinstance(payload, dict):
        raise ValidationError('The input data must be a JSON object')

    if not payload:
        raise ValidationError(
            'At least one key must be specified to update the request')

    valid_keys = {
        'dependencies', 'environment_variables', 'packages', 'pkg_managers',
        'state', 'state_reason'
    }
    invalid_keys = set(payload.keys()) - valid_keys
    if invalid_keys:
        raise ValidationError('The following keys are not allowed: {}'.format(
            ', '.join(invalid_keys)))

    for key, value in payload.items():
        if key in ('dependencies', 'packages',
                   'pkg_managers') and not isinstance(value, list):
            raise ValidationError(f'The value for "{key}" must be an array')

        if key == 'dependencies':
            for dep in value:
                Dependency.validate_json(dep, for_update=True)
        elif key == 'packages':
            for dep in value:
                Package.validate_json(dep)
        elif key == 'pkg_managers':
            for pkg_manager in value:
                if not isinstance(pkg_manager, str):
                    raise ValidationError(
                        'The value for "pkg_managers" must be an array of strings'
                    )
        elif key == 'environment_variables':
            if not isinstance(value, dict):
                raise ValidationError(
                    'The value for "{}" must be an object'.format(key))
            for env_var_name, env_var_value in value.items():
                EnvironmentVariable.validate_json(env_var_name, env_var_value)
        elif not isinstance(value, str):
            raise ValidationError(
                'The value for "{}" must be a string'.format(key))

    if 'state' in payload and 'state_reason' not in payload:
        raise ValidationError(
            'The "state_reason" key is required when "state" is supplied')
    elif 'state_reason' in payload and 'state' not in payload:
        raise ValidationError(
            'The "state" key is required when "state_reason" is supplied')

    request = Request.query.get_or_404(request_id)
    delete_bundle = False
    delete_bundle_temp = False
    if 'state' in payload and 'state_reason' in payload:
        new_state = payload['state']
        delete_bundle = new_state == 'stale'
        delete_bundle_temp = new_state in ('complete', 'failed')
        new_state_reason = payload['state_reason']
        # This is to protect against a Celery task getting executed twice and setting the
        # state each time
        if request.state.state_name == new_state and request.state.state_reason == new_state_reason:
            flask.current_app.logger.info(
                'Not adding a new state since it matches the last state')
        else:
            request.add_state(new_state, new_state_reason)

    if 'dependencies' in payload:
        for dep_and_replaces in payload['dependencies']:
            dep = copy.deepcopy(dep_and_replaces)
            replaces = dep.pop('replaces', None)

            dep_object = Dependency.get_or_create(dep)
            replaces_object = None
            if replaces:
                replaces_object = Dependency.get_or_create(replaces)
            request.add_dependency(dep_object, replaces_object)

    for package in payload.get('packages', []):
        package_object = Package.get_or_create(package)
        if package_object not in request.packages:
            request.packages.append(package_object)

    if 'pkg_managers' in payload:
        pkg_managers = PackageManager.get_pkg_managers(payload['pkg_managers'])
        for pkg_manager in pkg_managers:
            if pkg_manager not in request.pkg_managers:
                request.pkg_managers.append(pkg_manager)

    for name, value in payload.get('environment_variables', {}).items():
        env_var_obj = EnvironmentVariable.query.filter_by(name=name,
                                                          value=value).first()
        if not env_var_obj:
            env_var_obj = EnvironmentVariable.from_json(name, value)
            db.session.add(env_var_obj)

        if env_var_obj not in request.environment_variables:
            request.environment_variables.append(env_var_obj)

    db.session.commit()
    if delete_bundle and os.path.exists(request.bundle_archive):
        flask.current_app.logger.info('Deleting the bundle archive %s',
                                      request.bundle_archive)
        try:
            os.remove(request.bundle_archive)
        except:  # noqa E722
            flask.current_app.logger.exception(
                'Failed to delete the bundle archive %s',
                request.bundle_archive)

    if delete_bundle_temp and os.path.exists(request.bundle_temp_files):
        flask.current_app.logger.debug(
            'Deleting the temporary files used to create the bundle at %s',
            request.bundle_temp_files,
        )
        try:
            shutil.rmtree(request.bundle_temp_files)
        except:  # noqa E722
            flask.current_app.logger.exception(
                'Failed to delete the temporary files at %s',
                request.bundle_temp_files)

    if current_user.is_authenticated:
        flask.current_app.logger.info('The user %s patched request %d',
                                      current_user.username, request.id)
    else:
        flask.current_app.logger.info('An anonymous user patched request %d',
                                      request.id)

    return flask.jsonify(request.to_json()), 200