Esempio n. 1
0
def get_available_settings():
    """Return settings available on the current project"""
    try:
        project = Project()
    except (ValueError, SettingsModified):
        return None
    return project.get_available_settings().__members__
Esempio n. 2
0
def test_ddc_project_addition(minimal_project, mocker, capsys):
    from derex.runner import hookimpl

    class CustomAdditional:
        @staticmethod
        @hookimpl
        def ddc_project_options(
            project: Project, ) -> Dict[str, Union[str, List[str]]]:
            """See derex.runner.plugin_spec.ddc_project_options docstring
            """
            return {
                "options": ["custom-additional"],
                "name": "custom",
                "priority": ">local-derex",
            }

    with minimal_project:
        docker_compose_path = Project().root / "docker-compose.yml"
        with docker_compose_path.open("w") as fh:
            fh.write("lms:\n  image: foobar\n")
        project = Project()
        run_ddc_project([], project, dry_run=True)
        output = capsys.readouterr().out
        # The last option should be the path of the user docker compose file for this project
        assert output.endswith(f"-f {docker_compose_path}\n")
Esempio n. 3
0
def test_populate_settings(testproj):
    with testproj as projdir:

        default_settings_dir = Project().settings_directory_path()
        assert default_settings_dir.is_dir()

        create_settings_file(Path(projdir), "production")
        project = Project(read_only=True)

        assert default_settings_dir != project.settings_directory_path()

        project._populate_settings()
        assert (project.settings_dir / "base.py").is_file(), str(
            sorted((project.settings_dir).iterdir())
        )
        assert (project.settings_dir / "derex").is_dir(), str(
            sorted((project.settings_dir).iterdir())
        )
        base_py = project.settings_dir / "derex" / "base.py"
        assert base_py.is_file()
        assert (project.settings_dir / "derex" / "__init__.py").is_file()

        assert os.access(str(base_py), os.W_OK)

        # In case a settings file was already present, it should be overwritten,
        # even if it lacks owner write permission.
        base_py.write_text("# Changed")
        base_py.chmod(0o444)
        assert not os.access(str(base_py), os.W_OK)
        project._populate_settings()
        assert os.access(str(base_py), os.W_OK)
Esempio n. 4
0
def test_container_variables_json_serialized(minimal_project):
    with minimal_project:
        conf_file = Project().root / CONF_FILENAME
        config = {
            "project_name": "minimal",
            "variables": {
                "ALL_JWT_AUTH": {
                    "default": {
                        "JWT_AUDIENCE": "jwt-audience",
                        "JWT_SECRET_KEY": "jwt-secret",
                    },
                    "production": {
                        "JWT_AUDIENCE": "prod-audience",
                        "JWT_SECRET_KEY": "prod-secret",
                    },
                },
            },
        }
        conf_file.write_text(yaml.dump(config))
        create_settings_file(Project().root, "production")
        project = Project()
        env = project.get_container_env()
        assert "DEREX_JSON_ALL_JWT_AUTH" in env
        expected = config["variables"]["ALL_JWT_AUTH"]["default"]
        assert expected == json.loads(env["DEREX_JSON_ALL_JWT_AUTH"])

        project.settings = project._available_settings.production
        env = project.get_container_env()
        assert "DEREX_JSON_ALL_JWT_AUTH" in env
        expected = config["variables"]["ALL_JWT_AUTH"]["production"]
        assert expected == json.loads(env["DEREX_JSON_ALL_JWT_AUTH"])
Esempio n. 5
0
def test_container_variables_json_serialized(testproj):
    with testproj as projdir:
        conf_file = Path(projdir) / "derex.config.yaml"
        config = {
            "project_name": "minimal",
            "variables": {
                "lms_ALL_JWT_AUTH": {
                    "base": {
                        "JWT_AUDIENCE": "jwt-audience",
                        "JWT_SECRET_KEY": "jwt-secret",
                    },
                    "production": {
                        "JWT_AUDIENCE": "prod-audience",
                        "JWT_SECRET_KEY": "prod-secret",
                    },
                }
            },
        }
        conf_file.write_text(yaml.dump(config))
        create_settings_file(Path(projdir), "production")
        project = Project()
        env = project.get_container_env()
        assert "DEREX_JSON_LMS_ALL_JWT_AUTH" in env
        expected = json.loads(env["DEREX_JSON_LMS_ALL_JWT_AUTH"])
        assert expected == config["variables"]["lms_ALL_JWT_AUTH"]["base"]
def generate_ddc_project_compose(project: Project) -> Path:
    """This function is called every time ddc-project is run.
    It assembles a docker-compose file from the given configuration.
    It should execute as fast as possible.
    """
    project_compose_path = project.private_filepath("docker-compose.yml")
    template_path = DDC_PROJECT_TEMPLATE_PATH
    final_image = None
    if image_exists(project.image_name):
        final_image = project.image_name
    if not image_exists(project.requirements_image_name):
        logger.warning(f"Image {project.requirements_image_name} not found\n"
                       "Run\nderex build requirements\n to build it")

    openedx_customizations = project.get_openedx_customizations()

    tmpl = Template(template_path.read_text())
    text = tmpl.render(
        project=project,
        final_image=final_image,
        wsgi_py_path=WSGI_PY_PATH,
        derex_django_path=DEREX_DJANGO_PATH,
        openedx_customizations=openedx_customizations,
    )
    project_compose_path.write_text(text)
    return project_compose_path
Esempio n. 7
0
def test_project_name_constraints(minimal_project):
    with minimal_project:
        project_root = Project().root
        conf_file = project_root / CONF_FILENAME
        config = {"project_name": ";invalid;"}
        conf_file.write_text(yaml.dump(config))
        create_settings_file(project_root, "production")
        with pytest.raises(ValueError):
            Project()
Esempio n. 8
0
def test_derex_runmode_wrong(minimal_project):
    with minimal_project:
        project = Project()
        # Use low level API to inject invalid value
        project._set_status("runmode", "garbage-not-a-valid-runmode")

        result = runner.invoke(derex_cli_group, "runmode")
        # Ensure presence of error message
        assert "garbage-not-a-valid-runmode" in result.stderr
        assert "is not valid as" in result.stderr
Esempio n. 9
0
def test_derex_runmode_wrong(testproj):
    from derex.runner.cli import derex

    with testproj:
        project = Project()
        # Use low level API to inject invalid value
        project._set_status("runmode", "garbage")

        result = runner.invoke(derex, ["runmode"])
        # Ensure presence of error message
        assert "not valid" in result.stderr
Esempio n. 10
0
def test_derex_build(workdir, sys_argv):
    from derex.runner.cli import derex
    from derex.runner.ddc import ddc_project

    with workdir(COMPLETE_PROJ):
        result = runner.invoke(derex, ["compile-theme"])
        assert_result_ok(result)

        with sys_argv(["ddc-project", "config"]):
            ddc_project()
        assert Project().name in result.output
        assert os.path.isdir(Project().root / ".derex")
Esempio n. 11
0
def test_complete_project(workdir):
    with workdir(COMPLETE_PROJ / "themes"):
        project = Project()

    project_loaded_with_path = Project(COMPLETE_PROJ)

    assert project.root == project_loaded_with_path.root

    assert type(project.config) == dict
    assert project.requirements_dir == COMPLETE_PROJ / "requirements"
    assert project.themes_dir == COMPLETE_PROJ / "themes"
    assert project.name == "complete"
    assert project.requirements_image_name == "complete/openedx-requirements:6c92de"
Esempio n. 12
0
def test_run_django_script(minimal_project):
    with minimal_project:
        from derex.runner.project import Project
        from derex.runner.ddc import run_django_script

        result = run_django_script(
            Project(),
            "import json; print(json.dumps(dict(foo='bar', one=1)))")
        assert result == {"foo": "bar", "one": 1}

        result = run_django_script(
            Project(),
            "import json; print('This is not { the JSON you re looking for')")
        assert result is None
Esempio n. 13
0
def test_run_script(testproj):
    with testproj:
        from derex.runner.project import Project
        from derex.runner.compose_utils import run_script

        result = run_script(
            Project(),
            "import json; print(json.dumps(dict(foo='bar', one=1)))")
        assert result == {"foo": "bar", "one": 1}

        result = run_script(
            Project(),
            "import json; print('This is not { the JSON you re looking for')")
        assert result is None
Esempio n. 14
0
def test_runmode(minimal_project):
    with minimal_project:
        project = Project()
        # If no default is specified, the value should be debug
        assert project.runmode == ProjectRunMode.debug

        # If a default value is specified, it should be picked up
        with (project.root / CONF_FILENAME).open("a") as fh:
            fh.write("default_runmode: production\n")
        assert Project().runmode == ProjectRunMode.production

        Project().runmode = ProjectRunMode.debug
        # Runmode changes should be persisted in the project directory
        # and picked up by a second Project instance
        assert Project().runmode == ProjectRunMode.debug
Esempio n. 15
0
def test_derex_compile_theme(workdir_copy, sys_argv):
    from derex.runner.cli import derex

    with workdir_copy(COMPLETE_PROJ):
        result = runner.invoke(derex, ["compile-theme"])
        assert_result_ok(result)
        assert os.path.isdir(Project().root / ".derex")
Esempio n. 16
0
def ddc_project():
    """Proxy for docker-compose: writes a docker-compose.yml file with the
    configuration of this project, and then run `docker-compose` on it.

    You probably want do run `ddc-project up -d` and `ddc-project logs -f`.
    """
    check_docker()
    setup_logging()
    try:
        project = Project()
    except ValueError:
        click.echo("You need to run this command in a derex project")
        sys.exit(1)
    compose_args, dry_run = ddc_parse_args(sys.argv)
    # If trying to start up containers, first check that needed services are running
    is_start_cmd = any(param in compose_args for param in ["up", "start"])
    if is_start_cmd and not check_services(["mysql", "mongodb", "rabbitmq"]):
        click.echo(
            "Mysql/mongo/rabbitmq services not found.\nMaybe you forgot to run\nddc-services up -d"
        )
        return
    run_compose(list(compose_args),
                project=project,
                dry_run=dry_run,
                exit_afterwards=True)
Esempio n. 17
0
def test_docker_compose_addition_per_runmode(testproj, mocker):
    from derex.runner.compose_utils import get_compose_options

    with testproj:
        docker_compose_path = Path(testproj._tmpdir.name) / "docker-compose-debug.yml"
        with docker_compose_path.open("w") as fh:
            fh.write("lms:\n  image: foobar\n")
        project = Project()
        opts = get_compose_options(args=[], variant="", project=project)
        # The last option should be the path of the debug docker compose
        assert opts[-1] == str(docker_compose_path)

        project.runmode = ProjectRunMode.production
        opts = get_compose_options(args=[], variant="", project=project)
        # The last option should be the path of the production docker compose file
        assert opts[-1] != str(docker_compose_path)
Esempio n. 18
0
def test_ddc_project_minimal(sys_argv, mocker, minimal_project, capsys):
    from derex.runner.ddc import ddc_project
    from derex.runner.project import Project
    """Test the open edx ironwood docker compose shortcut."""
    # It should check for services to be up before trying to do anything
    wait_for_service = mocker.patch("derex.runner.ddc.wait_for_service")

    with minimal_project:
        for param in ["up", "start"]:
            wait_for_service.return_value = 0
            wait_for_service.side_effect = None
            with sys_argv(["ddc-project", param, "--dry-run"]):
                ddc_project()
            assert "Would have run" in capsys.readouterr().out

            wait_for_service.side_effect = RuntimeError(
                "mysql service not found.\n"
                "Maybe you forgot to run\n"
                "ddc-services up -d")
            with sys_argv(["ddc-project", param, "--dry-run"]):
                with pytest.raises(SystemExit):
                    ddc_project()
            assert "ddc-services up -d" in capsys.readouterr().out

        with sys_argv(["ddc-project", "config"]):
            ddc_project()
        assert "worker" in capsys.readouterr().out

        if Project().openedx_version.name == "juniper":
            with sys_argv(["ddc-project", "config"]):
                ddc_project()
            assert (
                "/derex/runner/compose_files/openedx_customizations/juniper/"
                in capsys.readouterr().out)
Esempio n. 19
0
def test_ddc_services(sys_argv, capsys, monkeypatch, complete_project):
    """Test the derex docker compose shortcut."""
    from derex.runner.ddc import ddc_services
    from derex.runner.project import Project

    os.environ["DEREX_ADMIN_SERVICES"] = "False"
    with sys_argv(["ddc-services", "config"]):
        ddc_services()
    output = capsys.readouterr().out
    assert "mongodb" in output
    assert "adminer" not in output

    os.environ["DEREX_ADMIN_SERVICES"] = "True"
    with sys_argv(["ddc-services", "config"]):
        ddc_services()
    output = capsys.readouterr().out
    assert "adminer" in output

    with complete_project:
        monkeypatch.setenv("DEREX_ETC_PATH",
                           str(Project().root / "derex_etc_dir"))
        with sys_argv(["ddc-services", "config"]):
            ddc_services()

    output = capsys.readouterr().out
    assert "my-overridden-secret-password" in output
Esempio n. 20
0
def derex(ctx):
    """Derex directs edX: commands to manage an Open edX installation
    """
    # Optimize --help and bash completion by importing
    from derex.runner.project import Project

    try:
        ctx.obj = Project()
    except ProjectNotFound:
        pass
    except Exception as ex:
        logger.error("\n".join(map(str, ex.args)))
        sys.exit(1)

    if ctx.invoked_subcommand:
        return

    click.echo(derex.get_help(ctx) + "\n")

    from derex.runner.docker_utils import get_exposed_container_names

    container_names = get_exposed_container_names()
    if not container_names:
        return

    console = Console()
    table = Table(
        title="[bold green]These containers are running and exposing an HTTP server on port 80",
        box=box.SIMPLE,
    )
    table.add_column("Name")
    for container in container_names:
        container = (f"[bold]{container[0]}",) + container[1:]
        table.add_row(*container)
    console.print(table)
Esempio n. 21
0
def runmode(project: Project, runmode: Optional[ProjectRunMode], force):
    """Get/set project runmode (debug/production)"""
    if runmode is None:
        click.echo(project.runmode.name)
    else:
        if project.runmode == runmode:
            click.echo(
                f"The current project runmode is already {runmode.name}", err=True
            )
            return
        if not force:
            if runmode is ProjectRunMode.production:
                if not HAS_MASTER_SECRET:
                    click.echo(
                        red("Set a master secret before switching to production"),
                        err=True,
                    )
                    sys.exit(1)
                    return 1
                    # We need https://github.com/Santandersecurityresearch/DrHeader/pull/102
                    # for the return 1 to work, but it's not released yet
        previous_runmode = project.runmode
        project.runmode = runmode
        click.echo(
            f"Switched runmode: {previous_runmode.name} → {runmode.name}", err=True
        )
Esempio n. 22
0
def test_docker_compose_addition(testproj, mocker):
    from derex.runner import hookimpl
    from derex.runner.compose_utils import get_compose_options
    from derex.runner.plugins import setup_plugin_manager

    class CustomAdditional:
        @staticmethod
        @hookimpl
        def local_compose_options(project: Project) -> Dict[str, Union[str, List[str]]]:
            """See derex.runner.plugin_spec.compose_options docstring
            """
            return {
                "options": ["custom-additional"],
                "name": "custom",
                "priority": ">local-derex",
            }

    with testproj:
        docker_compose_path = Path(testproj._tmpdir.name) / "docker-compose.yml"
        with docker_compose_path.open("w") as fh:
            fh.write("lms:\n  image: foobar\n")
        project = Project()
        mgr = setup_plugin_manager()
        mgr.register(CustomAdditional)
        mocker.patch(
            "derex.runner.compose_utils.setup_plugin_manager", return_value=mgr
        )
        opts = get_compose_options(args=[], variant="", project=project)
        # The last option should be the path of the user docker compose file for this project
        assert opts[-1] == str(docker_compose_path)
Esempio n. 23
0
def test_runmode(testproj):
    from derex.runner.utils import CONF_FILENAME

    with testproj:
        # If no default is specified, the value should be debug
        assert Project().runmode == ProjectRunMode.debug

        # If a default value is specified, it should be picked up
        with (Path(testproj._tmpdir.name) / CONF_FILENAME).open("a") as fh:
            fh.write("default_runmode: production\n")
        assert Project().runmode == ProjectRunMode.production

        Project().runmode = ProjectRunMode.production
        # Runmode changes should be persisted in the project directory
        # and picked up by a second Project instance
        assert Project().runmode == ProjectRunMode.production
Esempio n. 24
0
def create_settings_file(project_root: Path, filename: str):
    """Create an empty settings file inside the given project"""
    settings_dir = project_root / "settings"
    if not settings_dir.is_dir():
        settings_dir.mkdir()
        (settings_dir / "__init__.py").write_text("")
        project = Project(read_only=True)
    (project.settings_dir / f"{filename}.py").write_text("# Empty file")
Esempio n. 25
0
def test_project_name_constraints(testproj):
    with testproj as projdir:
        conf_file = Path(projdir) / "derex.config.yaml"
        config = {"project_name": ";invalid;"}
        conf_file.write_text(yaml.dump(config))
        create_settings_file(Path(projdir), "production")
        with pytest.raises(ValueError):
            Project()
Esempio n. 26
0
def test_settings_enum(testproj):
    with testproj:
        assert Project().settings == Project().get_available_settings().base

        create_settings_file(Project().root, "production")
        Project().settings = Project().get_available_settings().production
        assert Project().settings == Project().get_available_settings().production
Esempio n. 27
0
def test_get_final_image(mocker):
    from derex.runner.config import image_exists

    mocker.patch(
        "derex.runner.config.docker.APIClient",
        return_value=mocker.Mock(images=mocker.Mock(
            return_value=DOCKER_DAEMON_IMAGES_RESPONSE)),
    )
    project = Project(MINIMAL_PROJ)
    image_exists(project)
Esempio n. 28
0
def test_complete_project(workdir, complete_project):
    with complete_project:
        project_path = Project().root
        project_loaded_with_path = Project(project_path)
        with workdir(project_path / "themes"):
            project = Project()

    assert project.root == project_loaded_with_path.root
    assert type(project.config) == dict
    assert project.requirements_dir == project_path / "requirements"
    assert project.themes_dir == project_path / "themes"
    assert project.name == f"{project.openedx_version.name}-complete"

    if project.openedx_version.name == "ironwood":
        assert (project.requirements_image_name ==
                f"{project.name}/openedx-requirements:6c92de")
    elif project.openedx_version.name == "juniper":
        assert (project.requirements_image_name ==
                f"{project.name}/openedx-requirements:27703b")
Esempio n. 29
0
def test_generate_ddc_test_compose(complete_project):
    from derex.runner.compose_generation import generate_ddc_test_compose

    with complete_project:
        project = Project()
        compose_file = generate_ddc_test_compose(project)

        assert "cypress" in compose_file.read_text()
        assert project.name in compose_file.read_text()
        assert "/complete/e2e:/e2e" in compose_file.read_text()
Esempio n. 30
0
def test_settings_enum(minimal_project):
    with minimal_project:
        assert (Project().settings.value ==
                Project().get_available_settings().default.value)

        create_settings_file(Project().root, "production")
        Project().settings = Project().get_available_settings().production
        assert (Project().settings.value ==
                Project().get_available_settings().production.value)