diff --git a/src/_nebari/stages/terraform_state/__init__.py b/src/_nebari/stages/terraform_state/__init__.py index edee2db71..e0f643ed3 100644 --- a/src/_nebari/stages/terraform_state/__init__.py +++ b/src/_nebari/stages/terraform_state/__init__.py @@ -6,7 +6,7 @@ import re from typing import Any, Dict, List, Optional, Tuple, Type -from pydantic import field_validator +from pydantic import BaseModel, field_validator from _nebari import utils from _nebari.provider import terraform @@ -275,9 +275,15 @@ def check_immutable_fields(self): bottom_level_schema = bottom_level_schema[key] else: raise e - extra_field_schema = schema.ExtraFieldSchema( - **bottom_level_schema.model_fields[keys[-1]].json_schema_extra or {} - ) + + # Return a default (mutable) extra field schema if bottom level is not a Pydantic model (such as a free-form 'overrides' block) + if isinstance(bottom_level_schema, BaseModel): + extra_field_schema = schema.ExtraFieldSchema( + **bottom_level_schema.model_fields[keys[-1]].json_schema_extra or {} + ) + else: + extra_field_schema = schema.ExtraFieldSchema() + if extra_field_schema.immutable: key_path = ".".join(keys) raise ValueError( diff --git a/tests/tests_unit/cli_validate/local.happy.yaml b/tests/tests_unit/cli_validate/local.happy.yaml index e6ec771e0..3fd31f978 100644 --- a/tests/tests_unit/cli_validate/local.happy.yaml +++ b/tests/tests_unit/cli_validate/local.happy.yaml @@ -23,3 +23,8 @@ theme: certificate: type: lets-encrypt acme_email: test@example.com +jupyterhub: + overrides: + singleuser: + extraEnv: + TEST_ENV: "my_env" diff --git a/tests/tests_unit/test_stages.py b/tests/tests_unit/test_stages.py index 74e6d3f3d..c716d9303 100644 --- a/tests/tests_unit/test_stages.py +++ b/tests/tests_unit/test_stages.py @@ -103,3 +103,18 @@ def test_check_immutable_fields_old_nebari_version( # This should not raise an exception terraform_state_stage.check_immutable_fields() + + +@patch.object(TerraformStateStage, "get_nebari_config_state") +def test_check_immutable_fields_change_dict_any( + mock_get_state, terraform_state_stage, mock_config +): + old_config = mock_config.model_copy(deep=True).model_dump() + # Change the value of a config deep in 'overrides' block + old_config["jupyterhub"]["overrides"]["singleuser"]["extraEnv"][ + "TEST_ENV" + ] = "new_value" + mock_get_state.return_value = old_config + + # This should not raise an exception + terraform_state_stage.check_immutable_fields()