Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[flake8]
max-line-length = 88
select = C,E,F,W,B,B9
ignore = E203, E501, W503, B006
ignore = E203, E501, W503, B006, E712
exclude =
.hg,
.git,
Expand Down
63 changes: 63 additions & 0 deletions landoapi/api/repos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import logging

from flask import current_app, g

from landoapi import auth
from landoapi.models.repo import RepoNotice
from landoapi.repos import get_repos_for_env, SCM_ALLOW_DIRECT_PUSH
from landoapi.storage import db

logger = logging.getLogger(__name__)
auth_params = {"scopes": ("lando", "profile", "email"), "userinfo": True}


@auth.require_auth0(**auth_params)
def get_repo_notices():
if SCM_ALLOW_DIRECT_PUSH.active_group not in g.auth0_user.groups:
raise auth._not_authorized_problem_exception()
supported_repos = get_repos_for_env(current_app.config.get("ENVIRONMENT"))
repos = list(supported_repos.keys())
notices = RepoNotice.query.filter(RepoNotice.is_archived == False).order_by(
RepoNotice.updated_at.desc()
)

return {"notices": [n.serialize() for n in notices], "repos": repos}, 200


@auth.require_auth0(**auth_params)
def post_repo_notice(data):
if SCM_ALLOW_DIRECT_PUSH.active_group not in g.auth0_user.groups:
raise auth._not_authorized_problem_exception()
notice = RepoNotice()
for attr in data:
setattr(notice, attr, data[attr])
db.session.add(notice)
db.session.commit()
logger.info(f"{notice.id} was created.")
return notice.serialize(), 201


@auth.require_auth0(**auth_params)
def put_repo_notice(notice_id, data):
if SCM_ALLOW_DIRECT_PUSH.active_group not in g.auth0_user.groups:
raise auth._not_authorized_problem_exception()
notice = RepoNotice.get(notice_id)
for attr in data:
setattr(notice, attr, data[attr])
db.session.add(notice)
db.session.commit()
return notice.serialize(), 200


@auth.require_auth0(**auth_params)
def delete_repo_notice(notice_id):
if SCM_ALLOW_DIRECT_PUSH.active_group not in g.auth0_user.groups:
raise auth._not_authorized_problem_exception()
notice = RepoNotice.query.get(notice_id)
notice.is_archived = True
db.session.add(notice)
db.session.commit()
return notice.serialize(), 200
4 changes: 4 additions & 0 deletions landoapi/api/stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
serialize_diff,
serialize_status,
)
from landoapi.models.repo import RepoNotice
from landoapi.stacks import (
build_stack_graph,
calculate_landable_subgraphs,
Expand Down Expand Up @@ -155,12 +156,14 @@ def get(revision_id):
)

repositories = []
repo_notices = {}
for phid in stack_data.repositories.keys():
short_name = PhabricatorClient.expect(
stack_data.repositories[phid], "fields", "shortName"
)

repo = supported_repos.get(short_name)
repo_notices[short_name] = RepoNotice.get_active_repo_notices(short_name)
landing_supported = repo is not None
url = (
repo.url
Expand All @@ -181,6 +184,7 @@ def get(revision_id):

return {
"repositories": repositories,
"repo_notices": repo_notices,
"revisions": revisions_response,
"edges": [e for e in edges],
"landable_paths": landable,
Expand Down
2 changes: 2 additions & 0 deletions landoapi/api/transplants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from landoapi.hgexports import build_patch_for_revision
from landoapi.models.transplant import Transplant, TransplantStatus
from landoapi.models.landing_job import LandingJob, LandingJobStatus
from landoapi.models.repo import RepoNotice
from landoapi.patches import upload
from landoapi.phabricator import PhabricatorClient
from landoapi.projects import (
Expand Down Expand Up @@ -210,6 +211,7 @@ def _assess_transplant_request(phab, landing_path):
get_secure_project_phid(phab),
get_testing_tag_project_phids(phab),
get_testing_policy_phid(phab),
RepoNotice.get_active_repo_notices(landing_repo.short_name),
)
return (assessment, to_land, landing_repo, stack_data)

Expand Down
2 changes: 2 additions & 0 deletions landoapi/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ def _mock_userinfo_claims(userinfo):
"all_scm_level_2",
"active_scm_level_1",
"all_scm_level_1",
"active_scm_allow_direct_push",
"all_scm_allow_direct_push",
]
elif a0_mock_option == "inject_invalid":
userinfo["https://sso.mozilla.com/claim/groups"] = ["invalid_group"]
Expand Down
12 changes: 10 additions & 2 deletions landoapi/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from landoapi.models.configuration import ConfigurationVariable
from landoapi.models.landing_job import LandingJob
from landoapi.models.repo import RepoNotice
from landoapi.models.secapproval import SecApprovalRequest
from landoapi.models.transplant import Transplant
from landoapi.models.configuration import ConfigurationVariable

__all__ = ["LandingJob", "SecApprovalRequest", "Transplant", "ConfigurationVariable"]

__all__ = [
"ConfigurationVariable",
"LandingJob",
"RepoNotice",
"SecApprovalRequest",
"Transplant",
]
17 changes: 17 additions & 0 deletions landoapi/models/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import datetime
import re

from sqlalchemy.ext.declarative import declared_attr
Expand Down Expand Up @@ -44,3 +45,19 @@ def __repr__(self):
For example, `<Transplant: 1235>`.
"""
return f"<{self.__class__.__name__}: {self.id}>"

def serialize(self):
"""Return a JSON compatible dictionary.

This method should be extended in subclasses in order to include additional
fields.
"""
return {
"id": self.id,
"created_at": (
self.created_at.astimezone(datetime.timezone.utc).isoformat()
),
"updated_at": (
self.updated_at.astimezone(datetime.timezone.utc).isoformat()
),
}
66 changes: 66 additions & 0 deletions landoapi/models/repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import datetime
import logging

from sqlalchemy import or_, and_

from landoapi.models.base import Base
from landoapi.storage import db

logger = logging.getLogger(__name__)


class RepoNotice(Base):
"""A scheduled notice that is associated with a repository."""

# This currently matches the keys in `landoapi.repos.REPO_CONFIG`.
# In the future, a `Repo` model should be created to house repos.
repo_identifier = db.Column(db.String(254), nullable=False)
start_date = db.Column(db.DateTime(timezone=True), nullable=True)
end_date = db.Column(db.DateTime(timezone=True), nullable=True)
message = db.Column(db.Text(), default="")
is_archived = db.Column(db.Boolean, default=False)

# When set to `True`, results in a landing warning.
is_warning = db.Column(db.Boolean, default=False)

@classmethod
def get_active_repo_notices(cls, repo_short_name):
now = datetime.datetime.now()
notices = (
cls.query.filter(
and_(cls.repo_identifier == repo_short_name, cls.is_archived == False)
)
.filter(
or_(
and_(cls.start_date <= now, cls.end_date >= now),
and_(cls.start_date <= now, cls.end_date == None),
and_(cls.start_date == None, cls.end_date >= now),
and_(cls.start_date == None, cls.end_date == None),
)
)
.order_by(cls.updated_at.desc())
)
return [n.serialize() for n in notices.all()]

def serialize(self):
data = super().serialize()
data.update(
{
"repo_identifier": self.repo_identifier,
"start_date": self.start_date.astimezone(
datetime.timezone.utc
).isoformat()
if self.start_date
else None,
"end_date": self.end_date.astimezone(datetime.timezone.utc).isoformat()
if self.end_date
else None,
"message": self.message,
"is_archived": self.is_archived,
"is_warning": self.is_warning,
}
)
return data
6 changes: 6 additions & 0 deletions landoapi/repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ def __post_init__(self):
display_name="scm_firefoxci",
)

SCM_ALLOW_DIRECT_PUSH = AccessGroup(
active_group="active_scm_allow_direct_push",
membership_group="all_scm_allow_direct_push",
display_name="scm_allow_direct_push",
)

# DONTBUILD flag and help text.
DONTBUILD = (
"DONTBUILD",
Expand Down
99 changes: 99 additions & 0 deletions landoapi/spec/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,84 @@ paths:
schema:
allOf:
- $ref: '#/definitions/Error'
/repos/notices:
get:
operationId: landoapi.api.repos.get_repo_notices
description: Get a list of all repo notices
responses:
200:
description: OK
schema:
type: object
default:
description: Unexpected error
schema:
allOf:
- $ref: '#/definitions/Error'
post:
operationId: landoapi.api.repos.post_repo_notice
description: Post a new notice on a repo.
parameters:
- name: data
required: true
in: body
schema:
allOf:
- $ref: '#/definitions/RepoNotice'
responses:
201:
description: OK
schema:
type: object
default:
description: Unexpected error
schema:
allOf:
- $ref: '#/definitions/Error'
/repos/notices/{notice_id}:
put:
operationId: landoapi.api.repos.put_repo_notice
description: Edit a repo notice.
parameters:
- name: notice_id
in: path
type: string
description: The primary key of the repo notice to edit.
required: true
- name: data
required: true
in: body
schema:
type: object
responses:
200:
description: OK
schema:
type: object
default:
description: Unexpected error
schema:
allOf:
- $ref: '#/definitions/Error'
delete:
operationId: landoapi.api.repos.delete_repo_notice
description: Archive a repo notice.
parameters:
- name: notice_id
in: path
type: string
description: The primary key of the repo notice to archive.
required: true
responses:
200:
description: OK
schema:
type: object
default:
description: Unexpected error
schema:
allOf:
- $ref: '#/definitions/Error'
/stacks/{revision_id}:
get:
description: |
Expand Down Expand Up @@ -335,6 +413,27 @@ paths:
allOf:
- $ref: '#/definitions/Error'
definitions:
RepoNotice:
type: object
properties:
id:
type: integer
message:
type: string
start_date:
type: string
x-nullable: true
end_date:
type: string
x-nullable: true
created_at:
type: string
updated_at:
type: string
is_warning:
type: boolean
is_archived:
type: boolean
LandingPath:
type: array
description: |
Expand Down
Loading