Skip to content
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

Playbook generator #1136

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
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)


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:
pavangudiwada marked this conversation as resolved.
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.
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()
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


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
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"]
Loading