diff --git a/src/envars/cli.py b/src/envars/cli.py index f25e3ef..31d09db 100644 --- a/src/envars/cli.py +++ b/src/envars/cli.py @@ -33,7 +33,7 @@ def _resolve_and_print_context( ctx: typer.Context, loc: str | None, env: str | None -) -> tuple[VariableManager, str, str | None]: +) -> tuple[VariableManager, str | None, str | None]: """Resolves location and environment, printing debug info if verbose.""" if env: os.environ["ENVARS_ENV"] = env @@ -42,14 +42,33 @@ def _resolve_and_print_context( resolved_loc = loc if resolved_loc is None: - resolved_loc = get_default_location_name(manager) - if resolved_loc is None: - error_console.print("[bold red]Error:[/] Could not determine default location. Please specify with --loc.") + if not manager.locations: + # No locations configured, so no need to resolve a default or error + if verbose: + console.print("[dim]DEBUG: No locations configured, proceeding without a specific location.[/dim]") + resolved_loc = None # Explicitly set to None + else: + # Locations are configured, try to auto-detect + resolved_loc = get_default_location_name(manager) + if resolved_loc is None: + error_console.print( + "[bold red]Error:[/] Could not determine default location. Please specify with --loc." + ) + raise typer.Exit(code=1) + if verbose: + console.print(f"[dim]DEBUG: Auto-detected location: '{resolved_loc}'[/dim]") + else: + # A location was explicitly provided, validate it + if not manager.locations: + error_console.print( + "[bold red]Error:[/] 'locations' are not configured for use in the project. Cannot use '--loc'." + ) + raise typer.Exit(code=1) + if not any(l.name == resolved_loc for l in manager.locations.values()): + error_console.print(f"[bold red]Error:[/] Location '{resolved_loc}' not found in configuration.") raise typer.Exit(code=1) if verbose: - console.print(f"[dim]DEBUG: Auto-detected location: '{resolved_loc}'[/dim]") - elif verbose: - console.print(f"[dim]DEBUG: Using specified location: '{resolved_loc}'[/dim]") + console.print(f"[dim]DEBUG: Using specified location: '{resolved_loc}'[/dim]") final_env = env if final_env is None: @@ -59,7 +78,7 @@ def _resolve_and_print_context( elif verbose: console.print(f"[dim]DEBUG: Using specified environment: '{final_env}'[/dim]") - return manager, resolved_loc, env + return manager, resolved_loc, final_env @app.command(name="init") @@ -67,7 +86,7 @@ def init_envars( ctx: typer.Context, app_name: str = typer.Option(..., "--app", "-a", help="Application name."), env: str = typer.Option(..., "--env", "-e", help="Comma-separated list of environments."), - loc: str = typer.Option(..., "--loc", "-l", help="Comma-separated list of locations in name:id format."), + loc: str = typer.Option("", "--loc", "-l", help="Comma-separated list of locations in name:id format."), kms_key: str = typer.Option(None, "--kms-key", "-k", help="Global KMS key."), force: bool = typer.Option(False, "--force", help="Overwrite existing envars.yml file."), description_mandatory: bool = typer.Option( @@ -88,14 +107,15 @@ def init_envars( for env_name in environments: manager.add_environment(EnvarsEnvironment(name=env_name)) - locations = [l.strip() for l in loc.split(",")] - for loc_item in locations: - try: - name, loc_id = loc_item.split(":", 1) - manager.add_location(Location(name=name, location_id=loc_id)) - except ValueError as e: - error_console.print(f"[bold red]Error:[/] Invalid location format: {loc_item}. Use name:id.") - raise typer.Exit(code=1) from e + if loc: + locations = [l.strip() for l in loc.split(",")] + for loc_item in locations: + try: + name, loc_id = loc_item.split(":", 1) + manager.add_location(Location(name=name, location_id=loc_id)) + except ValueError as e: + error_console.print(f"[bold red]Error:[/] Invalid location format: {loc_item}. Use name:id.") + raise typer.Exit(code=1) from e try: write_envars_yml(manager, file_path) @@ -310,26 +330,26 @@ def add_env_var( environment_name = None location_id = None - if env and loc: - scope_type = "SPECIFIC" - environment_name = env - # Find location_id by name + if loc: + if not manager.locations: + error_console.print( + "[bold red]Error:[/] 'locations' are not configured for use in the project. Cannot use '--loc'." + ) + raise typer.Exit(code=1) found_loc = next((l for l in manager.locations.values() if l.name == loc), None) if not found_loc: error_console.print(f"[bold red]Error:[/bold red] Location '{loc}' not found.") raise typer.Exit(code=1) location_id = found_loc.location_id + + if env and loc: + scope_type = "SPECIFIC" + environment_name = env elif env: scope_type = "ENVIRONMENT" environment_name = env elif loc: scope_type = "LOCATION" - # Find location_id by name - found_loc = next((l for l in manager.locations.values() if l.name == loc), None) - if not found_loc: - error_console.print(f"[bold red]Error:[/bold red] Location '{loc}' not found.") - raise typer.Exit(code=1) - location_id = found_loc.location_id new_var_value = VariableValue( variable_name=var_name, diff --git a/src/envars/main.py b/src/envars/main.py index a962b77..2b25664 100644 --- a/src/envars/main.py +++ b/src/envars/main.py @@ -200,10 +200,11 @@ def write_envars_yml(manager: VariableManager, file_path: str): "kms_key": manager.kms_key, "description_mandatory": manager.description_mandatory, "environments": sorted(manager.environments.keys()), - "locations": locations_data, }, "environment_variables": {}, } + if locations_data: + data["configuration"]["locations"] = locations_data # Populate environment_variables sorted_vars = sorted(manager.variables.items()) @@ -265,10 +266,11 @@ def write_envars_yml(manager: VariableManager, file_path: str): if any(data["configuration"].values()): config_data = {"configuration": data["configuration"]} # Sort locations list of dicts - config_data["configuration"]["locations"] = sorted( - config_data["configuration"]["locations"], - key=lambda x: list(x.keys())[0], - ) + if "locations" in config_data["configuration"]: + config_data["configuration"]["locations"] = sorted( + config_data["configuration"]["locations"], + key=lambda x: list(x.keys())[0], + ) yaml.dump(config_data, f, sort_keys=False, Dumper=yaml.Dumper) f.write("\n") @@ -362,7 +364,7 @@ def _check_for_circular_dependencies(variables: dict[str, str | Secret]): def _get_resolved_variables( manager: VariableManager, - loc: str, + loc: str | None, env: str | None, decrypt: bool, ) -> dict[str, str | Secret]: @@ -375,7 +377,8 @@ def _get_resolved_variables( if env not in manager.environments: raise ValueError(f"Environment '{env}' not found in configuration.") - if not any(l.name == loc for l in manager.locations.values()): + # Only validate location if a specific one is provided and locations are configured + if loc is not None and not any(l.name == loc for l in manager.locations.values()): raise ValueError(f"Location '{loc}' not found in configuration.") resolved_vars = {} diff --git a/tests/test_cli.py b/tests/test_cli.py index 76d3c72..fccceed 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -70,6 +70,30 @@ def test_init_command(tmp_path): assert data["configuration"]["description_mandatory"] is False +def test_init_command_no_locations(tmp_path): + file_path = tmp_path / "envars_no_loc.yml" + result = runner.invoke( + app, + [ + "--file", + str(file_path), + "init", + "--app", + "MyApp", + "--env", + "dev,prod", + ], + ) + assert result.exit_code == 0 + assert "Successfully initialized" in result.stdout + + data = read_yaml_file(file_path) + assert data["configuration"]["app"] == "MyApp" + assert data["configuration"]["environments"] == ["dev", "prod"] + assert "locations" not in data["configuration"] + assert data["configuration"]["description_mandatory"] is False + + def test_add_default_variable(tmp_path): file_path = create_envars_file(tmp_path) result = runner.invoke(app, ["--file", file_path, "add", "MY_VAR=my_value"]) @@ -201,12 +225,24 @@ def test_add_variable_invalid_format(tmp_path): def test_add_variable_non_existent_location(tmp_path): - file_path = create_envars_file(tmp_path) + initial_content = """ +configuration: + locations: + - my_loc: "loc123" +""" + file_path = create_envars_file(tmp_path, initial_content) result = runner.invoke(app, ["--file", file_path, "add", "MY_VAR=value", "--loc", "non_existent_loc"]) assert result.exit_code == 1 assert "Location 'non_existent_loc' not found" in result.stderr +def test_add_variable_with_loc_but_no_locations_configured(tmp_path): + file_path = create_envars_file(tmp_path) + result = runner.invoke(app, ["--file", file_path, "add", "MY_VAR=value", "--loc", "some_loc"]) + assert result.exit_code == 1 + assert "locations' are not configured" in result.stderr + + def test_add_variable_non_existent_environment_for_specific(tmp_path): initial_content = """ configuration: @@ -471,6 +507,18 @@ def test_print_invalid_loc(tmp_path): assert "Location 'other_loc' not found" in result.stderr +def test_output_invalid_loc_no_locations_configured(tmp_path): + initial_content = """ +configuration: + environments: + - dev +""" + file_path = create_envars_file(tmp_path, initial_content) + result = runner.invoke(app, ["--file", file_path, "output", "--env", "dev", "--loc", "other_loc"]) + assert result.exit_code == 1 + assert "'locations' are not configured" in result.stderr + + def test_exec_invalid_env(tmp_path): initial_content = """ configuration: @@ -1253,7 +1301,7 @@ def test_add_value_from_file(tmp_path): -----END OPENSSH PRIVATE KEY-----""" value_file.write_text(file_content) - result = runner.invoke( + runner.invoke( app, [ "--file", @@ -1265,13 +1313,28 @@ def test_add_value_from_file(tmp_path): str(value_file), ], ) - assert result.exit_code == 0 - assert "Successfully added/updated MY_VAR" in result.stdout - data = read_yaml_file(file_path) assert data["environment_variables"]["MY_VAR"]["default"] == file_content +def test_output_command_no_locations(tmp_path): + initial_content = """ +configuration: + app: test-app + environments: + - prod + - staging +environment_variables: + TEST: + default: "test_value" +""" + file_path = create_envars_file(tmp_path, initial_content) + + result = runner.invoke(app, ["--file", file_path, "output", "--env", "prod"]) + assert result.exit_code == 0 + assert "TEST=test_value" in result.stdout + + class TestCircularDependency: def test_add_circular_dependency(self, tmp_path): initial_content = """