Exemple #1
0
 def test_generate_job_spec_image_pull_secrets_empty_string_in_runconfig(
         self, tmpdir):
     """Regression test for issue #5001."""
     run_config = KubernetesRun(image_pull_secrets="")
     agent = KubernetesAgent(namespace="testing")
     job = agent.generate_job_spec(self.build_flow_run(run_config))
     assert "imagePullSecrets" not in job["spec"]["template"]["spec"]
Exemple #2
0
 def test_generate_job_spec_image_pull_secrets_empty_string_in_env(
         self, tmpdir, monkeypatch):
     """Regression test for issue #5001."""
     run_config = KubernetesRun()
     monkeypatch.setenv("IMAGE_PULL_SECRETS", "")
     agent = KubernetesAgent(namespace="testing")
     job = agent.generate_job_spec(self.build_flow_run(run_config))
     assert "imagePullSecrets" not in job["spec"]["template"]["spec"]
Exemple #3
0
 def test_generate_job_spec_image_pull_secrets_from_env(
         self, tmpdir, monkeypatch):
     run_config = KubernetesRun()
     monkeypatch.setenv("IMAGE_PULL_SECRETS", "in-env")
     agent = KubernetesAgent(namespace="testing")
     job = agent.generate_job_spec(self.build_flow_run(run_config))
     assert job["spec"]["template"]["spec"]["imagePullSecrets"] == [{
         "name":
         "in-env"
     }]
Exemple #4
0
    def test_environment_has_api_key_from_config(self, config_with_api_key):
        """Check that the API key is passed through from the config via environ"""
        flow_run = self.build_flow_run(KubernetesRun())

        agent = KubernetesAgent(namespace="testing", )
        job = agent.generate_job_spec(flow_run)

        env_list = job["spec"]["template"]["spec"]["containers"][0]["env"]
        env = {item["name"]: item["value"] for item in env_list}

        assert env["PREFECT__CLOUD__API_KEY"] == "TEST_KEY"
        assert env["PREFECT__CLOUD__AUTH_TOKEN"] == "TEST_KEY"
        assert env[
            "PREFECT__CLOUD__TENANT_ID"] == config_with_api_key.cloud.tenant_id
Exemple #5
0
    def test_environment_has_tenant_id_from_server(self, config_with_api_key):
        """Check that the API key is passed through from the config via environ"""
        flow_run = self.build_flow_run(KubernetesRun())
        tenant_id = uuid.uuid4()

        with set_temporary_config({"cloud.tenant_id": None}):
            agent = KubernetesAgent(namespace="testing")

            agent.client._get_auth_tenant = MagicMock(return_value=tenant_id)
            job = agent.generate_job_spec(flow_run)

            env_list = job["spec"]["template"]["spec"]["containers"][0]["env"]
            env = {item["name"]: item["value"] for item in env_list}

        assert env["PREFECT__CLOUD__API_KEY"] == "TEST_KEY"
        assert env["PREFECT__CLOUD__AUTH_TOKEN"] == "TEST_KEY"
        assert env["PREFECT__CLOUD__TENANT_ID"] == tenant_id
Exemple #6
0
    def test_environment_has_api_key_from_disk(self, monkeypatch):
        """Check that the API key is passed through from the on disk cache"""
        tenant_id = str(uuid.uuid4())

        monkeypatch.setattr(
            "prefect.Client.load_auth_from_disk",
            MagicMock(return_value={
                "api_key": "TEST_KEY",
                "tenant_id": tenant_id
            }),
        )

        flow_run = self.build_flow_run(KubernetesRun())
        agent = KubernetesAgent(namespace="testing", )
        agent.client._get_auth_tenant = MagicMock(return_value=tenant_id)
        job = agent.generate_job_spec(flow_run)

        env_list = job["spec"]["template"]["spec"]["containers"][0]["env"]
        env = {item["name"]: item["value"] for item in env_list}

        assert env["PREFECT__CLOUD__API_KEY"] == "TEST_KEY"
        assert env["PREFECT__CLOUD__AUTH_TOKEN"] == "TEST_KEY"
        assert env["PREFECT__CLOUD__TENANT_ID"] == tenant_id
Exemple #7
0
class TestK8sAgentRunConfig:
    def setup(self):
        self.agent = KubernetesAgent(namespace="testing", )

    def read_default_template(self):
        from prefect.agent.kubernetes.agent import DEFAULT_JOB_TEMPLATE_PATH

        with open(DEFAULT_JOB_TEMPLATE_PATH) as f:
            return yaml.safe_load(f)

    def build_flow_run(self, config, storage=None):
        if storage is None:
            storage = Local()
        return GraphQLResult({
            "flow":
            GraphQLResult({
                "storage": storage.serialize(),
                "run_config": RunConfigSchema().dump(config),
                "id": "new_id",
                "core_version": "0.13.0",
            }),
            "id":
            "id",
        })

    def test_generate_job_spec_uses_job_template_provided_in_run_config(self):
        template = self.read_default_template()
        labels = template.setdefault("metadata", {}).setdefault("labels", {})
        labels["TEST"] = "VALUE"
        flow_run = self.build_flow_run(KubernetesRun(job_template=template))
        job = self.agent.generate_job_spec(flow_run)
        assert job["metadata"]["labels"]["TEST"] == "VALUE"

    def test_generate_job_spec_uses_job_template_path_provided_in_run_config(
            self, tmpdir, monkeypatch):
        path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        labels = template.setdefault("metadata", {}).setdefault("labels", {})
        labels["TEST"] = "VALUE"
        with open(path, "w") as f:
            yaml.safe_dump(template, f)
        template_path = f"agent://{path}"

        flow_run = self.build_flow_run(
            KubernetesRun(job_template_path=template_path))

        mocked_read_bytes = MagicMock(wraps=read_bytes_from_path)
        monkeypatch.setattr(
            "prefect.agent.kubernetes.agent.read_bytes_from_path",
            mocked_read_bytes)
        job = self.agent.generate_job_spec(flow_run)
        assert job["metadata"]["labels"]["TEST"] == "VALUE"
        assert mocked_read_bytes.call_args[0] == (template_path, )

    def test_generate_job_spec_metadata(self, tmpdir):
        template_path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        job_labels = template.setdefault("metadata",
                                         {}).setdefault("labels", {})
        pod_labels = (template["spec"]["template"].setdefault(
            "metadata", {}).setdefault("labels", {}))
        job_labels.update({"JOB_LABEL": "VALUE1"})
        pod_labels.update({"POD_LABEL": "VALUE2"})
        with open(template_path, "w") as f:
            yaml.safe_dump(template, f)
        self.agent.job_template_path = template_path

        flow_run = self.build_flow_run(KubernetesRun())
        job = self.agent.generate_job_spec(flow_run)

        identifier = job["metadata"]["labels"]["prefect.io/identifier"]
        labels = {
            "prefect.io/identifier": identifier,
            "prefect.io/flow_run_id": flow_run.id,
            "prefect.io/flow_id": flow_run.flow.id,
        }

        assert job["metadata"]["name"]
        assert job["metadata"]["labels"] == dict(JOB_LABEL="VALUE1", **labels)
        assert job["spec"]["template"]["metadata"]["labels"] == dict(
            POD_LABEL="VALUE2", **labels)

    @pytest.mark.parametrize(
        "run_config, storage, expected",
        [
            (
                KubernetesRun(),
                Docker(registry_url="test", image_name="name",
                       image_tag="tag"),
                "test/name:tag",
            ),
            (KubernetesRun(image="myimage"), Local(), "myimage"),
            (KubernetesRun(), Local(), "prefecthq/prefect:all_extras-0.13.0"),
        ],
        ids=["on-storage", "on-run_config", "default"],
    )
    def test_generate_job_spec_image(self, run_config, storage, expected):
        flow_run = self.build_flow_run(run_config, storage)
        job = self.agent.generate_job_spec(flow_run)
        image = job["spec"]["template"]["spec"]["containers"][0]["image"]
        assert image == expected

    def test_generate_job_spec_environment_variables(self, tmpdir):
        """Check that environment variables are set in precedence order

        - CUSTOM1 & CUSTOM2 are set on the template
        - CUSTOM2 & CUSTOM3 are set on the agent
        - CUSTOM3 & CUSTOM4 are set on the RunConfig
        """
        template_path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        template_env = template["spec"]["template"]["spec"]["containers"][
            0].setdefault("env", [])
        template_env.extend([
            {
                "name": "CUSTOM1",
                "value": "VALUE1"
            },
            {
                "name": "CUSTOM2",
                "value": "VALUE2"
            },
        ])
        with open(template_path, "w") as f:
            yaml.safe_dump(template, f)
        self.agent.job_template_path = template_path

        self.agent.env_vars = {"CUSTOM2": "OVERRIDE2", "CUSTOM3": "VALUE3"}
        run_config = KubernetesRun(image="test-image",
                                   env={
                                       "CUSTOM3": "OVERRIDE3",
                                       "CUSTOM4": "VALUE4"
                                   })

        flow_run = self.build_flow_run(run_config)
        job = self.agent.generate_job_spec(flow_run)
        env_list = job["spec"]["template"]["spec"]["containers"][0]["env"]
        env = {item["name"]: item["value"] for item in env_list}
        assert env == {
            "PREFECT__CLOUD__API": prefect.config.cloud.api,
            "PREFECT__CLOUD__AUTH_TOKEN":
            prefect.config.cloud.agent.auth_token,
            "PREFECT__CLOUD__USE_LOCAL_SECRETS": "false",
            "PREFECT__CONTEXT__FLOW_RUN_ID": flow_run.id,
            "PREFECT__CONTEXT__FLOW_ID": flow_run.flow.id,
            "PREFECT__CONTEXT__IMAGE": "test-image",
            "PREFECT__LOGGING__LOG_TO_CLOUD":
            str(self.agent.log_to_cloud).lower(),
            "PREFECT__ENGINE__FLOW_RUNNER__DEFAULT_CLASS":
            "prefect.engine.cloud.CloudFlowRunner",
            "PREFECT__ENGINE__TASK_RUNNER__DEFAULT_CLASS":
            "prefect.engine.cloud.CloudTaskRunner",
            "CUSTOM1": "VALUE1",
            "CUSTOM2":
            "OVERRIDE2",  # Agent env-vars override those in template
            "CUSTOM3":
            "OVERRIDE3",  # RunConfig env-vars override those on agent and template
            "CUSTOM4": "VALUE4",
        }

    def test_generate_job_spec_resources(self):
        flow_run = self.build_flow_run(
            KubernetesRun(cpu_request=1,
                          cpu_limit=2,
                          memory_request="4G",
                          memory_limit="8G"))
        job = self.agent.generate_job_spec(flow_run)
        resources = job["spec"]["template"]["spec"]["containers"][0][
            "resources"]
        assert resources == {
            "limits": {
                "cpu": "2",
                "memory": "8G"
            },
            "requests": {
                "cpu": "1",
                "memory": "4G"
            },
        }
Exemple #8
0
class TestK8sAgentRunConfig:
    def setup(self):
        self.agent = KubernetesAgent(
            namespace="testing",
        )

    def read_default_template(self):
        from prefect.agent.kubernetes.agent import DEFAULT_JOB_TEMPLATE_PATH

        with open(DEFAULT_JOB_TEMPLATE_PATH) as f:
            return yaml.safe_load(f)

    def build_flow_run(self, config, storage=None, core_version="0.13.0"):
        if storage is None:
            storage = Local()
        return GraphQLResult(
            {
                "flow": GraphQLResult(
                    {
                        "storage": storage.serialize(),
                        "id": "new_id",
                        "core_version": core_version,
                    }
                ),
                "run_config": None if config is None else config.serialize(),
                "id": "id",
            }
        )

    @pytest.mark.parametrize("run_config", [None, UniversalRun()])
    def test_generate_job_spec_null_or_univeral_run_config(self, run_config):
        self.agent.generate_job_spec_from_run_config = MagicMock(
            wraps=self.agent.generate_job_spec_from_run_config
        )
        flow_run = self.build_flow_run(run_config)
        self.agent.generate_job_spec(flow_run)
        assert self.agent.generate_job_spec_from_run_config.called

    def test_generate_job_spec_errors_if_non_kubernetesrun_run_config(self):
        with pytest.raises(
            TypeError,
            match="`run_config` of type `LocalRun`, only `KubernetesRun` is supported",
        ):
            self.agent.generate_job_spec(self.build_flow_run(LocalRun()))

    def test_generate_job_spec_uses_job_template_provided_in_run_config(self):
        template = self.read_default_template()
        labels = template.setdefault("metadata", {}).setdefault("labels", {})
        labels["TEST"] = "VALUE"
        flow_run = self.build_flow_run(KubernetesRun(job_template=template))
        job = self.agent.generate_job_spec(flow_run)
        assert job["metadata"]["labels"]["TEST"] == "VALUE"

    def test_generate_job_spec_uses_job_template_path_provided_in_run_config(
        self, tmpdir, monkeypatch
    ):
        path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        labels = template.setdefault("metadata", {}).setdefault("labels", {})
        labels["TEST"] = "VALUE"
        with open(path, "w") as f:
            yaml.safe_dump(template, f)
        template_path = f"agent://{path}"

        flow_run = self.build_flow_run(KubernetesRun(job_template_path=template_path))

        mocked_read_bytes = MagicMock(wraps=read_bytes_from_path)
        monkeypatch.setattr(
            "prefect.agent.kubernetes.agent.read_bytes_from_path", mocked_read_bytes
        )
        job = self.agent.generate_job_spec(flow_run)
        assert job["metadata"]["labels"]["TEST"] == "VALUE"
        assert mocked_read_bytes.call_args[0] == (template_path,)

    def test_generate_job_spec_metadata(self, tmpdir):
        template_path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        job_labels = template.setdefault("metadata", {}).setdefault("labels", {})
        pod_labels = (
            template["spec"]["template"]
            .setdefault("metadata", {})
            .setdefault("labels", {})
        )
        job_labels.update({"JOB_LABEL": "VALUE1"})
        pod_labels.update({"POD_LABEL": "VALUE2"})
        with open(template_path, "w") as f:
            yaml.safe_dump(template, f)
        self.agent.job_template_path = template_path

        flow_run = self.build_flow_run(KubernetesRun())
        job = self.agent.generate_job_spec(flow_run)

        identifier = job["metadata"]["labels"]["prefect.io/identifier"]
        labels = {
            "prefect.io/identifier": identifier,
            "prefect.io/flow_run_id": flow_run.id,
            "prefect.io/flow_id": flow_run.flow.id,
        }

        assert job["metadata"]["name"]
        assert job["metadata"]["labels"] == dict(JOB_LABEL="VALUE1", **labels)
        assert job["spec"]["template"]["metadata"]["labels"] == dict(
            POD_LABEL="VALUE2", **labels
        )
        assert job["spec"]["template"]["spec"]["restartPolicy"] == "Never"

    @pytest.mark.parametrize(
        "run_config, storage, expected",
        [
            (
                KubernetesRun(),
                Docker(registry_url="test", image_name="name", image_tag="tag"),
                "test/name:tag",
            ),
            (KubernetesRun(image="myimage"), Local(), "myimage"),
            (KubernetesRun(), Local(), "prefecthq/prefect:0.13.0"),
        ],
        ids=["on-storage", "on-run_config", "default"],
    )
    def test_generate_job_spec_image(self, run_config, storage, expected):
        flow_run = self.build_flow_run(run_config, storage)
        job = self.agent.generate_job_spec(flow_run)
        image = job["spec"]["template"]["spec"]["containers"][0]["image"]
        assert image == expected

    @pytest.mark.parametrize(
        "core_version, expected",
        [
            ("0.12.0", "prefect execute cloud-flow"),
            ("0.14.0", "prefect execute flow-run"),
        ],
    )
    def test_generate_job_spec_container_args(self, core_version, expected):
        flow_run = self.build_flow_run(KubernetesRun(), core_version=core_version)
        job = self.agent.generate_job_spec(flow_run)
        args = job["spec"]["template"]["spec"]["containers"][0]["args"]
        assert args == expected.split()

    def test_generate_job_spec_environment_variables(self, tmpdir):
        """Check that environment variables are set in precedence order

        - CUSTOM1 & CUSTOM2 are set on the template
        - CUSTOM2 & CUSTOM3 are set on the agent
        - CUSTOM3 & CUSTOM4 are set on the RunConfig
        """
        template_path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        template_env = template["spec"]["template"]["spec"]["containers"][0].setdefault(
            "env", []
        )
        template_env.extend(
            [
                {"name": "CUSTOM1", "value": "VALUE1"},
                {"name": "CUSTOM2", "value": "VALUE2"},
            ]
        )
        with open(template_path, "w") as f:
            yaml.safe_dump(template, f)
        self.agent.job_template_path = template_path

        self.agent.env_vars = {"CUSTOM2": "OVERRIDE2", "CUSTOM3": "VALUE3"}
        run_config = KubernetesRun(
            image="test-image", env={"CUSTOM3": "OVERRIDE3", "CUSTOM4": "VALUE4"}
        )

        flow_run = self.build_flow_run(run_config)
        job = self.agent.generate_job_spec(flow_run)
        env_list = job["spec"]["template"]["spec"]["containers"][0]["env"]
        env = {item["name"]: item["value"] for item in env_list}
        assert env == {
            "PREFECT__CLOUD__API": prefect.config.cloud.api,
            "PREFECT__CLOUD__AUTH_TOKEN": prefect.config.cloud.agent.auth_token,
            "PREFECT__CLOUD__USE_LOCAL_SECRETS": "false",
            "PREFECT__CONTEXT__FLOW_RUN_ID": flow_run.id,
            "PREFECT__CONTEXT__FLOW_ID": flow_run.flow.id,
            "PREFECT__CONTEXT__IMAGE": "test-image",
            "PREFECT__LOGGING__LOG_TO_CLOUD": str(self.agent.log_to_cloud).lower(),
            "PREFECT__ENGINE__FLOW_RUNNER__DEFAULT_CLASS": "prefect.engine.cloud.CloudFlowRunner",
            "PREFECT__ENGINE__TASK_RUNNER__DEFAULT_CLASS": "prefect.engine.cloud.CloudTaskRunner",
            "CUSTOM1": "VALUE1",
            "CUSTOM2": "OVERRIDE2",  # Agent env-vars override those in template
            "CUSTOM3": "OVERRIDE3",  # RunConfig env-vars override those on agent and template
            "CUSTOM4": "VALUE4",
        }

    def test_generate_job_spec_resources(self):
        flow_run = self.build_flow_run(
            KubernetesRun(
                cpu_request=1, cpu_limit=2, memory_request="4G", memory_limit="8G"
            )
        )
        job = self.agent.generate_job_spec(flow_run)
        resources = job["spec"]["template"]["spec"]["containers"][0]["resources"]
        assert resources == {
            "limits": {"cpu": "2", "memory": "8G"},
            "requests": {"cpu": "1", "memory": "4G"},
        }

    def test_generate_job_spec_service_account_name(self, tmpdir):
        template_path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        template["spec"]["template"]["spec"]["serviceAccountName"] = "on-agent-template"
        with open(template_path, "w") as f:
            yaml.safe_dump(template, f)

        self.agent.service_account_name = "on-agent"
        self.agent.job_template_path = template_path

        template["spec"]["template"]["spec"][
            "serviceAccountName"
        ] = "on-run-config-template"

        run_config = KubernetesRun(
            job_template=template, service_account_name="on-run-config"
        )

        # Check precedence order:
        # 1. Explicit on run-config"
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["serviceAccountName"] == "on-run-config"

        # 2. In job template on run-config
        run_config.service_account_name = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert (
            job["spec"]["template"]["spec"]["serviceAccountName"]
            == "on-run-config-template"
        )
        # None in run-config job template is still used
        run_config.job_template["spec"]["template"]["spec"]["serviceAccountName"] = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["serviceAccountName"] is None

        # 3. Explicit on agent
        # Not present in job template
        run_config.job_template["spec"]["template"]["spec"].pop("serviceAccountName")
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["serviceAccountName"] == "on-agent"
        # No job template present
        run_config.job_template = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["serviceAccountName"] == "on-agent"

        # 4. In job template on agent
        self.agent.service_account_name = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert (
            job["spec"]["template"]["spec"]["serviceAccountName"] == "on-agent-template"
        )

    def test_generate_job_spec_image_pull_secrets(self, tmpdir):
        template_path = str(tmpdir.join("job.yaml"))
        template = self.read_default_template()
        template["spec"]["template"]["spec"]["imagePullSecrets"] = [
            {"name": "on-agent-template"}
        ]
        with open(template_path, "w") as f:
            yaml.safe_dump(template, f)

        self.agent.image_pull_secrets = ["on-agent"]
        self.agent.job_template_path = template_path

        template["spec"]["template"]["spec"]["imagePullSecrets"] = [
            {"name": "on-run-config-template"}
        ]

        run_config = KubernetesRun(
            job_template=template, image_pull_secrets=["on-run-config"]
        )

        # Check precedence order:
        # 1. Explicit on run-config"
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["imagePullSecrets"] == [
            {"name": "on-run-config"}
        ]

        # 2. In job template on run-config
        run_config.image_pull_secrets = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["imagePullSecrets"] == [
            {"name": "on-run-config-template"}
        ]
        # None in run-config job template is still used
        run_config.job_template["spec"]["template"]["spec"]["imagePullSecrets"] = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["imagePullSecrets"] is None

        # 3. Explicit on agent
        # Not present in job template
        run_config.job_template["spec"]["template"]["spec"].pop("imagePullSecrets")
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["imagePullSecrets"] == [
            {"name": "on-agent"}
        ]
        # No job template present
        run_config.job_template = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["imagePullSecrets"] == [
            {"name": "on-agent"}
        ]

        # 4. In job template on agent
        self.agent.image_pull_secrets = None
        job = self.agent.generate_job_spec(self.build_flow_run(run_config))
        assert job["spec"]["template"]["spec"]["imagePullSecrets"] == [
            {"name": "on-agent-template"}
        ]
Exemple #9
0
 def test_generate_job_spec_without_image_pull_secrets(self, tmpdir):
     run_config = KubernetesRun()
     agent = KubernetesAgent(namespace="testing")
     job = agent.generate_job_spec(self.build_flow_run(run_config))
     assert "imagePullSecrets" not in job["spec"]["template"]["spec"]