Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ SLACK_CLIENT_SECRET=""
SWITCHER_JWT_SECRET=""
SWITCHER_URL="http://localhost:4200"
SWITCHER_API_URL="http://localhost:3000"
SWITCHER_CERT_PATH="path/to/cert.pem"
SWITCHER_CERT_PATH="path/to/cert.pem"
LOG_LEVEL="INFO"
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ A summary message containing all details about the change will be sent to a spec

# Running locally

## Requirements
## Requirements
- Python 3.14
- VirtualEnv - `pip install virtualenv`
- Pipenv - `pip install pipenv`
Expand Down Expand Up @@ -72,13 +72,14 @@ This will trigger the callback to Switcher Management [SWITCHER_URL] to proceed
2. Install dependencies: `make install`
3. Copy the values Client ID, Secret and Signing Secret.
4. Create a .env file based on .env.template and paste the copied values.
- `LOG_LEVEL` controls application and root logger verbosity. Use values such as `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL`.
5. Make sure that the SWITCHER_JWT_SECRET matches the Switcher API env value for SWITCHER_SLACK_JWT_SECRET
6. Make sure that Switcher Management has SWITCHERSLACKAPP_URL set to the app URL
7. Start the API by running: `make run`

## Contributing

You are more than welcome to contribute to the project.
You are more than welcome to contribute to the project.
Here are some important guidelines:

1. Suggestions: Open a discussion topic or issue and describe clearly what you have in mind.
Expand All @@ -99,4 +100,4 @@ make test
```
make test
make cover
```
```
7 changes: 7 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import logging

from events.request_change import RequestChangeEventHandler
from controller.slack_app import SlackAppHandler
from utils.logging_config import configure_logging

LOGGER = logging.getLogger(__name__)

slack_handler = SlackAppHandler()
request_change_handler = RequestChangeEventHandler()

# Build App
configure_logging()
app = slack_handler.build_app()

# Register App Events
Expand All @@ -14,4 +20,5 @@
slack_app = slack_handler.register_handler(app)

if __name__ == "__main__":
LOGGER.info("Starting Slack App...")
slack_app.run()
56 changes: 41 additions & 15 deletions src/controller/change_request.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging

from services.switcher_service import SwitcherService
from utils.switcher_util import get_environment_keyval, get_keyval, validate_context_request
Expand All @@ -23,18 +24,23 @@
read_request_metadata
)

LOGGER = logging.getLogger(__name__)

def on_domain_selected(ack, body, client, logger):
""" Load environments when domain is selected """

ack()
current_logger = logger or LOGGER

try:
# Collect args
team_id = body["team"]["id"]
domain_id = get_selected_action(body)
domain_name = get_selected_action_text(body)
current_logger.debug("Domain %s selected for team %s", domain_name, team_id)

envs = SwitcherService().get_environments(team_id, domain_id or "") or []
current_logger.debug("Environments %s loaded for domain %s", envs, domain_name)

# Clear previous selection
populate_selection(body["view"], "Group", NEW_SELECTION)
Expand Down Expand Up @@ -67,21 +73,24 @@ def on_domain_selected(ack, body, client, logger):
return body["view"]
except Exception as e:
error = f"Error opening change request form: {e}"
logger.error(error)
current_logger.exception(error)
return error

def on_environment_selected(ack, body, client, logger):
""" Load groups when environment is selected """

ack()
current_logger = logger or LOGGER

try:
# Collect args
env_selected = get_selected_action(body)
team_id = body["team"]["id"]
domain_id = read_request_metadata(body["view"])["domain_id"]
current_logger.debug("Environment %s selected for team %s in domain %s", env_selected, team_id, domain_id)

groups = SwitcherService().get_groups(team_id, domain_id, env_selected or "") or []
current_logger.debug("Groups %s loaded for environment %s", groups, env_selected)

# Clear previous selection
populate_selection(body["view"], "Group", NEW_SELECTION)
Expand Down Expand Up @@ -109,13 +118,14 @@ def on_environment_selected(ack, body, client, logger):
return body["view"]
except Exception as e:
error = f"Error selecting environment: {e}"
logger.error(error)
current_logger.exception(error)
return error

def on_group_selected(ack, body, client, logger):
""" Load switchers when group is selected """

ack()
current_logger = logger or LOGGER

try:
# Collect args
Expand All @@ -124,10 +134,12 @@ def on_group_selected(ack, body, client, logger):
group_status = get_selected_action_status(body)
team_id = body["team"]["id"]
domain_id = read_request_metadata(body["view"])["domain_id"]
current_logger.debug("Group %s selected for team %s in domain %s", group_selected, team_id, domain_id)

switchers = SwitcherService().get_switchers(
team_id, domain_id, env_selected, group_selected or ""
) or []
current_logger.debug("Switchers %s loaded for group %s", switchers, group_selected)

# Clear previous selection
populate_selection(body["view"], "Switcher", NEW_SELECTION)
Expand Down Expand Up @@ -157,21 +169,25 @@ def on_group_selected(ack, body, client, logger):
return body["view"]
except Exception as e:
error = f"Error selecting group: {e}"
logger.error(error)
current_logger.exception(error)
return error

def on_switcher_selected(ack, body, client):
""" Updates view's metadata with switcher selection """

ack()
current_logger = LOGGER

# Populate view
selected_switcher = get_selected_action_text(body)
current_logger.debug("Switcher %s selected", selected_switcher)
if selected_switcher != "-":
switcher_status = get_selected_action_status(body)
current_logger.debug("Switcher selected with status %s", switcher_status)
populate_selection_status(body["view"], switcher_status)
else:
selected_group = get_state_name(body["view"], "selection_group")
current_logger.debug("No switcher selected, reverting to group %s status", selected_group)
populate_selection_status(body["view"], get_status(selected_group))

view_hash = body["view"]["hash"]
Expand All @@ -190,6 +206,7 @@ def on_change_request_review(ack, body, client, view, logger):
""" Populate context with selections, validate via Switcher API then publish view for review """

ack()
current_logger = logger or LOGGER
user = body["user"]
team_id = body["team"]["id"]

Expand All @@ -207,6 +224,7 @@ def on_change_request_review(ack, body, client, view, logger):
"status": get_state_value(view, "selection_status")
}

current_logger.debug("Validating change request with context %s", json.dumps(context))
validate_context_request(context)
result = SwitcherService().validate_ticket(team_id, context)

Expand Down Expand Up @@ -241,13 +259,14 @@ def on_change_request_review(ack, body, client, view, logger):
)

error = f"Error on change request review: {e}"
logger.error(error)
current_logger.exception(error)
return error

def on_submit(ack, body, client, logger):
""" Create ticket, return to home view then publish approval message """

ack()
current_logger = logger or LOGGER
user = body["user"]
team_id = body["team"]["id"]

Expand All @@ -258,7 +277,6 @@ def on_submit(ack, body, client, logger):
**read_request_metadata(body["view"]),
"observations": "" if observation is None else observation,
}

domain_id = context["domain_id"]

# Return to initial state
Expand All @@ -269,6 +287,7 @@ def on_submit(ack, body, client, logger):
)

# Create ticket and post approval request
current_logger.debug("Creating ticket for change request with context %s", json.dumps(context))
ticket = SwitcherService().create_ticket(team_id, context)
ticket_payload = {
"id": ticket.get("ticket_id"),
Expand All @@ -284,12 +303,13 @@ def on_submit(ack, body, client, logger):

return request_message, ticket
except Exception as e:
logger.error(f"Error on submitting: {e}")
error = f"Error on submitting: {e}"
current_logger.exception(error)
client.chat_postMessage(
channel = user["id"],
text = f"There was an error with your request: {e}"
)
return e
return error

def on_change_request_abort(ack, body, client):
""" Return to home view """
Expand All @@ -306,6 +326,7 @@ def on_request_approved(ack, body, client, logger):
""" Approve ticket through Switcher API and update chat message """

ack()
current_logger = logger or LOGGER
message_ts = body["message"]["ts"]
team_id = body["team"]["id"]
channel_id = body["channel"]["id"]
Expand All @@ -319,6 +340,7 @@ def on_request_approved(ack, body, client, logger):
message_blocks.append(body["message"]["blocks"][2])

# Approve ticket and update message
current_logger.debug("Approving ticket %s for domain %s in team %s", ticket_id, domain_id, team_id)
SwitcherService().approve_request(team_id, domain_id, ticket_id)
client.chat_update(
channel = channel_id,
Expand All @@ -329,19 +351,21 @@ def on_request_approved(ack, body, client, logger):

return message_blocks
except Exception as e:
logger.error(f"Error on aborting: {e}")
error = f"Error on approving request: {e}"
current_logger.exception(error)
client.chat_update(
channel = channel_id,
text = e.args[0],
text = f"There was an error with your request: {e}",
ts = message_ts,
blocks = create_block_message(f":large_yellow_square: *{e.args[0]}*")
blocks = create_block_message(f":large_yellow_square: *{e}*")
)
return e
return error

def on_request_denied(ack, body, client, logger):
""" Deny ticket through Switcher API and update chat message """

ack()
current_logger = logger or LOGGER
message_ts = body["message"]["ts"]
team_id = body["team"]["id"]
channel_id = body["channel"]["id"]
Expand All @@ -355,6 +379,7 @@ def on_request_denied(ack, body, client, logger):
message_blocks.append(body["message"]["blocks"][2])

# Deny ticket and update message
current_logger.debug("Denying ticket %s for domain %s in team %s", ticket_id, domain_id, team_id)
SwitcherService().deny_request(team_id, domain_id, ticket_id)
client.chat_update(
channel = channel_id,
Expand All @@ -365,11 +390,12 @@ def on_request_denied(ack, body, client, logger):

return message_blocks
except Exception as e:
logger.error(f"Error on denying: {e}")
error = f"Error on denying request: {e}"
current_logger.exception(error)
client.chat_update(
channel = channel_id,
text = e.args[0],
text = f"There was an error with your request: {e}",
ts = message_ts,
blocks = create_block_message(f":large_yellow_square: *{e.args[0]}*")
blocks = create_block_message(f":large_yellow_square: *{e}*")
)
return e
return error
11 changes: 8 additions & 3 deletions src/controller/home.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import copy
import logging

from services.switcher_service import SwitcherService
from payloads.home import MODAL_REQUEST, APP_HOME
from utils.slack_payload_util import populate_selection
from utils.switcher_util import get_keyval_of_keys

LOGGER = logging.getLogger(__name__)

def on_home_opened(client, event, logger):
""" Displays the home view """

logger.warning(f"Home view opened by {event['user']}")
current_logger = logger or LOGGER
current_logger.info("Home view opened by %s", event["user"])
client.views_publish(
user_id = event["user"],
view = APP_HOME
Expand All @@ -20,13 +24,14 @@ def on_change_request_opened(ack, body, client, logger):
""" Displays the change request modal """

ack()
current_logger = logger or LOGGER

try:
change_request = copy.deepcopy(MODAL_REQUEST)

# Collect args
team_id = body["team"]["id"]
logger.warning(f"Team ID: {team_id}")
current_logger.debug("Opening change request modal for team %s", team_id)

domains = SwitcherService().get_domains(team_id)

Expand All @@ -44,5 +49,5 @@ def on_change_request_opened(ack, body, client, logger):

return change_request
except Exception as e:
logger.error(f"Error opening change request form: {e}")
current_logger.exception(f"Error opening change request form: {e}")
return None
8 changes: 4 additions & 4 deletions src/services/switcher_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ def get_environments(self, team_id: str, domain_id: str) -> list[str] | None:
}}
''')

environments = None
configuration = response.get("configuration", None)
if configuration is not None:
environments = configuration.get("environments", None)
if environments is not None:
return environments
return environments

def get_groups(self, team_id: str, domain_id: str, environment: str) -> list[dict] | None:
""" Fetch all Groups linked to the Environment """
Expand All @@ -64,11 +64,11 @@ def get_groups(self, team_id: str, domain_id: str, environment: str) -> list[dic
}}
''')

groups = None
configuration = response.get("configuration", None)
if configuration is not None:
groups = configuration.get("group", None)
if groups is not None:
return groups
return groups

def get_switchers(self, team_id: str, domain_id: str, environment: str, group: str) -> list[dict] | None:
""" Fetch all Switchers linked to the Group """
Expand Down
Loading