solutions_builder/cli/cli.py (253 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 traceback
import os
import re
import importlib.metadata
from typing import Optional
from typing_extensions import Annotated
from copier import run_copy
from .add import add_command
from .update import update_command
from .component import list_components
from .infra import infra_app
from .template import template_app
from .set import set_app, project_id as set_project_id
from .vars import vars_app, set_var
from .list import list_app
from .cli_utils import *
from .cli_constants import DEBUG, PLACEHOLDER_VALUES
__version__ = importlib.metadata.version("solutions-builder")
DEFAULT_DEPLOY_PROFILE = "cloudrun"
app = typer.Typer(
add_completion=False,
help="Solutions Builder CLI. See https://github.com/GoogleCloudPlatform/solutions-builder for details."
)
app.add_typer(add_command,
name="add",
help="Add components.")
app.add_typer(update_command,
name="update",
help="Update components.")
app.add_typer(infra_app,
name="infra",
help="Alias of [terraform] commands. Manage infrastructure (terraform).")
app.add_typer(infra_app,
name="terraform",
help="Manage infrastructure (terraform).")
app.add_typer(template_app,
name="template",
help="Create or update module templates.")
app.add_typer(set_app,
name="set",
help="Set properties to an existing solution folder.")
app.add_typer(vars_app,
name="vars",
help="Set variables in an existing solution folder.")
app.add_typer(list_app,
name="list",
help="List modules or other resources.")
# Create a new solution
@app.command()
def new(folder_name,
output_dir: Annotated[Optional[str], typer.Argument()] = ".",
template_path: Annotated[str,
typer.Option("--template", "-t")] = None,
answers=None):
"""
Create a new solution folder.
"""
output_path = f"{output_dir}/{folder_name}"
output_path = output_path.replace("//", "/")
answers_dict = get_answers_dict(answers)
if os.path.exists(output_path):
raise FileExistsError(f"Solution folder {output_path} already exists.")
# If the component name is a Git URL, use the URL as-is in copier.
if template_path:
if check_git_url(template_path):
template_path = clone_remote_git(template_path)
else:
if not os.path.exists(template_path):
raise FileNotFoundError(
f"Template '{template_path}' does not exist.")
else:
current_dir = os.path.dirname(__file__)
template_path = f"{current_dir}/../template_root"
# Copy template_root to destination.
print(f"template_path: {template_path}\n")
answers_dict["folder_name"] = folder_name
verify_copier_file(template_path)
worker = run_copy(template_path, output_path, data=answers_dict, unsafe=True)
# Get answer values inputed by user.
answers = worker.answers.user
# Optionally set up base terraform
if answers.get("terraform_base"):
run_module_template("terraform_base", dest_dir=output_path, data=answers)
# Optionally set up CI/CD
cicd_type = answers.get("cicd")
if cicd_type:
run_module_template(f"cicd_{cicd_type}",
dest_dir=output_path, data=answers)
print_success(f"Complete. New solution folder created at {output_path}.\n")
# Build and deploy services.
@app.command()
def deploy(
profile: Annotated[str, typer.Option(
"--profile", "-p")] = DEFAULT_DEPLOY_PROFILE,
component: Annotated[str, typer.Option(
"--component", "-c", "-m")] = None,
namespace: Annotated[str, typer.Option("--namespace", "-n")] = None,
dev: Optional[bool] = False,
dev_cleanup: Optional[bool] = False,
solution_path: Annotated[Optional[str],
typer.Argument()] = ".",
skaffold_args: Optional[str] = "",
yes: Optional[bool] = False):
"""
Build and deploy services.
"""
validate_solution_folder(solution_path)
sb_yaml = read_yaml(f"{solution_path}/sb.yaml")
global_variables = sb_yaml.get("global_variables", {})
# Get global vars from sb.yaml.
project_id = global_variables.get("project_id", None)
region = global_variables.get("region", None)
assert project_id, "project_id is not set in 'global_variables' in sb.yaml."
assert region, "region is not set in 'global_variables' in sb.yaml."
# Check default deploy method.
if not profile:
profile = global_variables.get("deploy_method", "cloudrun")
# Check namespace
deploy_config = sb_yaml.get("deploy", {})
if deploy_config.get("require_namespace") not in [None, False, ""] \
and not namespace:
assert namespace, "Please set namespace with --namespace or -n"
if project_id in PLACEHOLDER_VALUES:
project_id = None
while not project_id:
project_id = input("Please set the GCP project ID: ")
print()
set_project_id(project_id)
# Reload sb.yaml
sb_yaml = read_yaml(f"{solution_path}/sb.yaml")
global_variables = sb_yaml.get("global_variables", {})
# Set gcloud to project_id
set_gcloud_project(project_id)
create_default_artifact_repo(project_id, "default", "us")
commands = []
component_flag = f" -m {component} " if component else ""
no_prune_flag = " --no-prune " if not dev_cleanup else ""
port_forwarding_flag = ""
if dev:
skaffold_command = "skaffold dev"
port_forwarding_flag = "--port-forward"
else:
skaffold_command = "skaffold run"
# Get terraform_gke component settings.
terraform_gke = sb_yaml["components"].get("terraform_gke")
if terraform_gke:
cluster_name = terraform_gke["cluster_name"]
region = terraform_gke["region"]
commands.append(
f"gcloud container clusters get-credentials {cluster_name} --region {region} --project {project_id}"
)
# Set Skaffold namespace
namespace_flag = f"-n {namespace}" if namespace else ""
# Set default repo to Artifact Registry
artifact_region = "us" # TODO: Add support to other multi-regions.
default_repo = f"\"{artifact_region}-docker.pkg.dev/{project_id}/default\""
# Add skaffold command.
command_str = \
f"{skaffold_command} -p {profile} {component_flag} {namespace_flag}" \
f" --default-repo={default_repo}" \
f" {skaffold_args} {port_forwarding_flag} {no_prune_flag}"
commands.append(re.sub(" +", " ", command_str.strip()))
print("This will build and deploy all services using the command "
"and variables below:")
for command in commands:
print_success(f"- {command}")
namespace_str = namespace or "default"
print("\nnamespace:")
print_success(f"- {namespace_str}")
# print("\nenvironment variables:")
# env_vars = {
# "PROJECT_ID": project_id,
# }
# env_var_str = ""
# for key, value in env_vars.items():
# print_success(f"- {key}={value}")
# env_var_str += f"{key}={value} "
print("\nglobal_variables in sb.yaml:")
for key, value in sb_yaml.get("global_variables", {}).items():
print_success(f"- {key}: {value}")
print()
confirm("This may take a few minutes. Continue?", skip=yes)
set_gcloud_project(project_id)
exec_shell("gcloud auth configure-docker us-docker.pkg.dev",
working_dir=solution_path)
for command in commands:
exec_shell(command, working_dir=solution_path)
@ app.command()
def delete(profile: str = DEFAULT_DEPLOY_PROFILE,
component: Annotated[str, typer.Option(
"--component", "-c", "-m")] = None,
namespace: Annotated[str, typer.Option("--namespace", "-n")] = None,
solution_path: Annotated[Optional[str],
typer.Argument()] = ".",
yes: Optional[bool] = False):
"""
Delete deployment.
"""
validate_solution_folder(solution_path)
sb_yaml = read_yaml(f"{solution_path}/sb.yaml")
global_variables = sb_yaml.get("global_variables", {})
# Get global vars from sb.yaml.
project_id = global_variables.get("project_id", None)
assert project_id, "project_id is not set in 'global_variables' in sb.yaml."
component_flag = f" -m {component} " if component else ""
# Set Skaffold namespace
namespace_flag = f"-n {namespace}" if namespace else ""
# Set default repo to Artifact Registry
artifact_region = "us" # TODO: Add support to other multi-regions.
default_repo = f"\"{artifact_region}-docker.pkg.dev/{project_id}\""
command = f"skaffold delete -p {profile} {component_flag} {namespace_flag}" \
f" --default-repo={default_repo}"
print("This will DELETE deployed services using the command below:")
print_highlight(command)
confirm("\nThis may take a few minutes. Continue?", default=False, skip=yes)
exec_shell(command, working_dir=solution_path)
@ app.command()
def init(solution_path: Annotated[Optional[str], typer.Argument()] = "."):
"""
Initialize sb.yaml for a solution folder.
"""
components = None
if os.path.isfile(solution_path + "/sb.yaml"):
confirm(f"This will override the existing 'sb.yaml' in '{solution_path}'. "
"Continue?", default=False)
sb_yaml = read_yaml(f"{solution_path}/sb.yaml")
components = sb_yaml.get("components", {})
else:
confirm(f"This will create a new 'sb.yaml' in '{solution_path}'. "
"Continue?", default=True)
template_path = get_package_dir() + "/helper_modules/init_sb_yaml"
run_copy(template_path, solution_path, data={}, unsafe=True)
# Restore components.
if components:
sb_yaml = read_yaml(f"{solution_path}/sb.yaml")
sb_yaml["components"] = components
write_yaml(f"{solution_path}/sb.yaml", sb_yaml)
print_success("Complete.")
@ app.command("set-var")
def set_variable(
var_name,
var_value,
solution_path: Annotated[Optional[str], typer.Argument()] = "."
):
"Set a specific variable."
set_var(var_name, var_value, solution_path)
update_global_var(var_name, var_value, solution_path)
@ app.command("set-project")
def set_project(
project_id,
solution_path: Annotated[Optional[str], typer.Argument()] = ".",):
"Set 'project_id' and 'project_number' variables."
confirm(
f"This will set project_id to '{project_id}' and update project_number. "
+ "Continue?")
set_var("project_id", project_id, solution_path)
update_global_var("project_id", project_id, solution_path)
project_number = get_project_number(project_id)
set_var("project_number", project_number, solution_path)
update_global_var("project_number", project_number, solution_path)
@ app.command()
def info(solution_path: Annotated[Optional[str], typer.Argument()] = "."):
"""
Print info from ./sb.yaml.
"""
sb_yaml = read_yaml(f"{solution_path}/sb.yaml")
print(f"Printing info of the solution folder at '{solution_path}/'\n")
# Global variables
print("global_variables in sb.yaml:")
for key, value in sb_yaml.get("global_variables", {}).items():
print(f"- {key}: {value}")
print()
# List of installed components.
list_components()
@ app.command()
def version():
"""
Print version.
"""
print(f"Solutions Builder v{__version__}")
raise typer.Exit()
def main():
try:
print_highlight("Solutions Builder (version " +
typer.style(__version__, fg=typer.colors.CYAN, bold=True) +
")\n")
app()
print()
except Exception as e:
if DEBUG:
traceback.print_exc()
print_error(f"Error: {e}")
else:
print_error(f"Error: {e}")
print("\nTip: try adding 'DEBUG=true' to your environment variables to get more details.")
print("E.g. DEBUG=true sb new your-project\n")
return -1
return 0
if __name__ == "__main__":
main()