Skip to content

Commit

Permalink
write up some docs
Browse files Browse the repository at this point in the history
  • Loading branch information
OnGle committed Feb 16, 2023
1 parent 51c2dac commit 7af425c
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ tkldev-detective is a simple framework for linting turnkeylinux appliances, it
leverages existing linting tools, as well as allowing for custom handmade lints
and provides a unified output format/interface for utilizing these tools.

Dependencies
~~~~~~~~~~~~

tkldev-detective has no mandatory dependencies

Currently though there is 1 optional dependency:

pyyaml - enables yaml lint

Installation
------------

tkldev-detective can be installed as follows

1. Clone somewhere, I recommend cloning into ``/turnkey/public`` (creating
intermediate directories as required)

.. code-block:: bash
mkdir -p /turnkey/public
git clone [email protected]:turnkeylinux/tkldev-detective /turnkey/public/tkldev-detective
2. Add ``tkldev-detective`` to your path:

.. code-block:: bash
ln -s /turnkey/public/tkldev-detective/tkldev-detective /usr/local/bin
Usage
-----

Using ``tkldev-detective`` is as simple as running
``tkldev-detective lint <appliance>`` where ``<appliance>`` can be the name of
an appliance or the path to an appliance, provided you have the appliance
build-code present on your tkldev.

E.g.

``tkldev-detective lint zoneminder``

For more information on how it works and how to develop more functionality, see
`overview`_ and `custom modules`_

Copyright
---------

Expand All @@ -15,3 +58,6 @@ tkldev-detective is free software: you can redistribute it and/or modify it unde
tkldev-detective is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with tkldev-detective. If not, see <https://www.gnu.org/licenses/>.

.. _overview: ./docs/overview.rst
.. _custom modules: ./docs/custom_modules.rst
171 changes: 171 additions & 0 deletions docs/custom_modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
TKLDev-Detective Custom Modules
===============================

Custom modules are simply python files, with 1 or more class definitions that
subclass ``FileClassifier`` or ``FileLinter`` residing inside of the
``tkldet_modules`` directory.

(Non-file classifiers and linters might be possible but are not yet
officially supported)

XXX ReportFilters are also possible to subclass once fully implemented

Common Notes
------------

One thing that all custom operational models share is a concept of "weight"
which is used to determine the order that each check is done in.

By default all of these classes contain a class-variable ``WEIGHT: int``.

Counter-intuitively classes are used in ascending order of weight, the
recommended weight for situations that do not require specific ordering is
``100``.

Custom Classifiers
------------------

Classifiers are very simple by design, here's a very simple example of a
classifier which tags directories, it's only job is to take an ``Item``
and if it refers to a directory, tag it with a relevant tag.

.. code-block:: python3
from libtkldet.classifier import FileClassifier, FileItem, register_classifier
from os.path import isdir
@register_classifier # this line registers our custom class to be used as a classifier
class DirectoryClassifier(FileClassifier):
WEIGHT: int = 80
# we want this to run earlier than most classifier,
# but not necessarily first so we reduce the weight by 20
def classify(self, item: FileItem):
# classify is the only method you MUST implement when deriving from
# FileClassifier, it does not return anything
if isdir(item):
item.add_tags(self, ['directory'])
# we pass a reference to ourselves, so the item can keep track
# of which tags came from which classifiers
#
# we also pass a list of as many tags as we wish to add to this
# item. The exact value of our tag is unimportant, only that it
# doesn't conflict with any other tags.
See ``filetype.py``, ``shebang.py`` for more examples of custom classifiers.

Helper Classifiers
~~~~~~~~~~~~~~~~~~

There are currently 2 helper classifiers, ``ExactPathClassifier`` and
``SubdirClassifier``.

ExactPathClassifier classifies only a single file item with a specific path, to
use it you simply need to set a few required class variables:

.. code-block:: python3
from libtkldet.classifier import ExactPathClassifier, register_classifier
@register_classifier
class CustomExactPathClassifier(ExactPathClassifier):
path: str = "path/to/some/file"
# exact location of file, relative to appliance root
tags: list[str] = [ "tag1", "tag2" ]
# all these tags are added to the specific file matched
SubdirClassifier is only marginally more complex, it will tag all files that are
directory descendants of the given path, optionally recursively.

.. code-block:: python3
from libtkldet.classifier import SubdirClassifier, register_classifier
@register_classifier
class CustomSubdirClassifier(SubdirClassifier):
path: str = "path/to/some/file"
# exact location of parent directory, relative to appliance root
tags: list[str] = [ "tag1", "tag2" ]
# all these tags are added to children of directory
recursive: bool = True
# recurse into child directories?
Too see examples of these helper classifiers see the ``appliance_files.py``
module.

Custom Linters
--------------

Linters are a little more complex, but ultimately quite similar.

Here's an example of a linter that produces an info report for every line
that contains "TODO"

.. code-block:: python3
from libtkldet.linter import FileLinter, register_linter
from libtkldet.report import FileReport, ReportLevel
from typing import Generator
@register_linter
class TodoLinter(FileLinter):
ENABLE_TAGS: set[str] = set()
DISABLE_TAGS: set[str] = set()
# by default, linter will only run when either:
#
# 1. ENABLE_TAGS is empty and there is no overlap between item tags and
# DISABLE_TAGS
# 2. ENABLE_TAGS is not-empty, at least 1 tag from ENABLE_TAGS is
# included in the given item, and no items from DISABLE_TAGS are
# present
WEIGHT: int = 100
# weight here works the same as classifiers
def check(self, item: Item) -> Generator[Report, None, None]:
# note this is a generator, we "yield" reports
with open(item.abspath) as fob:
for i, line in enumerate(fob):
if 'TODO' in line:
yield FileReport(
item=item, # same item as input
line=i,
column=line.find('TODO'),
# both line or column can be a single integer, a
# tuple of integers or ommited entirely. However
# if line number is ommited, column should be too
location_metadata=None,
# currently populated by meta info that doessn't
# fit elseware, not output currently
message = 'Found todo NOTE: " + line.split('TODO', 1)[1],
# the message shown to user
fix = None,
# suggested fix as a string if known/relevant
# otherwise None
source = 'TodoLinter',
# arbitrary string to represent the linter that
# found this issue
level = ReportLevel.INFO
# kind of report level, 1 for all the standard log
# levels as well as "CONVENTION" and "REFACTOR"
# (possibly more as time goes on)
)
If the logic surrounding ``ENABLE_TAGS`` and ``DISABLE_TAGS`` is insufficient to
determine if the linter should run you can override ``Linter.should_check``
method which actually performs those checks it takes the ``Item`` as an argument
and returns a boolean indicating if the linter should check the item.
68 changes: 68 additions & 0 deletions docs/overview.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
TKLDev-Detective Overview
=========================

tkldev-detective is actually pretty simple, it boils down to 6 concepts, 2 data
models and 4 "operational" models which function in serial.

Data models
-----------

Item
An item is just a piece of unspecified data accompanied by a set of tags.

Subtypes of item are used to distinguish between concrete concepts,
currently the only type of ``Item`` supported is ``FileItem`` which (as you
might assume) contains a path as it's data.

Report
A report is the final piece of data to be shown to the user. It contains
various pieces of information about a potential issue, how serious it is,
information relating to where it is, what potential fixes might be
available and what linter found this issue.

Just like Item's this type is intended to be subclassed for other types of
data, and just like ``FileItem``; ``FileReport`` is currently the only such
subclass. It provides sufficient information to show file snippets related
to issues in a variety of ways contextual to how much information it's given
about the location of the issue inside the file.

Operational models
------------------

Locator
the "Locator" determines which files are checked in the first place
in the future this will provide a broader concept of "checkable thing" than
just file, for now though we can safely only consider files.

The locator produces "Items", these are a model that contains some kind of
"data" (here because they are just files, the data is the path itself)
and can be tagged.

Classifier
the "Classifier" determines various qualities about a certain file, these
qualities or classifications are referred to internally as "tags".

An example of a tag might be ``shebang:/usr/bin/python3`` which asserts that
this file has a shebang, and that the shebang is for python3. Note, the use
of ``shebang:`` prefix as there is infinite variation in shebangs.

Some tags though have no variation, such is the case for
``appliance-makefile`` which is only set for the top-level ``Makefile``
found in an appliance.

Linter
the "Linter" performs checks on the inputted "items" and yields any number
of "Reports". Linter's are the most diverse of these groups.

Examples of linters go from ``PyLinter`` and ``Shellcheck``, linter's which
wrap the 3rd-party linters "pylint" and "shellcheck" respectively, all the
way through ``JsonLint`` which just loads json files and yields a report if
loading fails.

ReportFilter
the "ReportFilter" is the last stop for reports before they are presented
to the user. A ``ReportFilter`` is mostly intended to remove false
positives but can also be used to add additional context to reports in a
way that might not make sense in the linter itself.

XXX: currently report filters are untested

0 comments on commit 7af425c

Please sign in to comment.