Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e40e34c
Feature: Adding playbook builder
pavangudiwada Jul 25, 2023
7a9d084
initial commit - actions part is broken
aantn Jul 28, 2023
c10a10c
get actions working
aantn Jul 28, 2023
ee6e93f
fixing wrong functions (#1006)
Avi-Robusta Jul 25, 2023
3253479
feat(helm): update kube-prometheus-stack to 47.2.0 (#964)
lippertmarkus Jul 26, 2023
6ec5ec2
add dropdown for prom crd error
RoiGlinik Jul 26, 2023
14126d9
Main page with sub pages but slow
pavangudiwada Sep 20, 2023
edaaf7b
recurssion issue - dont use
pavangudiwada Sep 21, 2023
37a0269
Working navigation between pages
pavangudiwada Oct 17, 2023
b55102c
Added new predefined playbooks
pavangudiwada Oct 26, 2023
1eb944e
Fixed ingress playbook
pavangudiwada Oct 27, 2023
368391c
Merge branch 'master' into natan-streamlit-triggers-test1
aantn Oct 27, 2023
b595e09
Update poetry.lock
aantn Oct 27, 2023
ab0195a
Fixed session_state code and variable name
pavangudiwada Oct 27, 2023
9e06e99
Removed extra methods, updated session variables, added a back button
pavangudiwada Oct 27, 2023
f8ba263
Updated deprecated code
pavangudiwada Oct 27, 2023
18ccbd1
simplify logic for expander_state
aantn Oct 28, 2023
0cce345
Improve pydantic models to work better w/ playbooks generator
aantn Oct 28, 2023
3fe1cd8
Improvements to playbook generator
aantn Oct 28, 2023
feb0d88
Updated Playbook text
pavangudiwada Oct 30, 2023
9a6fee8
Single page playbook generator, simple navigation (#1140)
pavangudiwada Nov 4, 2023
29de126
more schema fixes
aantn Nov 4, 2023
d17d25c
Fix helm triggers
aantn Nov 5, 2023
027a244
Update playbook_generator.py
aantn Nov 5, 2023
230e780
Use streamlit_antd_components for stepper and streamlit_extra for sty…
aantn Nov 6, 2023
2c626d3
Use pydantic_form instead of streamlit_extras.stylable_container
aantn Nov 6, 2023
723d077
Fix dependencies
aantn Nov 6, 2023
16a07a7
clarify comment
aantn Nov 6, 2023
0ebcad5
Switch back to styled container and not pydantic_form
aantn Nov 13, 2023
241d747
Fix bug with Warning events trigger
aantn Nov 13, 2023
dd98818
Issue with NonType
pavangudiwada Nov 16, 2023
bc46b1d
Fix a few bugs (not the main bug we're troubleshooting)
aantn Nov 17, 2023
633c942
Fix bug with trigger_data being None
aantn Nov 17, 2023
9a03a42
Give variable a better name
aantn Nov 20, 2023
7aef5a0
Fix bug w/ yaml output
aantn Nov 20, 2023
a9eb789
Fix bug when creating a playbook from scratch, not according to templ…
aantn Nov 20, 2023
2e9c342
Fix another bug rendering triggers to yaml
aantn Nov 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
2,793 changes: 1,673 additions & 1,120 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ robusta = "robusta.cli.main:app"


[tool.poetry.dependencies]
python = "^3.8"
python = "^3.10"
typer = "^0.4.1"
colorlog = "^5.0.1"
pydantic = "^1.8.1"
Expand Down Expand Up @@ -112,6 +112,8 @@ all = ["Flask", "grafana-api", "watchdog", "dulwich", "better-exceptions", "Cair
[tool.poetry.group.dev.dependencies]
sphinx-jinja = { git = "https://github.com/robusta-dev/sphinx-jinja.git" }
sphinx-reredirects = "^0.1.1"
streamlit = "^1.25.0"
streamlit-pydantic = "^0.6.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
44 changes: 27 additions & 17 deletions scripts/generate_playbook_descriptions.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from typing import Callable

from pydantic import BaseModel
from robusta.api import Action

from robusta.core.playbooks.generation import ExamplesGenerator

class PlaybookDescription(BaseModel):
function_name: str
Expand All @@ -32,13 +34,12 @@ def get_params_schema(func):
return action_params.schema()


def load_scripts(scripts_root):
# install_requirements(os.path.join(scripts_root, 'requirements.txt'))

def find_playbook_actions(scripts_root):
python_files = glob.glob(f"{scripts_root}/*.py")
actions = []

for script in python_files:
print(f"loading playbooks {script}")
print(f"found playbook file: {script}")
filename = os.path.basename(script)
(module_name, ext) = os.path.splitext(filename)
spec = importlib.util.spec_from_file_location(module_name, script)
Expand All @@ -47,26 +48,35 @@ def load_scripts(scripts_root):

playbooks = inspect.getmembers(
module,
lambda f: inspect.isfunction(f) and getattr(f, "__playbook", None) is not None,
lambda f: Action.is_action(f),
)
for _, func in playbooks:
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")
print("found playbook", func)
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


def main():
# TODO Arik - Need to be fixed in order to expose actions schema
parser = argparse.ArgumentParser(description="Generate playbook descriptions")
parser.add_argument("directory", type=str, help="directory containing the playbooks")
parser.add_argument("--directory", type=str, help="directory containing the playbooks", default="./playbooks/robusta_playbooks")
args = parser.parse_args()
load_scripts(args.directory)
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)
Comment on lines +75 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused variable assignment.

The triggers variable 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
actions = find_playbook_actions(args.directory)
generator = ExamplesGenerator()
trigger_to_actions = generator.get_triggers_to_actions(actions)
print(trigger_to_actions)
🧰 Tools
🪛 Ruff (0.11.9)

77-77: Local variable triggers is assigned to but never used

Remove assignment to unused variable triggers

(F841)

🤖 Prompt for AI Agents
In scripts/generate_playbook_descriptions.py around lines 75 to 79, the variable
'triggers' is assigned the result of 'generator.get_all_triggers()' but is never
used afterwards. Remove the assignment to 'triggers' to clean up the code and
avoid unused variable warnings.



if __name__ == "__main__":
Expand Down
15 changes: 15 additions & 0 deletions scripts/main_app.py
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:
st.session_state["current_page"] = "demo_playbooks"

if "playbook_choosen" not in st.session_state:
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()
67 changes: 67 additions & 0 deletions scripts/pages/demo_playbooks.py
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)
111 changes: 111 additions & 0 deletions scripts/pages/playbook_builder.py
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()
55 changes: 55 additions & 0 deletions src/robusta/core/playbooks/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix duplicate and unused imports.

Multiple import issues need to be resolved:

  • argparse is imported but unused
  • Callable is already imported on line 4
  • BaseModel is imported but unused
  • Action is already imported on line 9
-import argparse
-import glob
-import importlib
-import inspect
-import os
-from typing import Callable
-
-from pydantic import BaseModel
-from robusta.api import Action

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.11.9)

14-14: argparse imported but unused

Remove unused import: argparse

(F401)


19-19: Redefinition of unused Callable from line 4

Remove definition: Callable

(F811)


21-21: pydantic.BaseModel imported but unused

Remove unused import: pydantic.BaseModel

(F401)


22-22: Redefinition of unused Action from line 9

(F811)

🤖 Prompt for AI Agents
In src/robusta/core/playbooks/generation.py around lines 14 to 22, remove the
unused imports argparse and BaseModel, and eliminate the duplicate imports of
Callable and Action by keeping only one import statement for each. This will
clean up the import section and avoid redundancy.



def get_possible_types(t):
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Remove code duplication - this function already exists in generate_playbook_descriptions.py.

This function is an exact duplicate of the one in scripts/generate_playbook_descriptions.py. To follow DRY principles, keep it in one location and import it where needed.

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
In src/robusta/core/playbooks/generation.py around lines 157 to 186, the
function find_playbook_actions is duplicated from
scripts/generate_playbook_descriptions.py. Remove this function and its related
imports (lines 14-22) from generation.py, then import find_playbook_actions from
generate_playbook_descriptions.py or a shared utilities module to avoid
duplication and follow DRY principles.

25 changes: 25 additions & 0 deletions streamlit.Dockerfile
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"]