-
Notifications
You must be signed in to change notification settings - Fork 283
Playbook generator #1136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Playbook generator #1136
Changes from 13 commits
e40e34c
7a9d084
c10a10c
ee6e93f
3253479
6ec5ec2
14126d9
edaaf7b
37a0269
b55102c
1eb944e
368391c
b595e09
ab0195a
9e06e99
f8ba263
18ccbd1
0cce345
3fe1cd8
feb0d88
9a6fee8
29de126
d17d25c
027a244
230e780
2c626d3
723d077
16a07a7
0ebcad5
241d747
dd98818
bc46b1d
633c942
9a03a42
7aef5a0
a9eb789
2e9c342
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 3.10 |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import streamlit as st | ||
| from pages import demo_playbooks, playbook_builder | ||
| from streamlit import session_state as ss | ||
|
|
||
| if "current_page" not in st.session_state: | ||
pavangudiwada marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| st.session_state["current_page"] = "demo_playbooks" | ||
|
|
||
| if "playbook_choosen" not in st.session_state: | ||
pavangudiwada marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ss.playbook_choosen = False | ||
|
|
||
| if st.session_state.current_page == "demo_playbooks" and not ss.playbook_choosen: | ||
| demo_playbooks.display_demo_playbook() | ||
|
|
||
| elif st.session_state.current_page == "playbook_builder": | ||
| playbook_builder.display_playbook_builder() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import streamlit as st | ||
| from streamlit import session_state as ss | ||
|
|
||
|
|
||
| def update_changes(): | ||
| ss.current_page = "playbook_builder" | ||
| ss.expander_state = [False, False, False, False, True] | ||
| ss.playbook_choosen = True | ||
|
|
||
|
|
||
| def release_fail_options(): | ||
| ss.trigger = "on_helm_release_fail" | ||
| ss.actions = "helm_status_enricher" | ||
| update_changes() | ||
|
|
||
|
|
||
| def deployment_change_options(): | ||
| ss.trigger = "on_deployment_update" | ||
| ss.actions = "resource_babysitter" | ||
| update_changes() | ||
|
|
||
|
|
||
| def ingress_change_options(): | ||
| ss.trigger = "on_ingress_all_changes" | ||
| ss.actions = "resource_babysitter" | ||
| update_changes() | ||
|
|
||
|
|
||
| def display_demo_playbook(): | ||
|
|
||
| st.set_page_config( | ||
| page_title="Demo Playbooks", | ||
| page_icon=":wrench:", | ||
| ) | ||
| st.title("Demo Playbooks", anchor=None) | ||
|
|
||
| if "expander_state" not in st.session_state: | ||
| ss.expander_state = [True, False, False, False, False] | ||
| if "triggers" not in ss: | ||
| ss.triggers = "" | ||
| if "actions" not in ss: | ||
| ss.actions = "" | ||
|
|
||
| release_fail_expander = st.expander(":zap: Get notified when a Helm release fails", expanded=False) | ||
| deployment_change_expander = st.expander(":zap: Get notified when a deployment changes", expanded=False) | ||
| ingress_change_expander = st.expander(":zap: Get notified when an ingress changes", expanded=False) | ||
|
|
||
| with release_fail_expander: | ||
| st.markdown( | ||
| "on_helm_release_fail is triggered when a Helm release enters a failed state. This is a one-time trigger, meaning that it only fires once when the release fails." | ||
| ) | ||
| st.image("./docs/images/helm-release-failed.png") | ||
| st.button("Use Playbook", key="but_release_fail", on_click=release_fail_options) | ||
|
|
||
| with deployment_change_expander: | ||
| st.markdown( | ||
| "on_deployment_update is triggered for every deployment updated. When this happens the resource_babysitter action sends you the changed field and details of what changed." | ||
| ) | ||
| st.image("./docs/images/deployment-image-change.png") | ||
| st.button("Use Playbook", key="but_deploy_change", on_click=deployment_change_options) | ||
|
|
||
| with ingress_change_expander: | ||
| st.markdown( | ||
| "on_ingress_all_changes is triggered for every change in an ingress. The resource_babysitter action sends you the changed field and details of what changed." | ||
| ) | ||
| st.image("./docs/images/ingress-image-change.png") | ||
| st.button("Use Playbook", key="but_ingress_change", on_click=ingress_change_options) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| # run with poetry run streamlit run scripts/playbook_builder.py | ||
| from collections import OrderedDict | ||
|
|
||
| import streamlit as st | ||
| import streamlit_pydantic as sp | ||
| import yaml | ||
|
|
||
| # from robusta.api import Action | ||
| from robusta.core.playbooks.generation import ExamplesGenerator, find_playbook_actions | ||
|
|
||
| # from streamlit import session_state as ss | ||
|
|
||
|
|
||
| # from typing import List, Optional | ||
| # from pydantic import BaseModel, Field | ||
| generator = ExamplesGenerator() | ||
| triggers = generator.get_all_triggers() | ||
| actions = find_playbook_actions("./playbooks/robusta_playbooks") | ||
| actions_by_name = {a.action_name: a for a in actions} | ||
| triggers_to_actions = generator.get_triggers_to_actions(actions) | ||
|
|
||
|
|
||
| def display_playbook_builder(): | ||
|
|
||
| st.title(":wrench: Playbook Builder", anchor=None) | ||
| if "expander_state" not in st.session_state: | ||
| st.session_state.expander_state = [True, False, False, False, False] | ||
|
|
||
| # INITIALIZING ALL EXPANDERS | ||
| trigger_expander = st.expander( | ||
| ":zap: Trigger - A trigger is an event that starts your Playbook", expanded=st.session_state.expander_state[0] | ||
| ) | ||
| trigger_parameter_expander = st.expander("Configure Trigger", expanded=st.session_state.expander_state[1]) | ||
| action_expander = st.expander( | ||
| ":boom: Action - An action is an event a Playbook performs after it starts", | ||
| expanded=st.session_state.expander_state[2], | ||
| ) | ||
| action_parameter_expander = st.expander("Configure Action", expanded=st.session_state.expander_state[3]) | ||
| playbook_expander = st.expander(":scroll: Playbook", expanded=st.session_state.expander_state[4]) | ||
|
|
||
| # TRIGGER | ||
| with trigger_expander: | ||
|
|
||
| trigger_name = st.selectbox("Type to search", triggers.keys(), key="trigger") | ||
|
|
||
| if st.button("Continue", key="button1"): | ||
| st.session_state.expander_state = [False, True, False, False, False] | ||
| st.experimental_rerun() | ||
|
|
||
| # TRIGGER PARAMETER | ||
| with trigger_parameter_expander: | ||
| st.header("Available Parameters") | ||
| trigger_data = sp.pydantic_input(key=f"trigger_form-{trigger_name}", model=triggers[trigger_name]) | ||
|
|
||
| if st.button("Continue", key="button2"): | ||
| st.session_state.expander_state = [False, False, True, False, False] | ||
| st.experimental_rerun() | ||
|
|
||
| # ACTION | ||
| with action_expander: | ||
| relevant_actions = [a.action_name for a in triggers_to_actions[trigger_name]] | ||
| action_name = st.selectbox("Choose an action", relevant_actions, key="actions") | ||
|
|
||
| # st.markdown[action_name]["about"]) | ||
|
|
||
| if st.button("Continue", key="button3"): | ||
| st.session_state.expander_state = [False, False, False, True, False] | ||
| st.experimental_rerun() | ||
|
|
||
| # ACTION PARAMETER | ||
| with action_parameter_expander: | ||
| action_obj = actions_by_name.get(action_name, None) | ||
|
|
||
| if action_obj and hasattr(action_obj, "params_type") and hasattr(action_obj.params_type, "schema"): | ||
| action_data = sp.pydantic_input(key=f"action_form-{action_name}", model=action_obj.params_type) | ||
| if st.button("Continue", key="button4"): | ||
| st.session_state.expander_state = [False, False, False, False, True] | ||
| st.experimental_rerun() | ||
| else: | ||
| st.markdown("This action doesn't have any parameters") | ||
| st.session_state.expander_state = [False, False, False, False, True] | ||
| action_data = None | ||
|
|
||
| # DISPLAY PLAYBOOK | ||
| with playbook_expander: | ||
|
|
||
| st.markdown( | ||
| "Add this code to your **generated_values.yaml** and [upgrade Robusta](https://docs.robusta.dev/external-prom-docs/setup-robusta/upgrade.html)" | ||
| ) | ||
|
|
||
| if action_data is None: | ||
| playbook = { | ||
| "customPlaybooks": [ | ||
| OrderedDict([("triggers", [{trigger_name: trigger_data}]), ("actions", [{action_name: {}}])]) | ||
| ] | ||
| } | ||
| else: | ||
| playbook = { | ||
| "customPlaybooks": [ | ||
| OrderedDict( | ||
| [("triggers", [{trigger_name: trigger_data}]), ("actions", [{action_name: action_data}])] | ||
| ) | ||
| ] | ||
| } | ||
|
|
||
| yaml.add_representer( | ||
| OrderedDict, lambda dumper, data: dumper.represent_mapping("tag:yaml.org,2002:map", data.items()) | ||
| ) | ||
|
|
||
| st.code(yaml.dump(playbook)) | ||
| # st.experimental_rerun() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,15 @@ | |
| from robusta.core.playbooks.trigger import Trigger | ||
| from robusta.integrations.kubernetes.autogenerated.events import KubernetesAnyChangeEvent, KubernetesResourceEvent | ||
| from robusta.utils.json_schema import example_from_schema | ||
| import argparse | ||
| import glob | ||
| import importlib | ||
| import inspect | ||
| import os | ||
| from typing import Callable | ||
|
|
||
| from pydantic import BaseModel | ||
| from robusta.api import Action | ||
|
Comment on lines
+14
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix duplicate and unused imports. Multiple import issues need to be resolved:
-import argparse
-import glob
-import importlib
-import inspect
-import os
-from typing import Callable
-
-from pydantic import BaseModel
-from robusta.api import Action
🧰 Tools🪛 Ruff (0.11.9)14-14: Remove unused import: (F401) 19-19: Redefinition of unused Remove definition: (F811) 21-21: Remove unused import: (F401) 22-22: Redefinition of unused (F811) 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def get_possible_types(t): | ||
|
|
@@ -51,6 +60,20 @@ def __init__(self): | |
| for e in possible_events: | ||
| self.events_to_triggers[e].add(t) | ||
|
|
||
| def get_all_triggers(self): | ||
| """ | ||
| Return a dict with all triggers, in the format { "on_trigger_..." : TriggerClass } | ||
| """ | ||
| return dict((v, k) for k, v in self.triggers_to_yaml.items()) | ||
|
|
||
| def get_triggers_to_actions(self, all_actions: List[Action]): | ||
| triggers_to_actions = defaultdict(list) | ||
| for action in all_actions: | ||
| triggers = self.events_to_triggers[action.event_type] | ||
| for t in triggers: | ||
| triggers_to_actions[self.triggers_to_yaml[t]].append(action) | ||
| return triggers_to_actions | ||
|
|
||
| def get_possible_triggers(self, event_cls: Type[ExecutionBaseEvent]) -> List[str]: | ||
| name = event_cls.__name__ | ||
| # TODO: why? | ||
|
|
@@ -129,3 +152,35 @@ def generate_example_config( | |
| action_example = example_from_schema(action_schema) | ||
| example["customPlaybooks"][0]["actions"][0][action_metadata.action_name] = action_example | ||
| return yaml.dump(example, Dumper=NoAliasDumper) | ||
|
|
||
|
|
||
| def find_playbook_actions(scripts_root): | ||
| python_files = glob.glob(f"{scripts_root}/*.py") | ||
| actions = [] | ||
|
|
||
| for script in python_files: | ||
| filename = os.path.basename(script) | ||
| (module_name, ext) = os.path.splitext(filename) | ||
| spec = importlib.util.spec_from_file_location(module_name, script) | ||
| module = importlib.util.module_from_spec(spec) | ||
| spec.loader.exec_module(module) | ||
|
|
||
| playbooks = inspect.getmembers( | ||
| module, | ||
| lambda f: Action.is_action(f), | ||
| ) | ||
| for _, func in playbooks: | ||
| action = Action(func) | ||
| actions.append(action) | ||
|
|
||
| #description = PlaybookDescription( | ||
| # function_name=func.__name__, | ||
| # builtin_trigger_params=func.__playbook["default_trigger_params"], | ||
| # docs=inspect.getdoc(func), | ||
| # src=inspect.getsource(func), | ||
| # src_file=inspect.getsourcefile(func), | ||
| # action_params=get_params_schema(func), | ||
| #) | ||
| #print(description.json(), "\n\n") | ||
|
|
||
| return actions | ||
|
Comment on lines
+157
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Remove code duplication - this function already exists in generate_playbook_descriptions.py. This function is an exact duplicate of the one in Since this function is used by scripts, it would be better to keep it in the scripts file and import it here if needed, or move it to a shared utilities module. The duplicate function and its associated imports (lines 14-22) should be removed from this file. 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Use the specific version of Python as the base image | ||
| FROM python:3.10.12 | ||
|
|
||
|
|
||
| RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends nodejs \ | ||
| && pip3 install --no-cache-dir --upgrade pip \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Install poetry | ||
| RUN curl -sSL https://install.python-poetry.org | python3 - | ||
| RUN mv /root/.local/bin/poetry /usr/local/bin | ||
|
|
||
| # Set the working directory in the Docker image | ||
| WORKDIR /robusta | ||
|
|
||
| COPY . /robusta/ | ||
|
|
||
| # Configure poetry and install packages | ||
| RUN poetry config virtualenvs.create false | ||
| RUN poetry install --extras "all" | ||
|
|
||
| # Command to run Streamlit when the container starts | ||
| CMD ["streamlit", "run", "./scripts/main_app.py"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused variable assignment.
The
triggersvariable is assigned but never used.actions = find_playbook_actions(args.directory) generator = ExamplesGenerator() - triggers = generator.get_all_triggers() trigger_to_actions = generator.get_triggers_to_actions(actions) print(trigger_to_actions)📝 Committable suggestion
🧰 Tools
🪛 Ruff (0.11.9)
77-77: Local variable
triggersis assigned to but never usedRemove assignment to unused variable
triggers(F841)
🤖 Prompt for AI Agents