oncall-engine/tools/migrators/lib/grafana/transform.py
Bob Cotton 0e1dcd2e71
Service to service model migration (#5485)
# What this PR does

Adds Service and Business Service migration to the Pager Duty Migrator.

To test, in addition to the OnCall configs, you need to crate a Grafana
Service Account with `Admin` permission and generate a token. You will
set `GRAFANA_SERVICE_ACCOUNT_URL`, per the README, to
`https://<namespace>:<token>@<server>` The namespace is the stack id, in
the format of `stacks-<stack id>`

Service migration is configurable, filterable, and idempotent.

## Which issue(s) this PR closes

Related to [issue link here]

<!--
*Note*: If you want the issue to be auto-closed once the PR is merged,
change "Related to" to "Closes" in the line above.
If you have more than one GitHub issue that this PR closes, be sure to
preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.

---------

Co-authored-by: Joey Orlando <joey.orlando@grafana.com>
Co-authored-by: GitHub Actions <actions@github.com>
Co-authored-by: grafana-irm-app[bot] <165293418+grafana-irm-app[bot]@users.noreply.github.com>
Co-authored-by: Joey Orlando <joseph.t.orlando@gmail.com>
2025-03-15 21:07:59 -04:00

117 lines
3.9 KiB
Python

"""
Transformation logic for converting PagerDuty services to Grafana Service Model format.
This module provides functions to transform PagerDuty technical and business services
into the Backstage Catalog format used by Grafana's Service Model.
"""
from typing import Any, Dict, List, Union
from lib.pagerduty.resources.business_service import BusinessService
from lib.pagerduty.resources.services import TechnicalService
def transform_service(
service: Union[TechnicalService, BusinessService]
) -> Dict[str, Any]:
"""
Transform a PagerDuty service (technical or business) into a Backstage Component.
Args:
service: The PagerDuty service to transform (either TechnicalService or BusinessService)
Returns:
A dictionary containing the transformed service in Backstage Component format
"""
# Determine service type and required fields
is_technical = isinstance(service, TechnicalService)
service_type = "service" if is_technical else "business_service"
# Create the base component structure
component = {
"apiVersion": "servicemodel.ext.grafana.com/v1alpha1",
"kind": "Component",
"metadata": {
"name": service.name.lower().replace(
" ", "-"
), # Convert to k8s-friendly name
"annotations": {"pagerduty.com/service-id": service.id},
},
"spec": {"type": service_type, "description": service.description},
}
# Add status annotation for technical services
if is_technical and hasattr(service, "status"):
component["metadata"]["annotations"]["pagerduty.com/status"] = service.status
# Add PagerDuty URLs to annotations
if service.html_url:
component["metadata"]["annotations"][
"pagerduty.com/html-url"
] = service.html_url
if service.self_url:
component["metadata"]["annotations"]["pagerduty.com/api-url"] = service.self_url
return component
def validate_component(component: Dict[str, Any]) -> List[str]:
"""
Validate a transformed Component resource.
Args:
component: The Component resource to validate
Returns:
List of validation errors. Empty list means valid.
"""
errors = []
# Check required fields
required_fields = [
("apiVersion", str),
("kind", str),
("metadata", dict),
("spec", dict),
]
for field, field_type in required_fields:
if field not in component:
errors.append(f"Missing required field: {field}")
elif not isinstance(component[field], field_type):
errors.append(f"Field {field} must be of type {field_type.__name__}")
# If we're missing required fields, don't continue with deeper validation
if errors:
return errors
# Check metadata requirements
metadata = component["metadata"]
if "name" not in metadata:
errors.append("metadata.name is required")
elif not isinstance(metadata["name"], str):
errors.append("metadata.name must be a string")
# Check required annotations
if "annotations" not in metadata:
errors.append("metadata.annotations is required")
else:
annotations = metadata["annotations"]
if "pagerduty.com/service-id" not in annotations:
errors.append("Required annotation missing: pagerduty.com/service-id")
if (
component["spec"]["type"] == "service"
and "pagerduty.com/status" not in annotations
):
errors.append("Required annotation missing: pagerduty.com/status")
# Check spec requirements
spec = component["spec"]
if "type" not in spec:
errors.append("spec.type is required")
elif not isinstance(spec["type"], str):
errors.append("spec.type must be a string")
elif spec["type"] not in ["service", "business_service"]:
errors.append("spec.type must be either 'service' or 'business_service'")
return errors