Policy
The lunar_policy
Python package provides utilities for working with Lunar policies, allowing you to load, query, and make assertions about component metadata, such as the component JSON.
For the reference documentation check out:
Installation
The package is available through pip:
pip install lunar-policy
Policy environment
Earthly Lunar executes policies in an environment set up with the following variables:
LUNAR_HUB_HOST
: The host of the Lunar Hub.LUNAR_HUB_INSECURE
: Whether to skip SSL verification of the Lunar Hub.LUNAR_POLICY_NAME
: The name of the policy being executed.LUNAR_INITIATIVE_NAME
: The name of the initiative the policy belongs to.LUNAR_POLICY_OWNER
: The owner of the policy.LUNAR_COMPONENT_ID
: The ID of the component being checked ingithub.com/.../...
format.LUNAR_COMPONENT_DOMAIN
: The domain of the component.LUNAR_COMPONENT_OWNER
: The owner of the component.LUNAR_COMPONENT_PR
: The PR number of the component, if applicable.LUNAR_COMPONENT_GIT_SHA
: The Git SHA of the component that the policy is being executed for.LUNAR_COMPONENT_TAGS
: The tags of the component.LUNAR_COMPONENT_META
: The metadata of the component as a JSON object.LUNAR_BUNDLE_PATH
: Used internally by Lunar to pass in the component JSON and the deltas to the policy.Any secrets set in the Lunar Hub for the policy, via
HUB_POLICY_SECRETS=<name>=<value>;...
.
Core Components
The Policy SDK provides several key classes to help you write policies:
Check
The Check
class provides a fluent interface for making assertions about policy data. It tracks accessed data within the component JSON for traceability purposes.
from lunar_policy import Check, Path
# Create a check with a name and optional description
with Check("my-check", "Validates important properties") as check:
# Get data using JSONPath
value = check.get(".path.to.data")
# Make assertions
check.assert_equals(Path(".api.endpoints[0].method"), "GET")
check.assert_greater_or_equal(value, 50)
check.assert_contains(Path(".api.endpoints[0].path"), "/")
๐ For detailed reference, see the Check reference documentation.
Path
The Path
class is used to pass JSONPath expressions to assertion methods of Check.
๐ See the Path reference documentation.
NoDataError
The NoDataError
exception is used to indicate that required data for a policy check is still pending (hasn't been collected yet).
๐ For detailed reference, see the NoDataError reference documentation.
ComponentData
The ComponentData
class is used to initialize a Check
instance with component metadata from different sources. This can be useful for unit testing policies.
๐ For detailed reference, see the ComponentData reference documentation.
Automatic Data Loading
When a policy is executed through Lunar Hub, Lunar passes in the relevant context via LUNAR_BUNDLE_PATH
. This context includes the component JSON for the component that is being checked. When you create a Check
instance, the library automatically loads this data under the hood when data
is not provided.
Executing Policies Locally
To execute a policy locally for testing purposes, you can use the lunar
CLI.
lunar policy dev --component-json path/to/component.json ./path/to/policy.py
If you would like to use the real component JSON of one of your components, you can do so via the command:
lunar policy dev --component github.com/my-org/my-repo ./path/to/policy.py
Check outcomes
Checks can result in the following possible outcomes:
PASS
: The check passed successfully. All assertions within the check were satisfied.FAIL
: The check failed. One or more assertions within the check were not satisfied. This generally means that the developer working on the component needs to fix the issue.NO_DATA
: The check could not be completed due to data not being available yet. This is due collection not having finished yet (code or cron collectors are still running, or the CI is still running).ERROR
: The check encountered an error during execution. This indicates an unexpected error occurred during the check execution, such as a runtime exception. This is generally a bug either in the collection logic or in the policy code.
Handling Missing Data
Lunar makes a clear distinction between temporarily missing component JSON data (data is pending), permanently missing component JSON data, and failing component JSON data. This is needed under the hood to be able to provide partial policy results while collectors are still running. Policies that have enough data will provide accurate results, while policies that don't have enough data will often report a pending
status.
Here is a breakdown of how the different statuses are determined:
Temporarily missing data (
pending
status): This is when some or all of the component JSON data required for assertions is not present, but the collector are still running. In this case, the check will report apending
status for now, thanks to methods likeget
,Path
,exists
andassert_exists
automatically raisingNoDataError
when not enough data is available and collectors are still running.Permanently missing data - sometimes expected on failures (
fail
status): This is when some of the component JSON data required for assertions is not present, and the collectors have finished running.assert_exists
will report a failure in this case.Permanently missing data - unexpected (
error
status): This is when some of the component JSON data required for assertions is not present, and the collectors have finished running.get
orPath
will report aValueError
after collectors finished, resulting in anerror
status.Failing data (
fail
status): This is when the data is present, but the assertions (e.g.assert_equals
,assert_true
, etc.) fail.
This means that you should use get
or Path
to access data when you are assuming that the data will eventually be provided by a collector. Or use assert_exists
(or exists
within an if condition) to test for the existence of data if that's what the pass/fail outcome of the check should depend on.
Best Practices for Handling Missing Data
Below are examples demonstrating different approaches to handling missing data in policies:
Bad Approach: Blindly Assuming Data Exists
This approach incorrectly assumes all fields exist after retrieving an object, which can lead to errors when data is temporarily missing:
with Check("api-security-check") as check:
# Bad: Gets the entire API object once and assumes all fields exist
api = check.get(".api")
# These will cause policy errors if api is None or missing expected fields
check.assert_true(api["requires_auth"], "API should require authentication")
rate_limit = api["rate_limit"]
check.assert_equals(rate_limit, 100, f"API rate limit should be 100, but found {rate_limit}")
check.assert_contains(api["security_headers"], "Content-Security-Policy")
Good Approach: Using Path and get for Automatic NoDataError Handling
This approach uses targeted field access and relies on the Check API to handle missing data automatically:
with Check("api-security-check") as check:
# These assertions automatically raise NoDataError if paths don't exist yet:
check.assert_true(Path(".api.requires_auth"), "API should require authentication")
# get also raises NoDataError if the path doesn't exist yet
rate_limit = check.get(".api.rate_limit")
check.assert_equals(rate_limit, 100, f"API rate limit should be 100, but found {rate_limit}")
check.assert_contains(Path(".api.security_headers"), "Content-Security-Policy")
Good Approach, when needed: Using assert_exists or exists
In some cases, the test itself might be about the very existence of data. In this case, you can use assert_exists
to test for that.
with Check("api-security-check") as check:
check.assert_exists(".api.requires_auth", "API is missing requires_auth field")
The function assert_exists
raises NoDataError
(results in pending
status) before collectors finished, and fails the check if the data is missing after collectors finished.
Another option is to rely on exists
within an if condition.
with Check("api-security-check") as check:
if check.exists(".api"):
check.assert_true(Path(".api.requires_auth"), "API should require authentication")
In this case, if the the .api
field is missing the assertion is not executed.
Writing Unit Tests for Policies
Let's assume that we have the following policy:
from lunar_policy import Check, ComponentData, Path
def verify_readme(data=None):
check = Check("readme-long-enough", data=data)
with check:
lines = check.get('.readme.lines')
check.assert_greater_or_equal(
lines, 50,
f'README.md should have at least 50 lines. Current count: {lines}'
)
return check
if __name__ == "__main__":
verify_readme()
Here's an example showing how to write unit tests:
import unittest
from lunar_policy import Check, Path, ComponentData, CheckStatus
class TestReadmePolicy(unittest.TestCase):
def test_not_long_enough(self):
component_json = {
"readme": {
"lines": 49,
"missing": False
}
}
data = ComponentData.from_component_json(component_json)
check = verify_readme(data)
# Verify the check failed because the README isn't long enough
self.assertEqual(check.status, CheckStatus.FAIL)
self.assertEqual(check.failure_reasons[0], "README.md should have at least 50 lines. Current count: 49")
if __name__ == "__main__":
unittest.main()
You can run the test with:
python -m unittest test_readme_policy.py
Last updated