diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b79a6d4..f115b75 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,24 +35,6 @@ jobs: name: buildfiles path: . - pubishtest: - if: "!startsWith(github.ref, 'refs/tags/v')" - name: 📦 publish to TestPyPI - runs-on: ubuntu-latest - needs: [ build ] - - steps: - - uses: actions/download-artifact@master - with: - name: buildfiles - path: . - - - name: Publish distribution 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ - publish: if: "startsWith(github.ref, 'refs/tags/v')" name: 📦 publish to PyPI diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae0aa3..e48366a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ # Changelog This file documents any relevant changes. -## [2.0.0-dev2] 2024-01-09 + +## [2.0.0.dev3] 2024-02-19 +- feature: appengine Setup can now be done via CLI +- feature: Appengine Roles can now be managed via CLI +- feature: `viur package install` and `viur package update` functions + +## [2.0.0.dev2] 2024-01-09 - refactor: enabled profile support for all viur-cli functions - fix: `viur env` now runs without errors - refactor: project.json functionalities -- feat: `viur package install` and `viur package update` functions +- feature: cloudfunctions creation and deployment via ClI +- feature: backup disabling/ enabling via CLI +- refactor: `viur deploy` is now `viur cloud deploy` ## [1.1.1] 2023-11-28 - chore: updated dependencies diff --git a/README.md b/README.md index 672f831..a1b9202 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,6 @@ $ viur check {npm|--dev|--autofix} ``` Runs a security check for the python environment and for each npm project registered under builds. -```sh -$ viur deploy {app|index} [target] -``` -you can deploy the app or the index.yaml to a google cloud project target of your choice, though the target is optional. -By default this would be the projectID you gave when initializing the project, but you can add targets to the project.json -if you would like to have an additional system for testing for example. - ```sh $ viur enable {backup} ``` @@ -89,6 +82,38 @@ $ viur build app [appname] ``` build a specific app +```sh +$ viur cloud deploy {app|index|cloudfunction} {profile} {--ext|--yes|--name} +``` +This Function deploys the Google Cloud application and / or different .yaml files +Scripts: +- `app` Deploy application to the Google Appengine +- `index` Deploy index.yaml to Google Appenginge +- `cloudfunction` Deploy Cloudfunction to Google Appengine +Commands: +- `profile` The project.json profile you want to Work from + + +```sh +$ viur cloud {enable|disable} backup +``` +Enable/ Disable the Backup buckets you need to Backup a cloud project in the Google Cloud Console + +```sh +$ viur cloud setup {gcloud|gcroles} +``` +Scripts: +- `gcloud` This Function setups your project to work on the gcloud plattform +- `gcroles` This function lets you set up Roles for your google appengine Workspace + + +```sh +$ viur cloud get {gcroles} +``` +Scripts: +- `gcroles` This function lets you get Roles for your google appengine Workspace in a readable .json Format + + ```sh $ viur env ``` @@ -219,7 +244,7 @@ viur-cli depends on ## License -Copyright © 2023 by Mausbrand Informationssysteme GmbH.
+Copyright © 2024 by Mausbrand Informationssysteme GmbH.
Mausbrand and ViUR are registered trademarks of Mausbrand Informationssysteme GmbH. This project is free software under the MIT license.
diff --git a/setup.cfg b/setup.cfg index c4ec22c..411ee14 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,6 @@ install_requires = semver==3.0.2 - [options.packages.find] where = src diff --git a/src/viur_cli/__init__.py b/src/viur_cli/__init__.py index c098306..91438f6 100644 --- a/src/viur_cli/__init__.py +++ b/src/viur_cli/__init__.py @@ -1,5 +1,6 @@ from .cli import * from .conf import * +from .install import * from .deploy import * from .package import * from .local import * @@ -9,4 +10,4 @@ from .scriptor import script from .tool import * from .update import * -from .package import * +from .cloud import * diff --git a/src/viur_cli/cli.py b/src/viur_cli/cli.py index 3f12aba..c340f37 100644 --- a/src/viur_cli/cli.py +++ b/src/viur_cli/cli.py @@ -1,10 +1,11 @@ import re import subprocess +import click from .conf import * from .version import __version__ from .version import MINIMAL_PIPENV import semver - +import pprint @click.group(invoke_without_command=True, no_args_is_help=True, @@ -59,7 +60,9 @@ def cli(ctx): @click.argument("action", type=click.Choice(['list'])) @click.argument("profile", default="default") def project(action, profile): + """List your project.json Configuration File""" project_config = config.get_profile(profile) if action == "list": echo_info(f"These are the Settings for {profile} profile") - pprint(project_config) + pprint.pprint(project_config) + diff --git a/src/viur_cli/cloud.py b/src/viur_cli/cloud.py new file mode 100644 index 0000000..3b71255 --- /dev/null +++ b/src/viur_cli/cloud.py @@ -0,0 +1,573 @@ +import json +import subprocess +import os +import string +import click +import yaml +from viur_cli import echo_positive, echo_warning, echo_fatal +from .conf import config +from . import cli, echo_error, echo_info, replace_vars +from .update import create_req + + +@cli.group() +def cloud(): + """Manage cloud Actions""" + + +@cloud.command(context_settings={"ignore_unknown_options": True}) +@click.argument("action", type=click.Choice(["backup"])) +def enable(action): + """ + Enable specific features for the project. + """ + if action == "backup": + enable_gcp_backup() + + +def enable_gcp_backup(): + # Load the project Config + conf = config.get_profile("default") + + # Create helper Variables + project_id = conf["application_name"] + bucket_name = f'backup-dot-{project_id}' + backup_bucket_command = f'gsutil mb -l EUROPE-WEST3 -p {project_id} gs://{bucket_name}' + + # Create the Backup Bucket + try: + result = subprocess.run( + backup_bucket_command, + capture_output=True, + shell=True, + ) + if result.returncode != 0: + echo_error('Error creating bucket.') + + except Exception as e: + print(f'An Error Occured:\n {e} Please make sure you have the correct Google Cloud Access rights') + + # Helper Variables for IAM + iam_roles = ["roles/storage.admin", "roles/datastore.importExportAdmin"] + service_worker_mail = f'{project_id}@appspot.gserviceaccount.com' + + for r in iam_roles: + iam_roles_command = (f'gcloud projects add-iam-policy-binding {project_id} --member ' + f'serviceAccount:{service_worker_mail} --role {r}') + try: + subprocess.run(iam_roles_command, capture_output=True, shell=True) + + except Exception as e: + print(f'An Error Occured during Roles {e}\n ' + f'Please make sure you have the correct Google Cloud Access rights' + ) + return + + print('Success! It may take a while until you can use Gcloud Backups') + + +@cloud.command(context_settings={"ignore_unknown_options": True}) +@click.argument("action", type=click.Choice(["backup"])) +def disable(action): + """ + Disable specific features for the project. + """ + if action == "backup": + disable_gcp_backup() + + +def disable_gcp_backup(): + # Load the project Config + conf = config.get_profile("default") + + # Create helper Variables + project_id = conf["application_name"] + bucket_name = f'backup-dot-{project_id}' + backup_bucket_command = f'gsutil rm -r gs://{bucket_name}' + + # Remove the Backup Bucket + try: + result = subprocess.run( + backup_bucket_command, + capture_output=True, + shell=True, + ) + print(result) + if result.returncode != 0: + print('Error removing bucket.') + + except Exception as e: + print(f'An Error Occured:\n {e} Please make sure you have the correct Google Cloud Access rights') + + # Helper Variables for IAM + iam_roles = ["roles/storage.admin", "roles/datastore.importExportAdmin"] + service_worker_mail = f'{project_id}@appspot.gserviceaccount.com' + + for r in iam_roles: + iam_roles_command = (f'gcloud projects remove-iam-policy-binding {project_id} --member ' + f'serviceAccount:{service_worker_mail} --role {r}') + try: + subprocess.run(iam_roles_command, capture_output=True, shell=True) + + except Exception as e: + print(f'An Error Occured during Roles {e}\n ' + f'Please make sure you have the correct Google Cloud Access rights' + ) + return + + print('Success! Gcloud Backups have been disabled') + + +@cloud.command(context_settings={"ignore_unknown_options": True}) +@click.argument("action", type=click.Choice(["gcloud", "gcroles"])) +@click.argument("profile", default="default") +def setup(action, profile): + """Setup project for different Cloud Services""" + if action == "gcloud": + gcloud_setup() + + if action == "gcroles": + gcloud_setup_roles(profile) + + +@cloud.command(context_settings={"ignore_unknown_options": True}) +@click.argument("action", type=click.Choice(["gcroles"])) +@click.argument("profile", default='default') +def get(action, profile): + """Setup project for different Cloud Services""" + + if action == "gcroles": + gcloud_get_roles(profile) + + +def gcloud_get_roles(profile): + conf = config.get_profile(profile) + + try: + # Run gcloud command to get IAM policy and save it to a YAML file + yaml_file_path = f"./{conf['application_name']}.yaml" + run_command(f"gcloud projects get-iam-policy {conf['application_name']} > {yaml_file_path}") + + try: + # Load YAML data and transform it to a dictionary + with open(yaml_file_path, "r") as file: + roles_dict = yaml.safe_load(file) + usable_dict = transform_yaml_to_dict(roles_dict) + + # Save the transformed dictionary to a JSON file + json_file_path = f"./{profile}_roles.json" + with open(json_file_path, 'w') as json_file: + json.dump(usable_dict, json_file, indent=4) + + echo_positive(f"You can now watch your gcloud Roles Setup in your '{json_file_path}' file ") + + except yaml.YAMLError as e: + echo_error(f"An error occurred during YAML to Dictionary transformation: {e}") + + finally: + # Delete the temporary YAML file + if os.path.exists(yaml_file_path): + os.remove(yaml_file_path) + + except subprocess.CalledProcessError: + echo_error("An error occurred while fetching Role data") + + +def gcloud_setup_roles(profile): + conf = config.get_profile(profile) + + # Use a more descriptive variable name for the JSON file + roles_json_file_path = f"{profile}_roles.json" + + try: + with open(roles_json_file_path, "r") as json_file: + # Load roles from the JSON file and transform to .yaml + roles = json.load(json_file) + yaml_data = transform_dict_to_yaml(roles) + yaml_file_path = f"./{conf['application_name']}.yaml" + + # Save YAML data to a file + with open(yaml_file_path, 'w') as yaml_file: + yaml.dump(yaml_data, yaml_file, default_flow_style=False) + + try: + # Run gcloud command to get IAM policy + run_command(f"gcloud projects get-iam-policy {conf['application_name']} {yaml_file_path}") + except ValueError as e: + echo_error(f"An error occurred while uploading Role data to Cloud: {e}") + + except json.JSONDecodeError as e: + echo_fatal(f"Error decoding JSON from {roles_json_file_path}: {e}") + except yaml.YAMLError as e: + echo_fatal(f"Error opening or writing the .yaml file: {e}") + + +def transform_yaml_to_dict(dict_data): + transformed_data = {'bindings': []} + + # Create a dictionary to store unique members and their corresponding roles + member_roles = {} + + # Iterate through the original data and organize it + for binding in dict_data['bindings']: + role = binding['role'] + for member in binding['members']: + member_roles.setdefault(member, []).append(role) + + # Create the transformed data structure + transformed_data['bindings'] = [{'members': member, 'role': roles} for member, roles in member_roles.items()] + + # Add E-Tag and Version + transformed_data.update({"etag": dict_data["etag"], "version": dict_data["version"]}) + + return transformed_data + + +def transform_dict_to_yaml(transformed_data): + original_data = {'bindings': []} + + # Create a dictionary to store roles and their corresponding members + role_members = {} + + # Iterate through the transformed data and organize it + for binding in transformed_data['bindings']: + members = binding['members'] + roles = binding['role'] + + # Ensure members is a list + members = [members] if not isinstance(members, list) else members + + for role in roles: + role_members.setdefault(role, []).extend(members) + + # Create the original data structure + original_data['bindings'] = [ + {'members': list(set(members)), 'role': role} for role, members in role_members.items() + ] + + # Add E-Tag and Version + original_data.update({"etag": transformed_data["etag"], "version": transformed_data["version"]}) + + return original_data + + +def gcloud_setup(): + project = input("Enter PROJECT_ID: ").strip() + + if not project: + echo_fatal("Usage: viur setup gcloud PROJECT_ID") + return + + echo_info("Check if user is authorized with gcloud....") + + try: + run_command("gcloud auth print-access-token") + except subprocess.CalledProcessError: + echo_warning( + "##############################################################\n" + "# Please authenticate your Google user with gcloud SDK to #\n" + "# execute administrative commands. #\n" + "# In this step, a separate browser window opens to #\n" + "# authenticate. #\n" + "# This step is only required once on this computer. #\n" + "##############################################################\n" + ) + response = input("Are you ready?[Y/n]") + if not response.lower() in ("y", ""): + echo_fatal("User aborted") + return + + run_command("gcloud auth login --no-ptomote") + + # Check if App already exists + try: + run_command(f"gcloud app describe --project={project}") + except subprocess.CalledProcessError: + echo_warning( + "##############################################################\n" + "# Please check and confirm that your project is created and #\n" + "# connected with a billing account in Google Cloud console. #\n" + "# Otherwise, some of the following calls may fail. #\n" + "##############################################################" + ) + response = input("Continue? [Y/n] ") + if not response.lower() in ("y", ""): + print("User aborted.") + return + + # Create the Appengine app + run_command(f"gcloud app create --project={project} --region=europe-west3") + + # Activate APIs and Services + services = [ + "datastore.googleapis.com", + "firestore.googleapis.com", + "iamcredentials.googleapis.com", + "cloudbuild.googleapis.com", + "cloudtasks.googleapis.com", + "cloudscheduler.googleapis.com", + "secretmanager.googleapis.com" + ] + + for service in services: + run_command(f"gcloud services enable --project={project} {service}") + + # Configure Google Cloud Storage + run_command(f"gsutil uniformbucketlevelaccess set on gs://{project}.appspot.com/") + + for yaml in ["cron.yaml", "queue.yaml", "index.yaml"]: + run_command(f"cd deploy && gcloud app deploy -q --project={project} {yaml}") + + echo_info("Check if app engine default credentials are set...") + try: + run_command("gcloud auth application-default print-access-token") + + except subprocess.CalledProcessError: + echo_warning( + "##############################################################\n" + "# Please authenticate your Google user with gcloud SDK now #\n" + "# to set the application default user. This step is required #\n" + "# to run ViUR applications locally without further #\n" + "# credentials that must be supplied from a file. #\n" + "# This step is only required once on this computer. #\n" + "##############################################################") + response = input("Are you ready? [Y/n] ") + if not response.lower() in ("y", ""): + echo_fatal("User aborted.") + return + + run_command("gcloud auth application-default login") + + echo_positive( + "All done!\n" + "You should now be able to run your project locally with\n" + " viur run \n" + "At the first run, it might happen that some functions are\n" + "causing error 500 because indexes are not immediately\n" + "served. Therefore, maybe wait a few minutes.\n" + "Have a nice day.\n") + + +# Helper function for running Commands in subprocess and getting the Output +def run_command(command): + try: + return subprocess.check_output(command, shell=True) + except subprocess.CalledProcessError as e: + print(f"Error executing command: {e}") + + +@cloud.command() +@click.argument("action", type=click.Choice(['app', 'index', 'cron', 'queue', 'cloudfunction'])) +@click.argument("profile", default='develop') +@click.argument("additional_args", nargs=-1) +@click.option("--ext", "-e", default=None) +@click.option("--yes", "-y", is_flag=True, default=False) +@click.option("--name", "-n", default=None) +def deploy(action, profile, name, ext, yes, additional_args): + + """ + Deploy a Google Cloud application or different YAML files. + + This command allows you to deploy various components of a Google Cloud application, such as the app itself, + index.yaml configurations, cron.yaml configurations, or queue.yaml configurations. + The deployment action and the specific project configuration + to deploy are determined by the 'action' and 'name' parameters. + Please make sure to configure your global installation of the gcloud-cli accordingly. + + :param action: str + The deployment action. It can be one of the following: + - 'app': Deploy the Google Cloud application. + - 'index': Deploy the index.yaml configuration. + - 'cron': Deploy the cron.yaml configuration. + - 'queue': Deploy the queue.yaml configuration. + + :param name: str, default: 'develop' + The name of the project configuration to use for deployment. + It should correspond to a valid project configuration. + + :param additional_args: tuple + Additional arguments that can be passed to the deployment process. + + Example Usage: + ```shell + viur deploy app my_config --version v2 + viur deploy index my_config + viur deploy cron my_config + viur deploy queue my_config + ``` + + The `deploy` command deploys the specified components based on the 'action' and 'name' parameters. + It includes checks for successful deployments and offers a confirmation prompt for any failed checks. + + Note: + - Ensure that the specified project configuration ('name') is valid and defined in your project's configuration. + - The 'app' action includes vulnerability checks and a confirmation prompt if checks fail. + - The 'index' action sorts the index.yaml file by kind for cleaner organization. + + :return: None + """ + + conf = config.get_profile(profile) + + if action == "app": + from . import do_checks + if not do_checks(dev=False): + # --yes will not be implemented here because deploying security issues should be an explicit decission + if not click.confirm(f"The checks were not successful, do you want to continue?"): + return + else: + echo_info("\U00002714 No vulnerabilities found.") + version = replace_vars( + conf["version"], + {k: v for k, v in conf.items() if k not in ["version"]} + ) + + # gcloud only allows for version identifiers in lower-case order and only accepting these characters + version = "".join([c for c in version.lower() if c in string.ascii_lowercase + string.digits + "-"]) + + if ext: + version += f"-{ext}" + + # rebuild requirements.txt + create_req(yes, profile, confirm_value=False) + + os.system( + f'gcloud app deploy --project={conf["application_name"]} --version={version} ' + f'--no-promote {" ".join(additional_args)} {conf["distribution_folder"]} {"-q" if yes else ""}') + + if action == "cloudfunction": + os.system(build_deploy_command(name, conf)) + + else: + if action not in ["index", "queue", "cron"]: + echo_error(f"{action} is not a valid action. Valid is app, index, queue, cron") + + yaml_file = f'{conf["distribution_folder"]}/{action}.yaml' + + # Sort index.yaml by kind name, making it more clean to view. + if action == "index": + try: + with open(yaml_file, "r") as source_file: + data = yaml.safe_load(source_file) + + if "indexes" not in data: + raise ValueError("indexes section missing in index.yaml") + + indexes = sorted( + data["indexes"], + key=lambda k: k["kind"] if isinstance(k, dict) and "kind" in k else k + ) + + # Remove duplicate entries with the help of dict, + # where keys can only occur once. + # The keys are a hashable representation of an entry. + indexes = { + ( + entry.get("kind"), + tuple(tuple(prop.items()) + for prop in entry.get("properties", [])) + ): entry + for entry in indexes + } + indexes = list(indexes.values()) + + # Only update index.yaml when something has changed + if data["indexes"] != indexes: + data["indexes"] = indexes + + with open(yaml_file, "a+") as dst_file: + dst_file.seek(0) + dst_file.truncate() + dst_file.write( + yaml.dump(data).replace("- kind: ", "\n- kind: ") + ) + + echo_info(f"{yaml_file} has been sorted by kind and duplicates have been removed") + + except FileNotFoundError: + echo_error(f"{yaml_file} not found") + return + except ValueError: + echo_error(f"{yaml_file} is not a valid") + return + + os.system( + f'gcloud app deploy --project={conf["application_name"]} {" ".join(additional_args)} {yaml_file}') + + +def build_deploy_command(name, conf): + """Builds the deployment command string for the cloud function.""" + + if not name: + name = click.prompt("Please enter the name of the cloudfunction you want to deploy") + + if name not in conf["functions"]: + echo_fatal(f"The cloudfunction {name} was not found your project.json\n " + f"You can create a cloudfunction entry by calling 'viur cloud create function'") + + command = ( + f"gcloud functions deploy " + f"{name} " + f"--region='{conf['region']}'" + f"--max-instances={conf['max-instances']}" + ) + + for k, v in conf["functions"][name].items(): + if k in ["trigger", "update", "set", "remove"]: + command += f"--{k}-{v}" + else: + command += f" --{k}='{str(v)}'" + + return command + + +@cloud.command() +@click.argument("action", type=click.Choice(['function'])) +@click.argument("profile", default="default") +@click.option("--source", "-src") +@click.option("--name", "-n", default=None) +@click.option("--entrypoint", "-ep") +@click.option("--env-vars-file", "-ev") +@click.option("--memory", "-mem") +@click.option("--runtime", "-rt") +@click.option("--trigger", "-tr") +def create(profile, action, source, name, entrypoint, env_vars_file, memory, runtime, trigger): + """Create cloudfunction entry""" + if action == "function": + conf = config.get_profile(profile) + # First layer initialization: + conf["gcloud"] = conf.get("gcloud", {}) + conf["gcloud"]["functions"] = conf["gcloud"].get("functions", {}) + conf["gcloud"]["max-instances"] = conf["gcloud"].get("max-instances", click.prompt( + "Please input the Max Instances your cloud functions should run on")) + conf["gcloud"]["region"] = conf["gcloud"].get("region", click.prompt( + "Please enter your default cloudfunction region (europe-west3)")) + + # function layer + function_name = name if name else click.prompt("Please enter the Name of your cloudfunction") + function_dict = conf["gcloud"]["functions"].get(function_name, {}) + + function_dict["entry-point"] = function_dict.get("entry-point", entrypoint if entrypoint else click.prompt( + "Please enter your cloudfunction entrypoint (main)")) + + function_dict["env-vars-file"] = function_dict.get("env-vars-file", + env_vars_file if env_vars_file else click.prompt( + "Enter the name of your environment variables file")) + + function_dict["memory"] = function_dict.get("memory", memory if memory else click.prompt( + "Please enter your cloudfunction memory usage (512MB)")) + + function_dict["runtime"] = function_dict.get("runtime", runtime if runtime else click.prompt( + "Please enter your cloudfunction runtime (python311)")) + + function_dict["trigger"] = function_dict.get("trigger", trigger if trigger else click.prompt( + "Please enter your cloudfunction trigger type (http)")) + function_dict["source"] = function_dict.get("source", source if source else click.prompt( + "Enter the directory of your cloudfuntion")) + + conf["gcloud"]["functions"][function_name] = function_dict + + config[profile] = conf + config.migrate() + echo_positive("Your cloudfunction creation was succesfull, if you want to add more flags, " + "add them in your project.json under") diff --git a/src/viur_cli/conf.py b/src/viur_cli/conf.py index ba384da..cbfce73 100644 --- a/src/viur_cli/conf.py +++ b/src/viur_cli/conf.py @@ -80,17 +80,7 @@ def delete(self): raise click.ClickException(click.style(f"{configname} not found", fg="red")) def migrate(self): - """ - Update the project configuration. - - This method performs updates and migrations on the project configuration as needed. It includes version checks - and format updates. Ensure that 'load_config()' is called before invoking this method. - :param path: str, optional - The path to the project.json file. If not provided, the root directory of the project is used. - - :return: None - """ assert self["format"] in ["1.0.0", "1.0.1", "1.1.0", "1.1.1", "1.2.0", PROJECT_CONFIG_VERSION], \ "Invalid formatversion, you have to fix it manually" diff --git a/src/viur_cli/deploy.py b/src/viur_cli/deploy.py deleted file mode 100644 index 7e10ecf..0000000 --- a/src/viur_cli/deploy.py +++ /dev/null @@ -1,204 +0,0 @@ -import os -import string -import click -import yaml -import subprocess -from .conf import config -from . import cli, echo_error, echo_info, replace_vars -from .update import create_req - - -@cli.command(context_settings={"ignore_unknown_options": True}) -@click.argument("action", type=click.Choice(['app', 'index', 'cron', 'queue'])) -@click.argument("profile", default='develop') -@click.argument("additional_args", nargs=-1) -@click.option("--ext", "-e", default=None) -@click.option("--yes", "-y", is_flag=True, default=False) -def deploy(action, profile, ext, yes, additional_args): - """ - Deploy a Google Cloud application or different YAML files. - - This command allows you to deploy various components of a Google Cloud application, such as the app itself, - index.yaml configurations, cron.yaml configurations, or queue.yaml configurations. - The deployment action and the specific project configuration - to deploy are determined by the 'action' and 'name' parameters. - Please make sure to configure your global installation of the gcloud-cli accordingly. - - :param action: str - The deployment action. It can be one of the following: - - 'app': Deploy the Google Cloud application. - - 'index': Deploy the index.yaml configuration. - - 'cron': Deploy the cron.yaml configuration. - - 'queue': Deploy the queue.yaml configuration. - - :param name: str, default: 'develop' - The name of the project configuration to use for deployment. - It should correspond to a valid project configuration. - - :param additional_args: tuple - Additional arguments that can be passed to the deployment process. - - Example Usage: - ```shell - viur deploy app my_config --version v2 - viur deploy index my_config - viur deploy cron my_config - viur deploy queue my_config - ``` - - The `deploy` command deploys the specified components based on the 'action' and 'name' parameters. - It includes checks for successful deployments and offers a confirmation prompt for any failed checks. - - Note: - - Ensure that the specified project configuration ('name') is valid and defined in your project's configuration. - - The 'app' action includes vulnerability checks and a confirmation prompt if checks fail. - - The 'index' action sorts the index.yaml file by kind for cleaner organization. - - :return: None - """ - conf = config.get_profile(profile) - - if action == "app": - from . import do_checks - if not do_checks(dev=False): - # --yes will not be implemented here because deploying security issues should be an explicit decission - if not click.confirm(f"The checks were not successful, do you want to continue?"): - return - else: - echo_info("\U00002714 No vulnerabilities found.") - version = replace_vars( - conf["version"], - {k: v for k, v in conf.items() if k not in ["version"]} - ) - - # gcloud only allows for version identifiers in lower-case order and only accepting these characters - version = "".join([c for c in version.lower() if c in string.ascii_lowercase + string.digits + "-"]) - - if ext: - version += f"-{ext}" - - # rebuild requirements.txt - create_req(yes, profile, confirm_value=False) - - os.system( - f'gcloud app deploy --project={conf["application_name"]} --version={version} ' - f'--no-promote {" ".join(additional_args)} {conf["distribution_folder"]} {"-q" if yes else ""}') - else: - if action not in ["index", "queue", "cron"]: - echo_error(f"{action} is not a valid action. Valid is app, index, queue, cron") - - yaml_file = f'{conf["distribution_folder"]}/{action}.yaml' - - # Sort index.yaml by kind name, making it more clean to view. - if action == "index": - try: - with open(yaml_file, "r") as source_file: - data = yaml.safe_load(source_file) - - if "indexes" not in data: - raise ValueError("indexes section missing in index.yaml") - - indexes = sorted( - data["indexes"], - key=lambda k: k["kind"] if isinstance(k, dict) and "kind" in k else k - ) - - # Remove duplicate entries with the help of dict, - # where keys can only occur once. - # The keys are a hashable representation of an entry. - indexes = { - ( - entry.get("kind"), - tuple(tuple(prop.items()) - for prop in entry.get("properties", [])) - ): entry - for entry in indexes - } - indexes = list(indexes.values()) - - # Only update index.yaml when something has changed - if data["indexes"] != indexes: - data["indexes"] = indexes - - with open(yaml_file, "a+") as dst_file: - dst_file.seek(0) - dst_file.truncate() - dst_file.write( - yaml.dump(data).replace("- kind: ", "\n- kind: ") - ) - - echo_info(f"{yaml_file} has been sorted by kind and duplicates have been removed") - - except FileNotFoundError: - echo_error(f"{yaml_file} not found") - return - except ValueError: - echo_error(f"{yaml_file} is not a valid") - return - - os.system( - f'gcloud app deploy --project={conf["application_name"]} {" ".join(additional_args)} {yaml_file}') - - -@cli.command(context_settings={"ignore_unknown_options": True}) -@click.argument("action", type=click.Choice(["backup"])) -def enable(action): - """ - Enable specific features for the project. - - The 'enable' command allows enabling various features for the project. Currently, only the 'backup' action is - supported. - - Args: - action (str): The action to perform. Currently, only 'backup' is supported. - - Example: - ```bash - $ viur enable backup - ``` - - """ - if action == "backup": - enable_gcp_backup() - - -def enable_gcp_backup(): - # Load the project Config - conf = config.get_profile("default") - - # Create helper Variables - project_id = conf["application_name"] - bucket_name = f'backup-dot-{project_id}' - backup_bucket_command = f'gsutil mb -l EUROPE-WEST3 -p {project_id} gs://{bucket_name}' - - # Create the Backup Bucket - try: - result = subprocess.run( - backup_bucket_command, - capture_output=True, - shell=True, - ) - print(result) - if result.returncode != 0: - print('Error creating bucket.') - - except Exception as e: - print(f'An Error Occured:\n {e} Please make sure you have the correct Google Cloud Access rights') - - # Helper Variables for IAM - iam_roles = ["roles/storage.admin", "roles/datastore.importExportAdmin"] - service_worker_mail = f'{project_id}@appspot.gserviceaccount.com' - - for r in iam_roles: - iam_roles_command = (f'gcloud projects add-iam-policy-binding {project_id} --member ' - f'serviceAccount:{service_worker_mail} --role {r}') - try: - subprocess.run(iam_roles_command, capture_output=True, shell=True) - - except Exception as e: - print(f'An Error Occured during Roles {e}\n ' - f'Please make sure you have the correct Google Cloud Access rights' - ) - return - - print('Success! It may take a while until you can use Gcloud Backups') diff --git a/src/viur_cli/version.py b/src/viur_cli/version.py index 3a570fe..b899994 100644 --- a/src/viur_cli/version.py +++ b/src/viur_cli/version.py @@ -1,2 +1,2 @@ -__version__ = "2.0.0-dev2" +__version__ = "2.0.0.dev3" MINIMAL_PIPENV = "2023.11.15"