Example #1
0
def test_noop_compute_log_manager(
    template: HelmTemplate, compute_log_manager_type: ComputeLogManagerType
):
    helm_values = DagsterHelmValues.construct(
        computeLogManager=ComputeLogManager.construct(type=compute_log_manager_type)
    )

    configmaps = template.render(helm_values)
    instance = yaml.full_load(configmaps[0].data["dagster.yaml"])
    compute_logs_config = instance["compute_logs"]

    assert compute_logs_config["module"] == "dagster.core.storage.noop_compute_log_manager"
    assert compute_logs_config["class"] == "NoOpComputeLogManager"
Example #2
0
def test_service_account_global_name(template: HelmTemplate):
    global_service_account_name = "global-service-account-name"
    service_account_values = DagsterHelmValues.construct(
        global_=Global.construct(
            serviceAccountName=global_service_account_name), )

    service_account_templates = template.render(service_account_values)

    assert len(service_account_templates) == 1

    service_account_template = service_account_templates[0]

    assert service_account_template.metadata.name == global_service_account_name
def test_subchart_image_pull_secrets(subchart_template: HelmTemplate):
    image_pull_secrets = [{"name": "super-duper-secret"}]
    deployment_values = DagsterUserDeploymentsHelmValues.construct(
        imagePullSecrets=image_pull_secrets, )

    deployment_templates = subchart_template.render(deployment_values)

    assert len(deployment_templates) == 1

    deployment_template = deployment_templates[0]
    pod_spec = deployment_template.spec.template.spec

    assert pod_spec.image_pull_secrets[0].name == image_pull_secrets[0]["name"]
Example #4
0
def test_service_account_name(template: HelmTemplate):
    service_account_name = "service-account-name"
    service_account_values = DagsterHelmValues.construct(
        serviceAccount=ServiceAccount.construct(name=service_account_name,
                                                create=True))

    service_account_templates = template.render(service_account_values)

    assert len(service_account_templates) == 1

    service_account_template = service_account_templates[0]

    assert service_account_template.metadata.name == service_account_name
Example #5
0
def test_startup_probe_enabled(deployment_template: HelmTemplate,
                               enabled: bool):
    helm_values = DagsterHelmValues.construct(dagit=Dagit.construct(
        startupProbe=kubernetes.StartupProbe(enabled=enabled)))

    dagit = deployment_template.render(helm_values)
    assert len(dagit) == 1
    dagit = dagit[0]

    assert len(dagit.spec.template.spec.containers) == 1
    container = dagit.spec.template.spec.containers[0]

    assert (container.startup_probe is not None) == enabled
Example #6
0
def test_subchart_default_postgres_password(subchart_template: HelmTemplate):
    deployment_values = DagsterUserDeploymentsHelmValues.construct()

    deployment_templates = subchart_template.render(deployment_values)

    assert len(deployment_templates) == 1

    deployment_template = deployment_templates[0]
    pod_spec = deployment_template.spec.template.spec
    container = pod_spec.containers[0]

    assert container.env[1].name == "DAGSTER_PG_PASSWORD"
    assert container.env[1].value_from.secret_key_ref.name == "dagster-postgresql-secret"
Example #7
0
def test_daemon_default_image_tag_is_chart_version(template: HelmTemplate,
                                                   chart_version: str):
    helm_values = DagsterHelmValues.construct()

    daemon_deployments = template.render(helm_values,
                                         chart_version=chart_version)

    assert len(daemon_deployments) == 1

    image = daemon_deployments[0].spec.template.spec.containers[0].image
    _, image_tag = image.split(":")

    assert image_tag == chart_version
Example #8
0
def test_user_deployment_secrets_and_configmaps(
    template: HelmTemplate, include_config_in_launched_runs: bool
):
    name = "foo"

    secrets = [{"name": "my-secret"}, {"name": "my-other-secret"}]

    configmaps = [{"name": "my-configmap"}, {"name": "my-other-configmap"}]

    deployment = UserDeployment(
        name=name,
        image=kubernetes.Image(repository=f"repo/{name}", tag="tag1", pullPolicy="Always"),
        dagsterApiGrpcArgs=["-m", name],
        port=3030,
        envConfigMaps=[
            kubernetes.ConfigMapEnvSource.construct(None, **configmap) for configmap in configmaps
        ],
        envSecrets=[kubernetes.SecretEnvSource.construct(None, **secret) for secret in secrets],
        includeConfigInLaunchedRuns=UserDeploymentIncludeConfigInLaunchedRuns(
            enabled=include_config_in_launched_runs
        ),
    )

    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments(
            enabled=True,
            enableSubchart=True,
            deployments=[deployment],
        )
    )

    user_deployments = template.render(helm_values)

    assert len(user_deployments) == 1

    if include_config_in_launched_runs:
        container_context = user_deployments[0].spec.template.spec.containers[0].env[2]
        assert container_context.name == "DAGSTER_CLI_API_GRPC_CONTAINER_CONTEXT"
        assert json.loads(container_context.value) == {
            "k8s": {
                "image_pull_policy": "Always",
                "env_secrets": ["my-secret", "my-other-secret"],
                "env_config_maps": [
                    "release-name-dagster-user-deployments-foo-user-env",
                    "my-configmap",
                    "my-other-configmap",
                ],
            }
        }
    else:
        _assert_no_container_context(user_deployments[0])
Example #9
0
def test_dagit_labels(deployment_template: HelmTemplate):
    deployment_labels = {"deployment_label": "label"}
    pod_labels = {"pod_label": "label"}
    helm_values = DagsterHelmValues.construct(dagit=Dagit.construct(
        deploymentLabels=deployment_labels,
        labels=pod_labels,
    ))

    [dagit_deployment] = deployment_template.render(helm_values)

    assert set(deployment_labels.items()).issubset(
        dagit_deployment.metadata.labels.items())
    assert set(pod_labels.items()).issubset(
        dagit_deployment.spec.template.metadata.labels.items())
Example #10
0
def test_standalone_subchart_service_account_name(
        standalone_subchart_template: HelmTemplate):
    service_account_name = "service-account-name"
    service_account_values = DagsterUserDeploymentsHelmValues.construct(
        serviceAccount=ServiceAccount.construct(name=service_account_name), )

    service_account_templates = standalone_subchart_template.render(
        service_account_values)

    assert len(service_account_templates) == 1

    service_account_template = service_account_templates[0]

    assert service_account_template.metadata.name == service_account_name
Example #11
0
def test_celery_queue_default_image_tag_is_chart_version(
        deployment_template: HelmTemplate, chart_version: str):
    helm_values = DagsterHelmValues.construct(
        runLauncher=RunLauncher.construct(type=RunLauncherType.CELERY))

    celery_queue_deployments = deployment_template.render(
        helm_values, chart_version=chart_version)

    assert len(celery_queue_deployments) == 1

    image = celery_queue_deployments[0].spec.template.spec.containers[0].image
    _, image_tag = image.split(":")

    assert image_tag == chart_version
Example #12
0
def test_user_deployment_checksum_changes(template: HelmTemplate):
    pre_upgrade_helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments(
            enabled=True,
            enableSubchart=True,
            deployments=[
                create_simple_user_deployment("deployment-one"),
                create_simple_user_deployment("deployment-two"),
            ],
        )
    )
    post_upgrade_helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments(
            enabled=True,
            enableSubchart=True,
            deployments=[
                create_complex_user_deployment("deployment-one"),
                create_complex_user_deployment("deployment-two"),
            ],
        )
    )

    pre_upgrade_templates = template.render(pre_upgrade_helm_values)
    post_upgrade_templates = template.render(post_upgrade_helm_values)

    # User deployment templates with the same Helm values should not redeploy in a Helm upgrade
    for pre_upgrade_user_deployment, post_upgrade_user_deployment in zip(
        pre_upgrade_templates, post_upgrade_templates
    ):
        pre_upgrade_checksum = pre_upgrade_user_deployment.spec.template.metadata.annotations[
            "checksum/dagster-user-deployment"
        ]
        post_upgrade_checksum = post_upgrade_user_deployment.spec.template.metadata.annotations[
            "checksum/dagster-user-deployment"
        ]

        assert pre_upgrade_checksum != post_upgrade_checksum
Example #13
0
def test_ingress_tls(template: HelmTemplate):
    dagit_host = "dagit.com"
    dagit_readonly_host = "dagit-readonly.com"
    flower_host = "flower.com"

    dagit_tls_secret_name = "dagit_tls_secret_name"
    dagit_readonly_tls_secret_name = "dagit_readonly_tls_secret_name"
    flower_tls_secret_name = "flower_tls_secret_name"

    helm_values = DagsterHelmValues.construct(
        ingress=Ingress.construct(
            enabled=True,
            dagit=DagitIngressConfiguration.construct(
                host=dagit_host,
                tls=IngressTLSConfiguration(enabled=True,
                                            secretName=dagit_tls_secret_name),
            ),
            readOnlyDagit=DagitIngressConfiguration.construct(
                host=dagit_readonly_host,
                tls=IngressTLSConfiguration(
                    enabled=True, secretName=dagit_readonly_tls_secret_name),
            ),
            flower=FlowerIngressConfiguration.construct(
                host=flower_host,
                tls=IngressTLSConfiguration(enabled=True,
                                            secretName=flower_tls_secret_name),
            ),
        ),
        dagit=Dagit.construct(enableReadOnly=True),
    )

    [ingress] = template.render(helm_values)

    assert len(ingress.spec.tls) == 3

    dagit_tls = ingress.spec.tls[0]
    assert len(dagit_tls.hosts) == 1
    assert dagit_tls.hosts[0] == dagit_host
    assert dagit_tls.secret_name == dagit_tls_secret_name

    dagit_readonly_tls = ingress.spec.tls[1]
    assert len(dagit_readonly_tls.hosts) == 1
    assert dagit_readonly_tls.hosts[0] == dagit_readonly_host
    assert dagit_readonly_tls.secret_name == dagit_readonly_tls_secret_name

    flower_tls = ingress.spec.tls[2]
    assert len(flower_tls.hosts) == 1
    assert flower_tls.hosts[0] == flower_host
    assert flower_tls.secret_name == flower_tls_secret_name
Example #14
0
def test_startup_probe_enabled(template: HelmTemplate, enabled: bool):
    deployment = create_simple_user_deployment("foo")
    deployment.startupProbe = kubernetes.StartupProbe.construct(enabled=enabled)
    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments.construct(deployments=[deployment])
    )

    dagster_user_deployment = template.render(helm_values)
    assert len(dagster_user_deployment) == 1
    dagster_user_deployment = dagster_user_deployment[0]

    assert len(dagster_user_deployment.spec.template.spec.containers) == 1
    container = dagster_user_deployment.spec.template.spec.containers[0]

    assert (container.startup_probe is not None) == enabled
Example #15
0
def test_dagit_image_tag(deployment_template: HelmTemplate):
    repository = "repository"
    tag = "tag"
    helm_values = DagsterHelmValues.construct(dagit=Dagit.construct(
        image=kubernetes.Image.construct(repository=repository, tag=tag)))

    dagit_deployments = deployment_template.render(helm_values)

    assert len(dagit_deployments) == 1

    image = dagit_deployments[0].spec.template.spec.containers[0].image
    image_name, image_tag = image.split(":")

    assert image_name == repository
    assert image_tag == tag
Example #16
0
def test_dagit_default_image_tag_is_chart_version(
        deployment_template: HelmTemplate, enable_read_only: bool,
        chart_version: str):
    helm_values = DagsterHelmValues.construct(dagit=Dagit.construct(
        enableReadOnly=enable_read_only))

    dagit_deployments = deployment_template.render(helm_values,
                                                   chart_version=chart_version)

    assert len(dagit_deployments) == 1 + int(enable_read_only)

    for dagit_deployment in dagit_deployments:
        image = dagit_deployment.spec.template.spec.containers[0].image
        _, image_tag = image.split(":")

        assert image_tag == chart_version
Example #17
0
def test_readiness_probes(template: HelmTemplate):
    deployment = create_simple_user_deployment("foo")
    deployment.readinessProbe = kubernetes.ReadinessProbe.construct(timeout_seconds=3)
    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments.construct(deployments=[deployment])
    )

    dagster_user_deployment = template.render(helm_values)
    assert len(dagster_user_deployment) == 1
    dagster_user_deployment = dagster_user_deployment[0]

    assert len(dagster_user_deployment.spec.template.spec.containers) == 1
    container = dagster_user_deployment.spec.template.spec.containers[0]

    assert container.startup_probe is None
    assert container.startup_probe is None
    assert container.readiness_probe is not None
Example #18
0
def test_service_account_annotations(template: HelmTemplate):
    service_account_name = "service-account-name"
    service_account_annotations = {"hello": "world"}
    service_account_values = DagsterHelmValues.construct(
        serviceAccount=ServiceAccount.construct(
            name=service_account_name,
            create=True,
            annotations=service_account_annotations))

    service_account_templates = template.render(service_account_values)

    assert len(service_account_templates) == 1

    service_account_template = service_account_templates[0]

    assert service_account_template.metadata.name == service_account_name
    assert service_account_template.metadata.annotations == service_account_annotations
Example #19
0
def test_s3_compute_log_manager(template: HelmTemplate):
    bucket = "bucket"
    local_dir = "/dir"
    prefix = "prefix"
    use_ssl = True
    verify = True
    verify_cert_path = "/path"
    endpoint_url = "endpoint.com"
    skip_empty_files = True
    helm_values = DagsterHelmValues.construct(
        computeLogManager=ComputeLogManager.construct(
            type=ComputeLogManagerType.S3,
            config=ComputeLogManagerConfig.construct(
                s3ComputeLogManager=S3ComputeLogManagerModel(
                    bucket=bucket,
                    localDir=local_dir,
                    prefix=prefix,
                    useSsl=use_ssl,
                    verify=verify,
                    verifyCertPath=verify_cert_path,
                    endpointUrl=endpoint_url,
                    skipEmptyFiles=skip_empty_files,
                )
            ),
        )
    )

    configmaps = template.render(helm_values)
    instance = yaml.full_load(configmaps[0].data["dagster.yaml"])
    compute_logs_config = instance["compute_logs"]

    assert compute_logs_config["module"] == "dagster_aws.s3.compute_log_manager"
    assert compute_logs_config["class"] == "S3ComputeLogManager"
    assert compute_logs_config["config"] == {
        "bucket": bucket,
        "local_dir": local_dir,
        "prefix": prefix,
        "use_ssl": use_ssl,
        "verify": verify,
        "verify_cert_path": verify_cert_path,
        "endpoint_url": endpoint_url,
        "skip_empty_files": skip_empty_files,
    }

    # Test all config fields in configurable class
    assert compute_logs_config["config"].keys() == S3ComputeLogManager.config_type().keys()
Example #20
0
def test_subchart_postgres_password_global_override(subchart_template: HelmTemplate):
    deployment_values = DagsterUserDeploymentsHelmValues.construct(
        postgresqlSecretName="postgresql-secret",
        global_=Global.construct(
            postgresqlSecretName="global-postgresql-secret",
        ),
    )

    deployment_templates = subchart_template.render(deployment_values)

    assert len(deployment_templates) == 1

    deployment_template = deployment_templates[0]
    pod_spec = deployment_template.spec.template.spec
    container = pod_spec.containers[0]

    assert container.env[1].name == "DAGSTER_PG_PASSWORD"
    assert container.env[1].value_from.secret_key_ref.name == "global-postgresql-secret"
Example #21
0
def test_user_deployment_image(template: HelmTemplate):
    deployment = create_simple_user_deployment("foo")
    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments(
            enabled=True,
            enableSubchart=True,
            deployments=[deployment],
        ))

    user_deployments = template.render(helm_values)

    assert len(user_deployments) == 1

    image = user_deployments[0].spec.template.spec.containers[0].image
    image_name, image_tag = image.split(":")

    assert image_name == deployment.image.repository
    assert image_tag == deployment.image.tag
Example #22
0
def test_startup_probe_exec(template: HelmTemplate):
    deployment = create_simple_user_deployment("foo")
    deployment.startupProbe = kubernetes.StartupProbe.construct(
        enabled=True, exec=dict(command=["my", "command"]))
    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments.construct(
            deployments=[deployment]))

    dagster_user_deployment = template.render(helm_values)
    assert len(dagster_user_deployment) == 1
    dagster_user_deployment = dagster_user_deployment[0]

    assert len(dagster_user_deployment.spec.template.spec.containers) == 1
    container = dagster_user_deployment.spec.template.spec.containers[0]

    assert container.startup_probe._exec.command == [  # pylint:disable=protected-access
        "my",
        "command",
    ]
Example #23
0
def test_standalone_subchart_service_account_annotations(
    standalone_subchart_template: HelmTemplate, ):
    service_account_name = "service-account-name"
    service_account_annotations = {"hello": "world"}
    service_account_values = DagsterUserDeploymentsHelmValues.construct(
        serviceAccount=ServiceAccount.construct(
            name=service_account_name,
            create=True,
            annotations=service_account_annotations), )

    service_account_templates = standalone_subchart_template.render(
        service_account_values)

    assert len(service_account_templates) == 1

    service_account_template = service_account_templates[0]

    assert service_account_template.metadata.name == service_account_name
    assert service_account_template.metadata.annotations == service_account_annotations
Example #24
0
def test_dagit_port(deployment_template: HelmTemplate, service_port: int):
    helm_values = DagsterHelmValues.construct(
        dagit=Dagit.construct(service=kubernetes.Service(
            type="ClusterIP",
            port=service_port,
        ), ))

    dagit_template = deployment_template.render(helm_values)

    # Make sure dagit will start up serving the correct port
    dagit_command = "".join(
        dagit_template[0].spec.template.spec.containers[0].command)
    port_arg = f"-p {helm_values.dagit.service.port}"
    assert port_arg in dagit_command

    # Make sure k8s will open the correct port
    k8s_port = dagit_template[0].spec.template.spec.containers[0].ports[
        0].container_port
    assert k8s_port == service_port
Example #25
0
def test_user_deployment_labels(template: HelmTemplate, include_config_in_launched_runs: bool):
    name = "foo"

    labels = {"my-label-key": "my-label-val", "my-other-label-key": "my-other-label-val"}

    deployment = UserDeployment(
        name=name,
        image=kubernetes.Image(repository=f"repo/{name}", tag="tag1", pullPolicy="Always"),
        dagsterApiGrpcArgs=["-m", name],
        port=3030,
        labels=labels,
        includeConfigInLaunchedRuns=UserDeploymentIncludeConfigInLaunchedRuns(
            enabled=include_config_in_launched_runs
        ),
    )

    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments(
            enabled=True,
            enableSubchart=True,
            deployments=[deployment],
        )
    )

    user_deployments = template.render(helm_values)

    assert len(user_deployments) == 1

    if include_config_in_launched_runs:
        container_context = user_deployments[0].spec.template.spec.containers[0].env[2]
        assert container_context.name == "DAGSTER_CLI_API_GRPC_CONTAINER_CONTEXT"
        assert json.loads(container_context.value) == {
            "k8s": {
                "image_pull_policy": "Always",
                "env_config_maps": [
                    "release-name-dagster-user-deployments-foo-user-env",
                ],
                "labels": labels,
            }
        }
    else:
        _assert_no_container_context(user_deployments[0])
Example #26
0
def test_queued_run_coordinator_config(template: HelmTemplate, enabled: bool):
    max_concurrent_runs = 50
    tag_concurrency_limits = [
        TagConcurrencyLimit(key="key", value="value", limit=10)
    ]
    dequeue_interval_seconds = 50
    helm_values = DagsterHelmValues.construct(dagsterDaemon=Daemon.construct(
        runCoordinator=RunCoordinator.construct(
            enabled=enabled,
            type=RunCoordinatorType.QUEUED,
            config=RunCoordinatorConfig.construct(
                queuedRunCoordinator=QueuedRunCoordinatorConfig.construct(
                    maxConcurrentRuns=max_concurrent_runs,
                    tagConcurrencyLimits=tag_concurrency_limits,
                    dequeueIntervalSeconds=dequeue_interval_seconds,
                )),
        )))
    configmaps = template.render(helm_values)
    assert len(configmaps) == 1

    instance = yaml.full_load(configmaps[0].data["dagster.yaml"])

    assert ("run_coordinator" in instance) == enabled
    if enabled:
        assert instance["run_coordinator"][
            "module"] == "dagster.core.run_coordinator"
        assert instance["run_coordinator"]["class"] == "QueuedRunCoordinator"
        assert instance["run_coordinator"]["config"]

        run_coordinator_config = instance["run_coordinator"]["config"]

        assert run_coordinator_config[
            "max_concurrent_runs"] == max_concurrent_runs
        assert run_coordinator_config[
            "dequeue_interval_seconds"] == dequeue_interval_seconds

        assert len(run_coordinator_config["tag_concurrency_limits"]) == len(
            tag_concurrency_limits)
        assert run_coordinator_config["tag_concurrency_limits"] == [
            tag_concurrency_limit.dict()
            for tag_concurrency_limit in tag_concurrency_limits
        ]
Example #27
0
def test_celery_queue_image(deployment_template: HelmTemplate):
    repository = "repository"
    tag = "tag"
    helm_values = DagsterHelmValues.construct(runLauncher=RunLauncher(
        type=RunLauncherType.CELERY,
        config=RunLauncherConfig(
            celeryK8sRunLauncher=CeleryK8sRunLauncherConfig.construct(
                image=kubernetes.Image.construct(repository=repository,
                                                 tag=tag))),
    ))

    celery_queue_deployments = deployment_template.render(helm_values)

    assert len(celery_queue_deployments) == 1

    image = celery_queue_deployments[0].spec.template.spec.containers[0].image
    image_name, image_tag = image.split(":")

    assert image_name == repository
    assert image_tag == tag
Example #28
0
def test_default_redis_config(template: HelmTemplate):
    helm_values = DagsterHelmValues.construct(
        generateCeleryConfigSecret=True,
        runLauncher=RunLauncher.construct(type=RunLauncherType.CELERY),
        redis=Redis.construct(
            enabled=True,
            host="myhost",
        ),
    )
    [secret] = template.render(helm_values)

    expected_celery_broker = "redis://myhost:6379/0"
    expected_celery_backend = "redis://myhost:6379/0"

    assert secret.data["DAGSTER_CELERY_BROKER_URL"] == base64.b64encode(
        bytes(expected_celery_broker, encoding="utf-8")
    ).decode("utf-8")
    assert secret.data["DAGSTER_CELERY_BACKEND_URL"] == base64.b64encode(
        bytes(expected_celery_backend, encoding="utf-8")
    ).decode("utf-8")
Example #29
0
def test_startup_probe_default_exec(template: HelmTemplate):
    deployment = create_simple_user_deployment("foo")
    deployment.startupProbe = kubernetes.StartupProbe.construct(enabled=True)
    helm_values = DagsterHelmValues.construct(
        dagsterUserDeployments=UserDeployments.construct(
            deployments=[deployment]))

    dagster_user_deployment = template.render(helm_values)
    assert len(dagster_user_deployment) == 1
    dagster_user_deployment = dagster_user_deployment[0]

    assert len(dagster_user_deployment.spec.template.spec.containers) == 1
    container = dagster_user_deployment.spec.template.spec.containers[0]

    assert container.startup_probe._exec.command == [  # pylint: disable=protected-access
        "dagster",
        "api",
        "grpc-health-check",
        "-p",
        str(deployment.port),
    ]
Example #30
0
def test_celery_backend_override_connection_string(template: HelmTemplate):
    broker_url = "host:6380,password=password,ssl=True"
    backend_url = "host:6381,password=password,ssl=True"
    helm_values = DagsterHelmValues.construct(
        generateCeleryConfigSecret=True,
        runLauncher=RunLauncher.construct(type=RunLauncherType.CELERY),
        redis=Redis.construct(
            enabled=True,
            brokerUrl=broker_url,
            backendUrl=backend_url,
        ),
    )

    [secret] = template.render(helm_values)

    assert secret.data["DAGSTER_CELERY_BROKER_URL"] == base64.b64encode(
        bytes(broker_url, encoding="utf-8")
    ).decode("utf-8")
    assert secret.data["DAGSTER_CELERY_BACKEND_URL"] == base64.b64encode(
        bytes(backend_url, encoding="utf-8")
    ).decode("utf-8")