Skip to content

Commit

Permalink
offline working, misc cleanup
Browse files Browse the repository at this point in the history
This now does not break `make doctest`

Also cleaned up comments, added todo's and license
  • Loading branch information
Hal Wine committed Aug 27, 2020
1 parent 8815245 commit 466084a
Show file tree
Hide file tree
Showing 15 changed files with 312 additions and 182 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# IDE files
.vscode/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
17 changes: 17 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import datetime
from typing import Any

import pytest

Expand All @@ -10,6 +11,7 @@
from gcp.client import GCPClient
from gsuite.client import GsuiteClient
from heroku.client import HerokuAdminClient
from github.client import GitHubClient

import custom_config

Expand All @@ -18,6 +20,15 @@
gcp_client = None
gsuite_client = None
heroku_client = None
github_client = None

# globals in conftest.py are hard to import from several levels down, so provide access function
def get_client(client_name: str) -> Any:
# restrict to variables with defined suffix
suffix = "_client"
if client_name.endswith(suffix):
client_name = client_name[: -len(suffix)]
return globals()[f"{client_name}_client"]


def pytest_addoption(parser):
Expand Down Expand Up @@ -69,6 +80,7 @@ def pytest_configure(config):
global gcp_client
global gsuite_client
global heroku_client
global github_client

# monkeypatch cache.set to serialize datetime.datetime's
patch_cache_set(config)
Expand Down Expand Up @@ -102,6 +114,11 @@ def pytest_configure(config):
offline=config.getoption("--offline"),
)

github_client = GitHubClient(
debug_calls=config.getoption("--debug-calls"),
offline=config.getoption("--offline"),
)

config.custom_config = custom_config.CustomConfig(config.getoption("--config"))

try:
Expand Down
38 changes: 19 additions & 19 deletions github/branches/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,38 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

# Fixtures to fetch data for the various GitHub branch checks
# PyTest support for the various GitHub branch checks

# TODO:
# - convert to logger output
# - add sleep_* for 'core' functionality
# TODO convert to logger output
# TODO add sleep_* for 'core' functionality

# from datetime import datetime
from functools import lru_cache
import os
import pathlib

# import time
from typing import List

# import sys
import subprocess

import pytest

# from sgqlc.operation import Operation # noqa: I900
from sgqlc.endpoint.http import HTTPEndpoint # noqa: I900

# from github_schema import github_schema as schema # noqa: I900

# import branch_check.retrieve_github_data as retrieve_github_data
from . import retrieve_github_data

# Needed to dynamically grab globals
import conftest


# @pytest.fixture(scope="session")
def repos_to_check() -> List[str]:
# just shell out for now
# TODO: fix ickiness
# While there is no network operation done here, we don't want to go
# poking around the file system if we're in "--offline" mode
# (e.g. doctest mode)
global github_client
if conftest.get_client("github").is_offline():
return []

# real work
path_to_metadata = os.environ["PATH_TO_METADATA"]
meta_dir = pathlib.Path(os.path.expanduser(path_to_metadata)).resolve()
in_files = list(meta_dir.glob("*.json"))
Expand Down Expand Up @@ -64,17 +65,16 @@ def repos_to_check() -> List[str]:
]


# we expect to (eventually) make multiple tests against the same branch data
@lru_cache(maxsize=32)
def get_branch_info(gql_connection, repo_full_name: str) -> str:
assert False
repo_info = retrieve_github_data.get_repo_branch_protections(
gql_connection, repo_full_name
)
return repo_info


if __name__ == "__main__":
if os.environ.get("DEBUG"):
print(repos_to_check())
# for name in sys.argv[1:]:
# data = get_branch_info(gql_connection(), name)
# print(data)
# TODO add doctests
pass
22 changes: 10 additions & 12 deletions github/branches/retrieve_github_data.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#!/usr/bin/env python3

# 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 https://mozilla.org/MPL/2.0/.
"""
Collect Information about branches sufficient to check for all branch
protection guideline compliance.
"""
# TODO add doctests

# import datetime
import csv
from functools import lru_cache
import logging
Expand All @@ -15,12 +17,6 @@
import sys
from typing import Any, List

# import re

# import sys

# from collections import OrderedDict

from sgqlc.operation import Operation # noqa: I900
from sgqlc.endpoint.http import HTTPEndpoint # noqa: I900

Expand All @@ -30,6 +26,7 @@
DEFAULT_GRAPHQL_ENDPOINT = "https://api.github.com/graphql"
EXTENSION_TO_STRIP = ".git"

# TODO use logger
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -140,6 +137,7 @@ def create_operation(owner, name):
# we only get one item at a time to
# simplify getting all.
# N.B. anything we can get multiple of, we need to gather the 'id'
# as well, in case pagination is needed
branch_protection = repo.branch_protection_rules(first=10)
branch_protection.total_count()
branch_protection.page_info.__fields__(end_cursor=True, has_next_page=True)
Expand Down Expand Up @@ -170,20 +168,18 @@ def create_rule_query():
op = Operation(schema.Query)

node = op.branch_protection_rules.nodes(cursor="$LAST_CURSOR")
# node.__fields__(is_admin_enforced=True, id=True, pattern=True)
_add_protection_fields(node)
return op


# Should be able to produce iterator for lowest level data
# TODO Should be able to produce iterator for lowest level data


def get_nested_branch_data(endpoint, reponame):
owner, name = reponame.split("/", 1)
op = create_operation(owner, name)

logger.info("Downloading base information from %s", endpoint)
# logger.debug("Operation:\n%s", op)

d = endpoint(op)
errors = d.get("errors")
Expand All @@ -206,8 +202,10 @@ def _more_to_do(cur_result, fake_new_page=False):
)
return has_more_rules or has_more_refs

# TODO determine better way to test
fake_next_page = False

# TODO ensure errors are reported out to pytest when invoked from there
while _more_to_do(repodata, fake_next_page):
fake_next_page = False
# Need to work from inside out.
Expand Down Expand Up @@ -283,7 +281,7 @@ def csv_output(data, csv_writer) -> None:
csv_writer.writerow(line)


def parse_args(**kwargs):
def parse_args():
import argparse

ap = argparse.ArgumentParser(description="GitHub Agile Dashboard")
Expand Down
19 changes: 15 additions & 4 deletions github/branches/test_branch_protection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

from typing import Any
from github.branches.validate_compliance import Criteria
from typing import Any, Optional

from .retrieve_github_data import get_repo_branch_protections
from . import validate_compliance
Expand All @@ -15,10 +16,20 @@
import pytest


@pytest.mark.parametrize("repo_to_check", repos_to_check())
@pytest.mark.parametrize("criteria", validate_compliance.required_criteria)
def idfn(val: Any) -> Optional[str]:
string = None
if isinstance(val, (str,)):
if val.startswith("https://"):
string = "/".join(val.split("/")[3:5])
return string


@pytest.mark.parametrize("repo_to_check", repos_to_check(), ids=idfn)
@pytest.mark.parametrize(
"criteria", validate_compliance.required_criteria, ids=Criteria.idfn
)
def test_required_protections(
gql_connection: Any, repo_to_check: str, criteria: str
gql_connection: Any, repo_to_check: str, criteria: Criteria
) -> None:
line = repo_to_check
# for line in repos_to_check:
Expand Down
38 changes: 29 additions & 9 deletions github/branches/validate_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,41 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

from typing import List
from typing import Any, List, Optional, Tuple
from dataclasses import dataclass

from .retrieve_github_data import RepoBranchProtections, BranchProtectionRule


@dataclass
class Criteria:
standard_number: str # as defined in messages file. alpha-numeric
slug: str # id to match. alpha-numeric
description: str # whatever you want

@staticmethod
def idfn(val: Any) -> Optional[str]:
""" provide ID for pytest Parametrization
"""
if isinstance(val, (Criteria,)):
return f"{val.standard_number}-{val.slug}"
return None

def __str__(self: Any) -> str:
return f"{self.standard_number} {self.description}"


# define the criteria we care about. Identify each critera with a string that will
# appear in the results.
required_criteria = [
"admins restricted",
Criteria("SOGH001b", "admins", "admins not restricted"),
]
optional_criteria = [
"limited commiters",
Criteria("SOGH001c", "commiters", "allowed commiters not configured"),
# "commit signing", # may not be knowable
]
warning_criteria = [
"rule conflicts",
Criteria("SOGH001d", "conflicts", "Conflict in Protection Rules"),
]


Expand All @@ -35,22 +55,22 @@ def find_applicable_rules(
return result


def meets_criteria(protections: List[BranchProtectionRule], criteria: str) -> bool:
def meets_criteria(protections: List[BranchProtectionRule], criteria: Criteria) -> bool:
met = True
# ugly implementation for now
if criteria == "admins restricted":
if criteria.slug == "admins":
met = all(r.is_admin_enforced for r in protections)
elif criteria == "limited commiters":
elif criteria.slug == "commiters":
met = all(r.push_actor_count > 0 for r in protections)
elif criteria == "rule conflicts":
elif criteria.slug == "conflicts":
met = all(r.rule_conflict_count == 0 for r in protections)
else:
met = False
return met


def validate_branch_protections(
data: RepoBranchProtections, branch: str, criteria: str
data: RepoBranchProtections, branch: str, criteria: Criteria,
) -> List[str]:
"""
Validate the protections
Expand Down
32 changes: 32 additions & 0 deletions github/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Classes required by Frost
# 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 https://mozilla.org/MPL/2.0/.
from typing import Optional


class GitHubClient:

_debug_calls: bool = False
_offline: bool = False

@classmethod
def debug_calls(cls) -> bool:
return cls._debug_calls

@classmethod
def is_offline(cls) -> bool:
return cls._offline

@classmethod
def update(cls, debug_calls: Optional[bool] = None, offline: Optional[bool] = None):
# allow updates
if debug_calls:
cls._debug_calls = debug_calls
if offline:
cls._offline = offline

def __init__(
self, debug_calls: Optional[bool] = None, offline: Optional[bool] = None
):
self.update(debug_calls, offline)
Loading

0 comments on commit 466084a

Please sign in to comment.