Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

Feature flag for GitHub Summary feature #288

Merged
merged 9 commits into from
Jul 11, 2020
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
21 changes: 6 additions & 15 deletions busy_beaver/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,12 @@
)
from .common.oauth import OAuthError
from .config import DATABASE_URI, REDIS_URI, SECRET_KEY
from .exceptions import NotAuthorized, ValidationError
from .exceptions import NotAuthorized, StateMachineError, ValidationError
from .extensions import bootstrap, db, login_manager, migrate, rq
from .toolbox import make_response


def handle_http_error(error):
data = {"message": error.message}
return make_response(error.status_code, error=data)


def handle_oauth_error(error):
data = {"message": error.message}
return make_response(error.status_code, error=data)


def handle_validation_error(error):
def handle_error(error):
data = {"message": error.message}
return make_response(error.status_code, error=data)

Expand Down Expand Up @@ -57,9 +47,10 @@ def create_app(*, testing=False):
# TODO figure out CSP headers and re-enable
# talisman.init_app(app)

app.register_error_handler(NotAuthorized, handle_http_error)
app.register_error_handler(OAuthError, handle_oauth_error)
app.register_error_handler(ValidationError, handle_validation_error)
app.register_error_handler(NotAuthorized, handle_error)
app.register_error_handler(OAuthError, handle_error)
app.register_error_handler(ValidationError, handle_error)
app.register_error_handler(StateMachineError, handle_error)

app.register_blueprint(events_bp, cli_group=None)
app.register_blueprint(healthcheck_bp)
Expand Down
49 changes: 29 additions & 20 deletions busy_beaver/apps/github_integration/cli.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
from datetime import date, datetime, timedelta
import logging

import click
import pytz

from .blueprint import github_bp
from .models import GitHubSummaryConfiguration
from .summary.workflow import post_github_summary_message
from busy_beaver.exceptions import GitHubSummaryException
from busy_beaver.extensions import db
from busy_beaver.models import SlackInstallation, Task
from busy_beaver.models import Task

logger = logging.getLogger(__name__)


# currently we will only kick off one task, check which rows are active
@click.option("--workspace", required=True, prompt="Slack workspace ID")
@github_bp.cli.command(
"queue_github_summary_jobs", help="Queue GitHub summary jobs for tomorrow"
)
def queue_github_summary_jobs_for_tomorrow(workspace: str):
installation = SlackInstallation.query.filter_by(workspace_id=workspace).first()
time_to_post = _get_time_to_post(installation.github_summary_config)
job = post_github_summary_message.schedule(time_to_post, workspace=workspace)

task = Task(
job_id=job.id,
name="post_github_summary_message",
task_state=Task.TaskState.SCHEDULED,
data={"workspace_id": workspace},
)
db.session.add(task)
db.session.commit()
def queue_github_summary_jobs_for_tomorrow():
all_active_configs = GitHubSummaryConfiguration.query.filter_by(enabled=True)

for config in all_active_configs:
workspace_id = config.slack_installation.workspace_id
time_to_post = _get_time_to_post(config)
if not time_to_post:
continue

job = post_github_summary_message.schedule(
time_to_post, workspace_id=workspace_id
)
task = Task(
job_id=job.id,
name="post_github_summary_message",
task_state=Task.TaskState.SCHEDULED,
data={
"workspace_id": workspace_id,
"time_to_post": time_to_post.isoformat(),
},
)
db.session.add(task)
db.session.commit()


def _get_time_to_post(config):
# TODO state machine can remove this
if not config.summary_post_time or not config.summary_post_timezone:
extra = {"workspace": config.workspace}
raise GitHubSummaryException("Time to post configuration ", extra=extra)
extra = {"workspace_id": config.slack_installation.workspace_id}
logger.error("No time to post configuration", extra=extra)
return None
tomorrow = date.today() + timedelta(days=1)
dt_to_post = datetime.combine(tomorrow, config.summary_post_time)
localized_dt = config.summary_post_timezone.localize(dt_to_post)
Expand Down
41 changes: 41 additions & 0 deletions busy_beaver/apps/github_integration/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
from finite_state_machine import StateMachine, transition
from finite_state_machine.exceptions import ConditionNotMet
from sqlalchemy_utils import TimezoneType

from busy_beaver.common.models import BaseModel
from busy_beaver.exceptions import StateMachineError
from busy_beaver.extensions import db


def time_to_post_has_been_configured(self):
return self.config.summary_post_time and self.config.summary_post_timezone


class GitHubConfigEnabledStateMachine(StateMachine):
initial_state = False

def __init__(self, config):
self.config = config
self.state = config.enabled
super().__init__()

@transition(
source=False, target=True, conditions=[time_to_post_has_been_configured]
)
def enable_github_summary_feature(self):
pass

@transition(source=True, target=False)
def disable_github_summary_feature(self):
pass

def toggle(self):
if self.state:
self.disable_github_summary_feature()
else:
self.enable_github_summary_feature()


class GitHubSummaryConfiguration(BaseModel):
__tablename__ = "github_summary_configuration"

def __repr__(self): # pragma: no cover
return f"<GitHubSummaryConfiguration: {self.slack_installation.workspace_name}>"

enabled = db.Column(db.Boolean, default=False, nullable=False)
installation_id = db.Column(
db.Integer,
db.ForeignKey("slack_installation.id", name="fk_installation_id"),
Expand All @@ -27,6 +60,14 @@ def __repr__(self): # pragma: no cover
"GitHubSummaryUser", back_populates="configuration"
)

def toggle_configuration_enabled_status(self):
machine = GitHubConfigEnabledStateMachine(self)
try:
machine.toggle()
except ConditionNotMet as e:
raise StateMachineError(f"Condition failed: {e.condition.__name__}")
self.enabled = machine.state


class GitHubSummaryUser(BaseModel):

Expand Down
4 changes: 2 additions & 2 deletions busy_beaver/apps/github_integration/summary/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@


@rq.job
def post_github_summary_message(workspace: str):
installation = SlackInstallation.query.filter_by(workspace_id=workspace).first()
def post_github_summary_message(workspace_id: str):
installation = SlackInstallation.query.filter_by(workspace_id=workspace_id).first()
if not installation:
raise ValidationError("workspace not found")

Expand Down
22 changes: 21 additions & 1 deletion busy_beaver/apps/web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,24 @@ def github_summary_settings():

channel = config.channel
channel_info = slack.channel_details(channel)
return render_template("set_time.html", form=form, channel=channel_info["name"])
return render_template(
"set_time.html", form=form, channel=channel_info["name"], enabled=config.enabled
)


@web_bp.route("/settings/github-summary/toggle")
@login_required
def toggle_github_summary_config_view():
logger.info("Hit GitHub Summary Settings page")
installation = current_user.installation
slack = SlackClient(installation.bot_access_token)

is_admin = slack.is_admin(current_user.slack_id)
if not is_admin:
raise NotAuthorized("Need to be an admin to access")

config = installation.github_summary_config
config.toggle_configuration_enabled_status()
db.session.add(config)
db.session.commit()
return redirect(url_for("web.github_summary_settings"))
12 changes: 8 additions & 4 deletions busy_beaver/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ class EventEmitterEventNotRegistered(EventEmitterException):
pass


class GitHubSummaryException(BusyBeaverException):
pass


class NoMeetupEventsFound(BusyBeaverException):
pass

Expand All @@ -46,6 +42,14 @@ class SlackTooManyBlocks(BusyBeaverException):
pass


class StateMachineError(BusyBeaverException):
status_code = 500

def __init__(self, error):
super().__init__()
self.message = error


class UnverifiedWebhookRequest(NotAuthorized):
pass

Expand Down
1 change: 1 addition & 0 deletions busy_beaver/templates/set_time.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ <h1>Configure GitHub Summary</h1>


<h2>Configuration Settings</h2>
<p>Enabled: {{ enabled }} (<a href="{{ url_for('web.toggle_github_summary_config_view') }}">toggle</a>)</p>
<p>Channel: {{ channel }}</p>

<h3>Update Settings</h3>
Expand Down
4 changes: 3 additions & 1 deletion helm/charts/busybeaver/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ app.kubernetes.io/instance: {{ .Release.Name }}
Environment Variables
*/}}
{{- define "busybeaver.env_vars" }}
{{- if eq .Values.environment "production" }}
- name: ENVIRONMENT
value: {{ .Values.environment }}
{{- if eq .Values.environment "production" }}
- name: IN_PRODUCTION
value: "1"
{{- end }}
- name: BASE_URL
value: "https://{{ .Values.ingress.host }}"
- name: PYTHONPATH
value: .
- name: FLASK_APP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,4 @@ spec:
command: ["flask"]
args:
- "queue_github_summary_jobs"
- "--workspace"
- {{ .Values.workspaceId | quote }}
env: {{- include "busybeaver.env_vars" . | indent 12 }}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ include "busybeaver.fullname" . }}--scheduler"
name: "{{ include "busybeaver.fullname" . }}-scheduler"
labels:
type: scheduler-deploy
{{- include "busybeaver.labels" . | nindent 4 }}
Expand Down
2 changes: 1 addition & 1 deletion helm/charts/busybeaver/templates/deployment--web.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "busybeaver.fullname" . }}
name: "{{ include "busybeaver.fullname" . }}-web"
labels:
type: web-deploy
{{- include "busybeaver.labels" . | nindent 4 }}
Expand Down
2 changes: 1 addition & 1 deletion helm/charts/busybeaver/templates/deployment--worker.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ include "busybeaver.fullname" . }}--workers"
name: "{{ include "busybeaver.fullname" . }}-workers"
labels:
type: worker-deploy
{{- include "busybeaver.labels" . | nindent 4 }}
Expand Down
7 changes: 5 additions & 2 deletions helm/values/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

|Filename|Description|
|---|---|
|production.yaml|Production file for Busy Beaver chart|
|staging.yaml|Staging file for Busy Beaver chart|
|`bb_production.yaml`|Production file for Busy Beaver chart|
|`bb_staging.yaml`|Staging file for Busy Beaver chart|
|`redis.yaml`|Values for [bitnmai/redis](https://github.com/bitnami/charts/tree/master/bitnami/redis)|

## Commands

Expand All @@ -19,6 +20,8 @@ We will need to install Redis and copy the service DNS into the `bb_[environment
helm repo add bitnami https://charts.bitnami.com/bitnami

helm install bb-queue-staging bitnami/redis -f ./helm/values/redis.yaml

helm upgrade bb-queue-production bitnami/redis -f ./helm/values/redis.yaml
```

### Busy Beaver App
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""add enabled field to github_summary_config

Revision ID: f1adfb5b4d39
Revises: 9bc99f240f5f
Create Date: 2020-07-11 14:59:22.112438

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "f1adfb5b4d39"
down_revision = "9bc99f240f5f"
branch_labels = None
depends_on = None


def upgrade():
# Step 1: Schema migration
# Add enabled field that is nullable
op.add_column(
"github_summary_configuration",
sa.Column("enabled", sa.Boolean(), nullable=True),
)

# Step 2: Data migration
# Set enabled=False
engine = op.get_bind()
meta = sa.MetaData(bind=engine)
event = sa.Table("github_summary_configuration", meta, autoload=True)
stmt = event.update().values(enabled=False)
engine.execute(stmt)

# Step 3: Data migration
# enabled field cannot be nullable
op.alter_column("github_summary_configuration", "enabled", nullable=False)


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("github_summary_configuration", "enabled")
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ alembic==1.3.2
bootstrap-flask==1.4
cryptography==2.7
factory_boy==2.12.0
finite-state-machine==0.1.1
finite-state-machine==0.2.0
flask-login==0.5.0
Flask-Migrate==2.5.2
Flask-RQ2==18.3
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cryptography==2.7 # via -r requirements.in
decorator==4.4.2 # via validators
factory_boy==2.12.0 # via -r requirements.in
faker==1.0.7 # via factory-boy
finite-state-machine==0.1.1 # via -r requirements.in
finite-state-machine==0.2.0 # via -r requirements.in
flask-login==0.5.0 # via -r requirements.in
flask-migrate==2.5.2 # via -r requirements.in
flask-rq2==18.3 # via -r requirements.in
Expand Down
1 change: 1 addition & 0 deletions scripts/dev/populate_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

if not installation.github_summary_config:
config = GitHubSummaryConfiguration(
enabled=True,
slack_installation=installation,
channel=channel_id,
summary_post_time=time(14, 00),
Expand Down
1 change: 1 addition & 0 deletions tests/_utilities/factories/github_summary_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Meta:
sqlalchemy_session_persistence = "commit"
sqlalchemy_session = session

enabled = True
channel = "busy-beaver"
summary_post_time = time(14, 00)
summary_post_timezone = "America/Chicago"
Expand Down
Loading