From 70f8950375e669d297143c7f696d886e4fb6cbe3 Mon Sep 17 00:00:00 2001 From: Pallab Pain Date: Sat, 6 May 2023 23:40:43 +0530 Subject: [PATCH] feat(apply): adds option show dependency graph This commit exposes the hidden feature of rio apply where you can visualise the dependency between various rapyuta.io resources in your manifests. It uses `mermaid.live` to show the visualization. --- riocli/apply/__init__.py | 41 ++++++++++++++++++++++++++++------- riocli/apply/parse.py | 33 +++++++++++++++++++--------- riocli/utils/mermaid.py | 46 +++++++++++++++++----------------------- 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/riocli/apply/__init__.py b/riocli/apply/__init__.py index 7cfdd4c5..92a87c63 100644 --- a/riocli/apply/__init__.py +++ b/riocli/apply/__init__.py @@ -29,18 +29,34 @@ help_headers_color='yellow', help_options_color='green', ) -@click.option('--dryrun', '-d', is_flag=True, default=False, help='dry run the yaml files without applying any change') +@click.option('--dryrun', '-d', is_flag=True, default=False, + help='dry run the yaml files without applying any change') +@click.option('--show-graph', '-g', is_flag=True, default=False, + help='Opens a mermaid.live dependency graph') @click.option('--values', '-v', - help="path to values yaml file. key/values specified in the values file can be used as variables in template yamls") + help="path to values yaml file. key/values " + "specified in the values file can be " + "used as variables in template YAMLs") @click.option('--secrets', '-s', - help="secret files are sops encoded value files. rio-cli expects sops to be authorized for decoding files on this computer") -@click.option('--workers', '-w', help="number of parallel workers while running apply command. defaults to 6.", - type=int) -@click.option('-f', '--force', '--silent', 'silent', is_flag=True, type=click.BOOL, default=False, + help="secret files are sops encoded value files. " + "rio-cli expects sops to be authorized for " + "decoding files on this computer") +@click.option('--workers', '-w', + help="number of parallel workers while running apply " + "command. defaults to 6.", type=int) +@click.option('-f', '--force', '--silent', 'silent', is_flag=True, + type=click.BOOL, default=False, help="Skip confirmation") @click.argument('files', nargs=-1) -def apply(values: str, secrets: str, files: Iterable[str], dryrun: bool = False, workers: int = 6, - silent: bool = False) -> None: +def apply( + values: str, + secrets: str, + files: Iterable[str], + dryrun: bool = False, + workers: int = 6, + silent: bool = False, + show_graph: bool = False, +) -> None: """ Apply resource manifests """ @@ -58,6 +74,15 @@ def apply(values: str, secrets: str, files: Iterable[str], dryrun: bool = False, rc = Applier(glob_files, abs_values, abs_secrets) rc.parse_dependencies() + if show_graph and dryrun: + click.secho('You cannot dry run and launch the graph together.', + fg='yellow') + return + + if show_graph: + rc.show_dependency_graph() + return + if not silent and not dryrun: click.confirm("Do you want to proceed?", default=True, abort=True) diff --git a/riocli/apply/parse.py b/riocli/apply/parse.py index d3b17c73..a13fb6fe 100644 --- a/riocli/apply/parse.py +++ b/riocli/apply/parse.py @@ -13,7 +13,6 @@ # limitations under the License. import copy import json -import os import queue import threading import typing @@ -138,14 +137,20 @@ def delete(self, *args, **kwargs): delete_order = list(self.graph.static_order()) delete_order.reverse() for obj in delete_order: - if obj in self.resolved_objects and 'manifest' in self.resolved_objects[obj]: + if obj in self.resolved_objects and 'manifest' in \ + self.resolved_objects[obj]: self._delete_manifest(obj, *args, **kwargs) def print_resolved_manifests(self): manifests = [o for _, o in self.objects.items()] dump_all_yaml(manifests) - def parse_dependencies(self, check_missing=True, delete=False, template=False): + def parse_dependencies( + self, + check_missing=True, + delete=False, + template=False, + ): number_of_objects = 0 for f, data in self.files.items(): for model in data: @@ -170,7 +175,10 @@ def parse_dependencies(self, check_missing=True, delete=False, template=False): if not template: self._display_context( - total_time=total_time, total_objects=number_of_objects, resource_list=resource_list) + total_time=total_time, + total_objects=number_of_objects, + resource_list=resource_list, + ) if check_missing: missing_resources = [] @@ -365,14 +373,19 @@ def _initialize_kind_dependency(self, kind): if not self.dependencies.get(kind): self.dependencies[kind] = {} + def show_dependency_graph(self): + """Lauches mermaid.live dependency graph""" + link = mermaid_link("\n".join(self.diagram)) + click.launch(link) + # Utils - def _display_context(self, total_time: int, total_objects: int, resource_list: typing.List) -> None: + def _display_context( + self, + total_time: int, + total_objects: int, + resource_list: typing.List + ) -> None: # Display context - - if os.environ.get('MERMAID'): - diagram_link = mermaid_link("\n".join(self.diagram)) - click.launch(diagram_link) - headers = [click.style('Resource Context', bold=True, fg='yellow')] context = [ ['Expected Time (mins)', round(total_time, 2)], diff --git a/riocli/utils/mermaid.py b/riocli/utils/mermaid.py index fddf5205..eba7ae55 100644 --- a/riocli/utils/mermaid.py +++ b/riocli/utils/mermaid.py @@ -1,3 +1,17 @@ +# Copyright 2023 Rapyuta Robotics +# +# 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 +# +# http://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 base64 import json @@ -6,41 +20,18 @@ def mermaid_safe(s: str): return s.replace(" ", "_") -def js_string_to_byte(data): +def js_string_to_byte(data: str): return bytes(data, 'iso-8859-1') -def js_bytes_to_string(data): +def js_bytes_to_string(data: bytes): return data.decode('iso-8859-1') -def js_btoa(data): +def js_btoa(data: bytes): return base64.b64encode(data) -# def js_encode_uri_component(data): -# return quote(data) - - -# def js_atob(data): -# return base64.b64decode(data) - -# def pako_inflate_raw(data): -# decompress = zlib.decompressobj(-15) -# decompressed_data = decompress.decompress(data) -# decompressed_data += decompress.flush() -# return decompressed_data - - -# def pako_deflate_raw(data): -# compress = zlib.compressobj( -# zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15, 8, -# zlib.Z_DEFAULT_STRATEGY) -# compressed_data = compress.compress(js_string_to_byte(js_encode_uri_component(data))) -# compressed_data += compress.flush() -# return compressed_data - - def mermaid_link(diagram): obj = { "code": diagram, @@ -54,4 +45,5 @@ def mermaid_link(diagram): json_str = json.dumps(obj) json_bytes = js_string_to_byte(json_str) encoded_uri = js_btoa(json_bytes) - return "https://mermaid.live/view#base64:{}".format(js_bytes_to_string(encoded_uri)) + return 'https://mermaid.live/view#base64:{}'.format( + js_bytes_to_string(encoded_uri))