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
- Process A (e.g. jacBuilder) has its own
jac.toml with [plugins.scale.kubernetes].mongodb_storage_size = "5Gi".
- 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);
/tmp/userapp/jac.toml has mongodb_storage_size = "20Gi".
- Expected: user's MongoDB StatefulSet is created with a 20Gi PVC.
- 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:
-
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.
-
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.
-
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
Summary
KubernetesTarget.deploy(app_config)is documented to deploy the app atapp_config.code_folder, but the database providers (MongoDB, Redis) and several other code paths inside the deploy actually read configuration from the host process'sjac.tomlvia the globalget_scale_config(). When jac-scale is used programmatically from a long-lived host (e.g. jacBuilder callingtarget.deploy()to deploy user projects), the user's app inherits the host's infrastructure settings instead of its own.Reproduction
jac.tomlwith[plugins.scale.kubernetes].mongodb_storage_size = "5Gi"./tmp/userapp/jac.tomlhasmongodb_storage_size = "20Gi".jac.toml. The user'sjac.tomlis never read.In the failure that triggered this report, jacBuilder's
jac.tomlhadmongodb_storage_size = \"\${MONGO_STORAGE_SIZE}\"with no default.MONGO_STORAGE_SIZEwas unset in the running pod, the substitution resolved to an empty string, and every user production deploy 400'd with: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 globalJacScaleConfiginstance and discards itsproject_dirargument on subsequent calls:jac_scale/impl/config_loader.impl.jac:348-356
Every provider then calls the no-arg form and gets the cached instance:
KubernetesTarget.deploy()(kubernetes_target.jac:1266) does correctly useapp_config.code_folderforload_env_variables(line 1301), but never threads it into the providers' config lookups.KubernetesConfigpassed to the constructor only carriesapp_nameandnamespace; the rest of the K8s config (storage size, replicas, image registry, secrets) is read live from the global cachedJacScaleConfigby 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_urland Redis credentialsmongodb_root_username/mongodb_root_passworddomainandcert_manager_email[plugins.scale.secrets]The CLI flow (
jac start --scalefrom 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:
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.KubernetesTarget.deploy(app_config)- immediately after entry, loadget_scale_config(Path(app_config.code_folder))and use that for the rest of the deploy. Pass the resolvedKubernetesConfigintoDatabaseProviderFactory.create()and any other provider constructors.Database providers (
kubernetes_mongo.jac,kubernetes_redis.jac, others) - accept an optional config dict from the caller and prefer it overget_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.tomlso 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 userjac.tomlsettings are ignored.Labels
bug, jac-scale