|
| 1 | +# |
| 2 | +# Copyright (c) 2021 Airbyte, Inc., all rights reserved. |
| 3 | +# |
| 4 | + |
| 5 | +from glob import glob |
| 6 | +from typing import List, Tuple |
| 7 | + |
| 8 | +import airbyte_api_client |
| 9 | +import click |
| 10 | +from octavia_cli.check_context import REQUIRED_PROJECT_DIRECTORIES, requires_init |
| 11 | + |
| 12 | +from .resources import BaseResource |
| 13 | +from .resources import factory as resource_factory |
| 14 | + |
| 15 | + |
| 16 | +@click.command(name="apply", help="Create an Airbyte resources from a YAML definition") |
| 17 | +@click.option("--file", "-f", "configurations_files", type=click.Path(), multiple=True) |
| 18 | +@click.option("--force", is_flag=True, default=False, help="Does not display the diff and updates without user prompt.") |
| 19 | +@click.pass_context |
| 20 | +@requires_init |
| 21 | +def apply(ctx: click.Context, configurations_files: List[click.Path], force: bool): |
| 22 | + if not configurations_files: |
| 23 | + configurations_files = find_local_configuration_files() |
| 24 | + |
| 25 | + resources = get_resources_to_apply(configurations_files, ctx.obj["API_CLIENT"], ctx.obj["WORKSPACE_ID"]) |
| 26 | + for resource in resources: |
| 27 | + apply_single_resource(resource, force) |
| 28 | + |
| 29 | + |
| 30 | +def get_resources_to_apply( |
| 31 | + configuration_files: List[str], api_client: airbyte_api_client.ApiClient, workspace_id: str |
| 32 | +) -> List[BaseResource]: |
| 33 | + """Create resource objects with factory and sort according to apply priority. |
| 34 | +
|
| 35 | + Args: |
| 36 | + configuration_files (List[str]): List of YAML configuration files. |
| 37 | + api_client (airbyte_api_client.ApiClient): the Airbyte API client. |
| 38 | + workspace_id (str): current Airbyte workspace id. |
| 39 | +
|
| 40 | + Returns: |
| 41 | + List[BaseResource]: Resources sorted according to their apply priority. |
| 42 | + """ |
| 43 | + all_resources = [resource_factory(api_client, workspace_id, path) for path in configuration_files] |
| 44 | + return sorted(all_resources, key=lambda resource: resource.apply_priority) |
| 45 | + |
| 46 | + |
| 47 | +def apply_single_resource(resource: BaseResource, force: bool) -> None: |
| 48 | + """Runs resource creation if it was not created, update it otherwise. |
| 49 | +
|
| 50 | + Args: |
| 51 | + resource (BaseResource): The resource to apply. |
| 52 | + force (bool): Wetheir force mode is on. |
| 53 | + """ |
| 54 | + if resource.was_created: |
| 55 | + click.echo( |
| 56 | + click.style(f"🐙 - {resource.resource_name} exists on your Airbyte instance, let's check if we need to update it!", fg="yellow") |
| 57 | + ) |
| 58 | + messages = update_resource(resource, force) |
| 59 | + else: |
| 60 | + click.echo(click.style(f"🐙 - {resource.resource_name} does not exists on your Airbyte instance, let's create it!", fg="green")) |
| 61 | + messages = create_resource(resource) |
| 62 | + click.echo("\n".join(messages)) |
| 63 | + |
| 64 | + |
| 65 | +def should_update_resource(force: bool, diff: str, local_file_changed: bool) -> Tuple[bool, str]: |
| 66 | + """Function to decide if the resource needs an update or not. |
| 67 | +
|
| 68 | + Args: |
| 69 | + force (bool): Wetheir force mode is on. |
| 70 | + diff (str): The computed diff between local and remote for this resource. |
| 71 | + local_file_changed (bool): Wetheir the local file describing the resource was modified. |
| 72 | +
|
| 73 | + Returns: |
| 74 | + Tuple[bool, str]: Boolean to know if resource should be updated and string describing the update reason. |
| 75 | + """ |
| 76 | + if force: |
| 77 | + should_update, update_reason = True, "🚨 - Running update because force mode is on." |
| 78 | + elif diff: |
| 79 | + should_update, update_reason = True, "✍️ - Running update because a diff was detected between local and remote resource." |
| 80 | + elif local_file_changed: |
| 81 | + should_update, update_reason = ( |
| 82 | + True, |
| 83 | + "✍️ - Running update because a local file change was detected and a secret field might have been edited.", |
| 84 | + ) |
| 85 | + else: |
| 86 | + should_update, update_reason = False, "😴 - Did not update because no change detected." |
| 87 | + return should_update, click.style(update_reason, fg="green") |
| 88 | + |
| 89 | + |
| 90 | +def display_diff_line(diff_line: str) -> None: |
| 91 | + """Prettify a diff line and print it to standard output. |
| 92 | +
|
| 93 | + Args: |
| 94 | + diff_line (str): The diff line to display. |
| 95 | + """ |
| 96 | + if "changed from" in diff_line: |
| 97 | + color = "yellow" |
| 98 | + prefix = "E" |
| 99 | + elif "added" in diff_line: |
| 100 | + color = "green" |
| 101 | + prefix = "+" |
| 102 | + elif "removed" in diff_line: |
| 103 | + color = "red" |
| 104 | + prefix = "-" |
| 105 | + else: |
| 106 | + prefix = "" |
| 107 | + color = None |
| 108 | + click.echo(click.style(f"\t{prefix} - {diff_line}", fg=color)) |
| 109 | + |
| 110 | + |
| 111 | +def prompt_for_diff_validation(resource_name: str, diff: str) -> bool: |
| 112 | + """Display the diff to user and prompt them from validation. |
| 113 | +
|
| 114 | + Args: |
| 115 | + resource_name (str): Name of the resource the diff was computed for. |
| 116 | + diff (str): The diff. |
| 117 | +
|
| 118 | + Returns: |
| 119 | + bool: Wetheir user validated the diff. |
| 120 | + """ |
| 121 | + if diff: |
| 122 | + click.echo( |
| 123 | + click.style("👀 - Here's the computed diff (🚨 remind that diff on secret fields are not displayed):", fg="magenta", bold=True) |
| 124 | + ) |
| 125 | + for line in diff.split("\n"): |
| 126 | + display_diff_line(line) |
| 127 | + return click.confirm(click.style(f"❓ - Do you want to update {resource_name}?", bold=True)) |
| 128 | + else: |
| 129 | + return False |
| 130 | + |
| 131 | + |
| 132 | +def create_resource(resource: BaseResource) -> List[str]: |
| 133 | + """Run a resource creation. |
| 134 | +
|
| 135 | + Args: |
| 136 | + resource (BaseResource): The resource to create. |
| 137 | +
|
| 138 | + Returns: |
| 139 | + List[str]: Post create messages to display to standard output. |
| 140 | + """ |
| 141 | + created_resource, state = resource.create() |
| 142 | + return [ |
| 143 | + click.style(f"🎉 - Successfully created {created_resource.name} on your Airbyte instance!", fg="green", bold=True), |
| 144 | + click.style(f"💾 - New state for {created_resource.name} saved at {state.path}", fg="yellow"), |
| 145 | + ] |
| 146 | + |
| 147 | + |
| 148 | +def update_resource(resource: BaseResource, force: bool) -> List[str]: |
| 149 | + """Run a resource update. Check if update is required and prompt for user diff validation if needed. |
| 150 | +
|
| 151 | + Args: |
| 152 | + resource (BaseResource): Resource to update |
| 153 | + force (bool): Wetheir force mode is on. |
| 154 | +
|
| 155 | + Returns: |
| 156 | + List[str]: Post update messages to display to standard output. |
| 157 | + """ |
| 158 | + diff = resource.get_diff_with_remote_resource() |
| 159 | + should_update, update_reason = should_update_resource(force, diff, resource.local_file_changed) |
| 160 | + output_messages = [update_reason] |
| 161 | + if not force and diff: |
| 162 | + should_update = prompt_for_diff_validation(resource.resource_name, diff) |
| 163 | + if should_update: |
| 164 | + updated_resource, state = resource.update() |
| 165 | + output_messages.append( |
| 166 | + click.style(f"🎉 - Successfully updated {updated_resource.name} on your Airbyte instance!", fg="green", bold=True) |
| 167 | + ) |
| 168 | + output_messages.append(click.style(f"💾 - New state for {updated_resource.name} stored at {state.path}.", fg="yellow")) |
| 169 | + return output_messages |
| 170 | + |
| 171 | + |
| 172 | +def find_local_configuration_files() -> List[str]: |
| 173 | + """Discover local configuration files. |
| 174 | +
|
| 175 | + Returns: |
| 176 | + List[str]: Paths to YAML configuration files. |
| 177 | + """ |
| 178 | + configuration_files = [] |
| 179 | + for resource_directory in REQUIRED_PROJECT_DIRECTORIES: |
| 180 | + configuration_files += glob(f"./{resource_directory}/**/configuration.yaml") |
| 181 | + if not configuration_files: |
| 182 | + click.echo(click.style("😒 - No YAML file found to run apply.", fg="red")) |
| 183 | + return configuration_files |
0 commit comments