Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add silence datasource #403

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions changelogs/fragments/605-silence-datastore-feature.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- Add the ability to define which alertmanager datasource to target in grafana_silence
54 changes: 45 additions & 9 deletions plugins/modules/grafana_silence.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@
type: list
elements: dict
required: true
alertmanager_datasource:
description:
- Which alertmanager datasource to target
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Which alertmanager datasource to target
- Which alertmanager datasource to target.

type: str
required: false
default: grafana
state:
description:
- Delete the first occurrence of a silence with the same settings. Can be "absent" or "present".
Expand Down Expand Up @@ -104,6 +110,22 @@
value: test
state: present

- name: Create a silence against a specific datasource
community.grafana.grafana_silence:
grafana_url: "https://grafana.example.com"
grafana_api_key: "{{ some_api_token_value }}"
comment: "a testcomment"
created_by: "me"
starts_at: "2029-07-29T08:45:45.000Z"
ends_at: "2029-07-29T08:55:45.000Z"
matchers:
- isEqual: true
isRegex: true
name: environment
value: test
alertmanager_datasource: exampleDS
state: present

- name: Delete a silence
community.grafana.grafana_silence:
grafana_url: "https://grafana.example.com"
Expand Down Expand Up @@ -196,6 +218,8 @@
self._module = module
self.grafana_url = base.clean_url(module.params.get("url"))
self.org_id = None
# Default here because you can't look up "grafana" DS via API
self.alertmanager_path = "grafana"
Comment on lines +221 to +222
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Default here because you can't look up "grafana" DS via API
self.alertmanager_path = "grafana"
self.alertmanager_id = module.params["alertmanager_datasource"]

# {{{ Authentication header
self.headers = {"Content-Type": "application/json"}
if module.params.get("grafana_api_key", None):
Expand Down Expand Up @@ -225,6 +249,11 @@
msg="Silences API is available starting with Grafana v8",
)

if module.params.get("alertmanager_datasource", None):
self.alertmanager_path = self.datasource_by_name(

Check warning on line 253 in plugins/modules/grafana_silence.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/grafana_silence.py#L253

Added line #L253 was not covered by tests
module.params["alertmanager_datasource"]
)
Comment on lines +252 to +255
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if module.params.get("alertmanager_datasource", None):
self.alertmanager_path = self.datasource_by_name(
module.params["alertmanager_datasource"]
)
if module.params.get("alertmanager_datasource") != "grafana":
self.alertmanager_id = self.datasource_by_name(
module.params["alertmanager_datasource"]
)


def _send_request(self, url, data=None, headers=None, method="GET"):
if data is not None:
data = json.dumps(data)
Expand Down Expand Up @@ -268,6 +297,16 @@
failed=True, msg="Current user isn't member of organization: %s" % org_name
)

def datasource_by_name(self, datasource_name):
url = f"/api/datasources/name/{datasource_name}"
datasource = self._send_request(url, headers=self.headers, method="GET")

Check warning on line 302 in plugins/modules/grafana_silence.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/grafana_silence.py#L301-L302

Added lines #L301 - L302 were not covered by tests
if datasource:
return datasource["uid"]

Check warning on line 304 in plugins/modules/grafana_silence.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/grafana_silence.py#L304

Added line #L304 was not covered by tests

return self._module.fail_json(

Check warning on line 306 in plugins/modules/grafana_silence.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/grafana_silence.py#L306

Added line #L306 was not covered by tests
failed=True, msg=f"Datasource not found: {datasource_name}"
)

def get_version(self):
url = "/api/health"
response = self._send_request(
Expand All @@ -280,7 +319,7 @@
raise GrafanaError("Failed to retrieve version from '%s'" % url)

def create_silence(self, comment, created_by, starts_at, ends_at, matchers):
url = "/api/alertmanager/grafana/api/v2/silences"
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences"
url = f"/api/alertmanager/{self.alertmanager_id}/api/v2/silences"

silence = dict(
comment=comment,
createdBy=created_by,
Expand All @@ -297,7 +336,7 @@
return response

def get_silence(self, comment, created_by, starts_at, ends_at, matchers):
url = "/api/alertmanager/grafana/api/v2/silences"
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences"
url = f"/api/alertmanager/{self.alertmanager_id}/api/v2/silences"


responses = self._send_request(url, headers=self.headers, method="GET")

Expand All @@ -313,21 +352,17 @@
return None

def get_silence_by_id(self, silence_id):
url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format(
SilenceId=silence_id
)
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silence/{silence_id}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silence/{silence_id}"
url = f"/api/alertmanager/{self.alertmanager_id}/api/v2/silence/{silence_id}"

response = self._send_request(url, headers=self.headers, method="GET")
return response

def get_silences(self):
url = "/api/alertmanager/grafana/api/v2/silences"
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences"

Check warning on line 360 in plugins/modules/grafana_silence.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/grafana_silence.py#L360

Added line #L360 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silences"
url = f"/api/alertmanager/{self.alertmanager_id}/api/v2/silences"

response = self._send_request(url, headers=self.headers, method="GET")
return response

def delete_silence(self, silence_id):
url = "/api/alertmanager/grafana/api/v2/silence/{SilenceId}".format(
SilenceId=silence_id
)
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silence/{silence_id}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
url = f"/api/alertmanager/{self.alertmanager_path}/api/v2/silence/{silence_id}"
url = f"/api/alertmanager/{self.alertmanager_id}/api/v2/silence/{silence_id}"

response = self._send_request(url, headers=self.headers, method="DELETE")
return response

Expand All @@ -352,6 +387,7 @@
org_name=dict(type="str"),
skip_version_check=dict(type="bool", default=False),
starts_at=dict(type="str", required=True),
alertmanager_datasource=dict(type=str),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
alertmanager_datasource=dict(type=str),
alertmanager_datasource=dict(type=str, default="grafana"),

state=dict(type="str", choices=["present", "absent"], default="present"),
)

Expand Down
148 changes: 148 additions & 0 deletions tests/unit/modules/grafana/grafana_silence/test_grafana_silence.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ def get_version_resp():
return {"major": 10, "minor": 0, "rev": 0}


def get_datasource_resp():
server_response = json.dumps({"uid": "AB981B38A76F"})
return (MockedReponse(server_response), {"status": 200})


class GrafanaSilenceTest(TestCase):
def setUp(self):
self.authorization = basic_auth_header("admin", "changeme")
Expand Down Expand Up @@ -164,6 +169,96 @@ def test_create_silence_new_silence(
)
self.assertEqual(result, {"silenceID": "470b7116-8f06-4bb6-9e6c-6258aa92218e"})

# create a new silence with alertmanager datasource defined
@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name"
)
@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_silence"
)
@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version"
)
@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url"
)
def test_create_silence_new_silence_with_datasource(
self,
mock_fetch_url,
mock_get_version,
mock_get_silence,
mock_datasource_by_name,
):
set_module_args(
{
"url": "https://grafana.example.com",
"url_username": "admin",
"url_password": "changeme",
"comment": "a testcomment",
"created_by": "me",
"starts_at": "2029-07-29T08:45:45.000Z",
"ends_at": "2029-07-29T08:55:45.000Z",
"alertmanager_datasource": "testds",
"matchers": [
{
"isEqual": True,
"isRegex": True,
"name": "environment",
"value": "test",
}
],
"state": "present",
}
)
module = grafana_silence.setup_module_object()
mock_get_version.return_value = get_version_resp()
mock_datasource_by_name.return_value = get_datasource_resp()
mock_fetch_url.return_value = silence_created_resp()
mock_get_silence.return_value = silence_get_resp()

grafana_iface = grafana_silence.GrafanaSilenceInterface(module)
result = grafana_iface.create_silence(
"a testcomment",
"me",
"2029-07-29T08:45:45.000Z",
"2029-07-29T08:55:45.000Z",
[
{
"isEqual": True,
"isRegex": True,
"name": "environment",
"value": "test",
}
],
)
mock_fetch_url.assert_called_with(
module,
"https://grafana.example.com/api/alertmanager/AB981B38A76F/api/v2/silences",
data=json.dumps(
{
"comment": "a testcomment",
"createdBy": "me",
"startsAt": "2029-07-29T08:45:45.000Z",
"endsAt": "2029-07-29T08:55:45.000Z",
"matchers": [
{
"isEqual": True,
"isRegex": True,
"name": "environment",
"value": "test",
}
],
},
sort_keys=True,
),
headers={
"Content-Type": "application/json",
"Authorization": self.authorization,
},
method="POST",
)
self.assertEquals(result, {"silenceID": "470b7116-8f06-4bb6-9e6c-6258aa92218e"})

@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version"
)
Expand Down Expand Up @@ -208,4 +303,57 @@ def test_delete_silence(self, mock_fetch_url, mock_get_version):
},
method="DELETE",
)
self.assertEquals(result, {"message": "silence deleted"})

@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.datasource_by_name"
)
@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.GrafanaSilenceInterface.get_version"
)
@patch(
"ansible_collections.community.grafana.plugins.modules.grafana_silence.fetch_url"
)
def test_delete_silence_with_datasource(
self, mock_fetch_url, mock_get_version, mock_datasource_by_name
):
set_module_args(
{
"url": "https://grafana.example.com",
"url_username": "admin",
"url_password": "changeme",
"comment": "a testcomment",
"created_by": "me",
"ends_at": "2029-07-29T08:55:45.000Z",
"alertmanager_datasource": "testds",
"matchers": [
{
"isEqual": True,
"isRegex": True,
"name": "environment",
"value": "test",
}
],
"starts_at": "2029-07-29T08:45:45.000Z",
"state": "present",
}
)
module = grafana_silence.setup_module_object()
mock_fetch_url.return_value = silence_deleted_resp()
mock_get_version.return_value = get_version_resp()
mock_datasource_by_name.return_value = get_datasource_resp()

grafana_iface = grafana_silence.GrafanaSilenceInterface(module)
silence_id = "470b7116-8f06-4bb6-9e6c-6258aa92218e"
result = grafana_iface.delete_silence(silence_id)
mock_fetch_url.assert_called_with(
module,
"https://grafana.example.com/api/alertmanager/AB981B38A76F/api/v2/silence/470b7116-8f06-4bb6-9e6c-6258aa92218e",
data=None,
headers={
"Content-Type": "application/json",
"Authorization": self.authorization,
},
method="DELETE",
)
self.assertEqual(result, {"message": "silence deleted"})
Loading