Skip to content

jac-scale: KubernetesTarget.deploy() ignores app_config.code_folder and reads the host process's jac.toml #6148

@udithishanka

Description

@udithishanka

Summary

KubernetesTarget.deploy(app_config) is documented to deploy the app at app_config.code_folder, but the database providers (MongoDB, Redis) and several other code paths inside the deploy actually read configuration from the host process's jac.toml via the global get_scale_config(). When jac-scale is used programmatically from a long-lived host (e.g. jacBuilder calling target.deploy() to deploy user projects), the user's app inherits the host's infrastructure settings instead of its own.

Reproduction

  1. Process A (e.g. jacBuilder) has its own jac.toml with [plugins.scale.kubernetes].mongodb_storage_size = "5Gi".
  2. Process A calls:
    target = KubernetesTarget(config=KubernetesConfig(app_name="userapp", namespace="userns"));
    app_config = AppConfig(code_folder="/tmp/userapp", file_name="main.jac", build=False);
    target.deploy(app_config);
  3. /tmp/userapp/jac.toml has mongodb_storage_size = "20Gi".
  4. Expected: user's MongoDB StatefulSet is created with a 20Gi PVC.
  5. Actual: PVC is 5Gi, taken from the host's jac.toml. The user's jac.toml is never read.

In the failure that triggered this report, jacBuilder's jac.toml had mongodb_storage_size = \"\${MONGO_STORAGE_SIZE}\" with no default. MONGO_STORAGE_SIZE was unset in the running pod, the substitution resolved to an empty string, and every user production deploy 400'd with:

StatefulSet "v1" cannot be handled as a StatefulSet: quantities must match the regular expression '^([+-]?[0-9.]+)([eEinumkKMGTP][-+]?[0-9])$'

after code-server, the NGINX ingress, and an AWS NLB were already provisioned, leaving an orphan namespace and a paid load balancer for every attempt.

Root Cause

get_scale_config() caches a single global JacScaleConfig instance and discards its project_dir argument on subsequent calls:

jac_scale/impl/config_loader.impl.jac:348-356

impl get_scale_config(project_dir: Path | None = None) -> JacScaleConfig {
    global _scale_config_instance;
    if _scale_config_instance is not None {
        return _scale_config_instance;
    }
    new_instance = JacScaleConfig(project_dir);
    _scale_config_instance = new_instance;
    return new_instance;
}

Every provider then calls the no-arg form and gets the cached instance:

KubernetesTarget.deploy() (kubernetes_target.jac:1266) does correctly use app_config.code_folder for load_env_variables (line 1301), but never threads it into the providers' config lookups.

KubernetesConfig passed to the constructor only carries app_name and namespace; the rest of the K8s config (storage size, replicas, image registry, secrets) is read live from the global cached JacScaleConfig by each provider.

Impact

Any host application that uses jac-scale programmatically to deploy on behalf of users will have its own infrastructure settings leak into every user deploy. Beyond storage size, this affects:

  • image_registry (deploys could push to the host's registry instead of the user's)
  • redis_url and Redis credentials
  • mongodb_root_username / mongodb_root_password
  • domain and cert_manager_email
  • Everything under [plugins.scale.secrets]

The CLI flow (jac start --scale from the user's project directory) is unaffected because in that case the cached config is the user's config.

Proposed Fix

Three surgical changes that preserve CLI behavior:

  1. config_loader.impl.jac - replace the single-instance cache with a per-path cache keyed on the resolved project directory. Calls with no arg continue to use cwd, calls with a path get their own instance.

  2. KubernetesTarget.deploy(app_config) - immediately after entry, load get_scale_config(Path(app_config.code_folder)) and use that for the rest of the deploy. Pass the resolved KubernetesConfig into DatabaseProviderFactory.create() and any other provider constructors.

  3. Database providers (kubernetes_mongo.jac, kubernetes_redis.jac, others) - accept an optional config dict from the caller and prefer it over get_scale_config(). Fall through to the global for backward compatibility.

CLI users notice nothing. Programmatic deploys correctly use the deployed app's jac.toml.

Workaround (jacBuilder, shipped)

Added a default to jacBuilder's own jac.toml so the empty substitution can't 400 user deploys: mongodb_storage_size = \"\${MONGO_STORAGE_SIZE:-1Gi}\". This only stops jacBuilder's empty value from poisoning user deploys; it does not fix the underlying issue that user jac.toml settings are ignored.

Labels

bug, jac-scale

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions