solutions_builder/cli/vars.py (94 lines of code) (raw):

# Copyright 2023 Google LLC # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # https://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import typer import pathlib import re import jinja2 from typing import Optional from typing_extensions import Annotated from .cli_utils import * from .cli_constants import DEBUG vars_app = typer.Typer() INCLUDE_PATTERNS = [ "*.yaml", "*.yml", "*.env", "*.tfvars", "*.tf", "*.sh" ] EXCLUDE_PATTERNS = ["**/.terraform/**/*.*", "**/node_modules/**/*.*", "**/.venv/**/*.*"] # Replace a variable with a given text content. def replace_var_to_template(var_name, text, custom_template=False): # Regex test: https://regex101.com/r/XtnJQI/4 # match_pattern matches lines with sb-var anchor in the comment at the end. # For example: # PROJECT_ID: 12345 # sb-var:project_id # region = "us-central1" # sb-var:region match_pattern = f"(\\s*[\":=-][ ]*)(-[ ]*)?([\"\']?)([^\"^\'^\r^\n]*)([\"\']?)\\s*#\\s*sb-var:{var_name}" # output_pattern prints the jinja2 template for the specific variable name. # For example: # PROJECT_ID: {{project_id}} # sb-var:project_id output_pattern = f"\\1\\2\\3{{{{{var_name}}}}}\\5 # sb-var:{var_name}" # In addition, if custom_template is true, the pattern will extend to the custom # template string at the end of the anchor. For example: # BUCKET_NAME: my-project-bucket # sb-var:project_id:{{project_id}}-bucket if custom_template: match_pattern = match_pattern + ":(.*)" output_pattern = f"\\1\\2\\3\\6\\5 # sb-var:{var_name}:\\6" # Replace with regex pattern and returns new text and count of changes. text, count = re.subn(match_pattern, output_pattern, text) return (text, count) def restore_template_in_comment(var_name, var_value, text): # Restore jinja2 variables in the custom content comment. match_pattern = f"(#\\s*sb-var:{var_name}:)(.*){var_value}(.*)" output_pattern = f"\\1\\2{{{{{var_name}}}}}\\3" text, count = re.subn(match_pattern, output_pattern, text) return (text, count) def replace_var_to_value(var_name, var_value, text): overall_count = 0 # Replace simple variable pattern with sb-var:var_name text, count = replace_var_to_template(var_name, text) overall_count += count # Replace custom-content variable pattern with sb-var:var_name:custom_template text, count = replace_var_to_template(var_name, text, custom_template=True) overall_count += count # Update variables using Jinja2 jinja_env = jinja2.Environment() text = text.replace("# copier:raw", "# copier:raw{% raw %}") text = text.replace("# copier:endraw", "# copier:endraw{% endraw %}") template = jinja_env.from_string(text) # Set vars data for jinja data = {} data[var_name] = var_value # Apply variable values using Jinja2 text = template.render(**data) # Restore vars to template in comment. text, count = restore_template_in_comment(var_name, var_value, text) return (text, overall_count) # Apply a specific variable with a new value. def apply_var_to_folder(solution_path, var_name, var_value): file_set = set() # Adding includes. for pattern in INCLUDE_PATTERNS: file_list = pathlib.Path(solution_path).rglob(f"{pattern}") file_set.update(set([str(x) for x in file_list])) # Removing excludes. for pattern in EXCLUDE_PATTERNS: file_list = pathlib.Path(solution_path).rglob(f"{pattern}") file_set = file_set - set([str(x) for x in file_list]) modified_files_list = [] for filename in list(file_set): if DEBUG: print(filename) with open(filename, "r") as file: # Replace variable filedata = file.read() filedata, count = replace_var_to_value(var_name, var_value, filedata) filedata = filedata + "\n" if count > 0: modified_files_list.append(filename) # If there's any changes, write back to the original file. if count > 0: with open(filename, "w") as file: file.write(filedata) return modified_files_list # CLI command for `sb vars set <var_name> <var_value>` @vars_app.command(name="set") def set_var( var_name, var_value, solution_path: Annotated[Optional[str], typer.Argument()] = ".", ): validate_solution_folder(solution_path) print(f"Setting variable '{var_name}' to '{var_value}'...") # Update to the root sb.yaml root_st_yaml = read_yaml(f"{solution_path}/sb.yaml") global_variables = root_st_yaml.get("global_variables", {}) global_variables[var_name] = var_value root_st_yaml["global_variables"] = global_variables write_yaml(f"{solution_path}/sb.yaml", root_st_yaml) # Apply vars to individual files filenames = apply_var_to_folder(solution_path, var_name, var_value) print_success( f"Complete. {len(filenames)} files updated.\n" ) # CLI command for `sb vars apply-all` @vars_app.command(name="apply-all") def apply_all( solution_path: Annotated[Optional[str], typer.Argument()] = ".", ): validate_solution_folder(solution_path) # Update to the root sb.yaml root_st_yaml = read_yaml(f"{solution_path}/sb.yaml") global_variables = root_st_yaml.get("global_variables", {}) filenames = [] for var_name, var_value in global_variables.items(): filenames += apply_var_to_folder(solution_path, var_name, var_value) print_success( f"Complete. {len(filenames)} files updated.\n" )