Skip to content

Commit 3f380eb

Browse files
authored
flagsmith_tags :: init (#11)
* flagsmith_tags :: init * flagsmith_tag :: add doc * flagsmith_feature :: init * flagsmith_feature :: add doc * readme :: add flagsmith modules * flagsmith_feature :: manage json initial_value
1 parent 515e8f4 commit 3f380eb

File tree

9 files changed

+654
-6
lines changed

9 files changed

+654
-6
lines changed

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ This Ansible collection includes a variety of Ansible content to help automate t
66

77
### Modules
88

9-
| Name | Description |
10-
|--------------------------------------------------------|--------------------------------------------------|
11-
| toucantoco.toucantoco.betteruptime_monitor | Create & manage betteruptime monitors |
12-
| toucantoco.toucantoco.betteruptime_monitor_sla | Retrieve SLA of monitors |
13-
| toucantoco.toucantoco.betteruptime_status_page | Create & manage betteruptime status pages |
14-
| toucantoco.toucantoco.betteruptime_status_page_report | Create & manage betteruptime status page reports |
9+
| Name | Description |
10+
| -------------------------------------------------------- | -------------------------------------------------- |
11+
| toucantoco.toucantoco.betteruptime_monitor | Create & manage betteruptime monitors |
12+
| toucantoco.toucantoco.betteruptime_monitor_sla | Retrieve SLA of monitors |
13+
| toucantoco.toucantoco.betteruptime_status_page | Create & manage betteruptime status pages |
14+
| toucantoco.toucantoco.betteruptime_status_page_report | Create & manage betteruptime status page reports |
15+
| toucantoco.toucantoco.flagsmith_feature | Create & manage Flagsmith features |
16+
| toucantoco.toucantoco.flagsmith_tag | Create & manage Flagsmith tags |
17+
1518

1619
### Installing this collection
1720

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# toucantoco.toucantoco.flagsmith_feature
2+
3+
### Purpose
4+
Manage Flagsmith features
5+
6+
### Parameters
7+
| Parameters | Required | Type | Choices/Default | Comments |
8+
| ----------------- | ---------- | ----------- | ----------------- | ----------------- |
9+
| api_key | True | str | | |
10+
| base_url | True | str | | Base URL of the API |
11+
| state | True | str | present/absent | |
12+
| project_name | True | str | | |
13+
| name | True | str | | |
14+
| type | False | str | | |
15+
| default_enabled | False | bool | | |
16+
| initial_value | False | str | | |
17+
| description | False | str | | |
18+
| is_archived | False | bool | | |
19+
| tags | False | List of str | | |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# toucantoco.toucantoco.flagsmith_tag
2+
3+
### Purpose
4+
Manage Flagsmith tags
5+
6+
### Parameters
7+
| Parameters | Required | Type | Choices/Default | Comments |
8+
|--------------|----------|------|-----------------|---------------------|
9+
| api_key | True | str | | |
10+
| base_url | True | str | | Base URL of the API |
11+
| state | True | str | present/absent | |
12+
| project_name | True | str | | |
13+
| label | True | str | | |
14+
| color | False | str | | |
15+
| description | False | str | | |

plugins/module_utils/flagsmith.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import requests
2+
3+
def get_project_ids_from_names(base_url: str, headers: dict, projects_names: list) -> list:
4+
""" Return the ids of the matching projects"""
5+
response = requests.get(f"{base_url}/projects/", headers=headers)
6+
if response.status_code != 200:
7+
return []
8+
json_object = response.json()
9+
return [i['id'] for i in json_object if i['name'] in projects_names]
10+
11+
def get_tag_ids_from_labels(url: str, headers: dict, project_id: int, tags_labels: list) -> list:
12+
""" Return the ids of the matching tags"""
13+
response = requests.get(url, headers=headers)
14+
if response.status_code != 200:
15+
return []
16+
17+
json_object = response.json()
18+
ids = [i['id'] for i in json_object['results'] if i['label'] in tags_labels]
19+
20+
if len(ids) != len(tags_labels) and json_object['next'] is not None:
21+
ids = ids + get_tag_ids_from_labels(json_object['next'], headers, project_id, tags_labels)
22+
23+
return ids
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/python
2+
3+
from http import HTTPStatus
4+
5+
import requests
6+
from ansible.module_utils.basic import AnsibleModule
7+
8+
from ..module_utils.payload import sanitize_payload
9+
from ..module_utils.flagsmith import get_project_ids_from_names, get_tag_ids_from_labels
10+
11+
import collections
12+
13+
import ast
14+
15+
import json
16+
17+
TAG_FIELDS = {
18+
"api_key": {"required": True, "type": "str", "no_log": True},
19+
"base_url": {"required": True, "type": "str"},
20+
"state": {"required": True, "choices": ["present", "absent"], "type": "str"},
21+
"project_name": {"required": True, "type": "str"},
22+
"name": {"required": True, "type": "str"},
23+
"type": {"required": False, "type": "str"},
24+
"default_enabled": {"required": False, "type": "bool"},
25+
"initial_value": {"required": False, "type": "str"},
26+
"description": {"required": False, "type": "str"},
27+
"is_archived": {"required": False, "type": "bool"},
28+
"tags": {"required": False, "type": "list", "elements": "str"},
29+
}
30+
31+
class FlagsmithFeature:
32+
def __init__(self, module):
33+
self.module = module
34+
self.payload = module.params
35+
self.api_key = self.payload.pop("api_key")
36+
self.base_url = self.payload.pop("base_url")
37+
self.project_name = self.payload.pop("project_name")
38+
self.state = self.payload.pop("state")
39+
self.headers = {"Authorization": f"Token {self.api_key}", "Accept": "application/json"}
40+
self.id = None
41+
self.project_id = None
42+
self.retrieved_attributes = None
43+
44+
self.payload = sanitize_payload(self.payload)
45+
46+
if 'initial_value' in self.payload:
47+
# Transform to a valid json string if initial_value is a dict
48+
try:
49+
typed_initial_value = ast.literal_eval(self.payload['initial_value'])
50+
if type(typed_initial_value) is dict:
51+
self.payload['initial_value'] = json.dumps(typed_initial_value)
52+
except ValueError:
53+
pass
54+
55+
def retrieve_id(self, api_url):
56+
""" Retrieve the id of a feature if it exists """
57+
response = requests.get(api_url, headers=self.headers)
58+
json_object = response.json()
59+
60+
for item in json_object["results"]:
61+
if item["name"] == self.payload["name"]:
62+
self.id = item["id"]
63+
self.retrieved_attributes = {
64+
"name": item["name"],
65+
"type": item["type"],
66+
"default_enabled": item["default_enabled"],
67+
"initial_value": item["initial_value"],
68+
"description": item["description"],
69+
"is_archived": item["is_archived"],
70+
"tags": item["tags"]
71+
}
72+
return
73+
74+
if json_object["next"] is not None:
75+
self.retrieve_id(json_object["next"])
76+
77+
def diff_attributes(self):
78+
""" Update the payload to only have the diff between the wanted and the existing attributes """
79+
diff_attributes = {}
80+
for key in self.payload:
81+
if key == "tags":
82+
if collections.Counter(self.retrieved_attributes[key]) != collections.Counter(self.payload[key]):
83+
# tags is a special case since we are comparing two unordered lists
84+
diff_attributes[key] = self.payload[key]
85+
elif key not in self.retrieved_attributes or self.retrieved_attributes[key] != self.payload[key]:
86+
diff_attributes[key] = self.payload[key]
87+
88+
self.payload = diff_attributes
89+
90+
def create(self):
91+
""" Create a new feature """
92+
if 'tags' in self.payload:
93+
self.payload['tags'] = get_tag_ids_from_labels(f"{self.base_url}/projects/{self.project_id}/tags/", self.headers, self.project_id, self.payload['tags'])
94+
95+
resp = requests.post(f"{self.base_url}/projects/{self.project_id}/features/", headers=self.headers, json=self.payload)
96+
if resp.status_code == HTTPStatus.CREATED:
97+
self.module.exit_json(changed=True)
98+
else:
99+
self.module.fail_json(msg=resp.content)
100+
101+
def update(self):
102+
""" Update an existing feature """
103+
if 'tags' in self.payload:
104+
self.payload['tags'] = get_tag_ids_from_labels(f"{self.base_url}/projects/{self.project_id}/tags/", self.headers, self.project_id, self.payload['tags'])
105+
106+
self.diff_attributes()
107+
if not self.payload:
108+
self.module.exit_json(changed=False)
109+
110+
resp = requests.patch(f"{self.base_url}/projects/{self.project_id}/features/{self.id}/", headers=self.headers, json=self.payload)
111+
112+
if resp.status_code == HTTPStatus.OK:
113+
self.module.exit_json(changed=True)
114+
else:
115+
self.module.fail_json(msg=resp.content)
116+
117+
def delete(self):
118+
""" Delete an existing feature """
119+
resp = requests.delete(f"{self.base_url}/projects/{self.project_id}/features/{self.id}/", headers=self.headers)
120+
if resp.status_code == HTTPStatus.NO_CONTENT:
121+
self.module.exit_json(changed=True)
122+
else:
123+
self.module.fail_json(msg=resp.content)
124+
125+
def manage(self):
126+
""" Manage state of a feature """
127+
project_ids = get_project_ids_from_names(self.base_url, self.headers, [self.project_name])
128+
if len(project_ids) == 0:
129+
self.module.fail_json(msg="Project was not found")
130+
else:
131+
self.project_id = project_ids[0]
132+
133+
self.retrieve_id(f"{self.base_url}/projects/{self.project_id}/features?search={self.payload['name']}")
134+
135+
if self.state == "present":
136+
if not self.id:
137+
self.create()
138+
else:
139+
self.update()
140+
elif self.state == "absent":
141+
if not self.id:
142+
self.module.exit_json(changed=False, msg="No feature to delete")
143+
else:
144+
self.delete()
145+
146+
147+
def main():
148+
module = AnsibleModule(
149+
argument_spec=TAG_FIELDS,
150+
supports_check_mode=True,
151+
)
152+
153+
feature_object = FlagsmithFeature(module)
154+
155+
if not module.check_mode:
156+
feature_object.manage()
157+
else:
158+
return module.exit_json(changed=False)
159+
160+
161+
if __name__ == "__main__":
162+
main()

plugins/modules/flagsmith_tag.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/python
2+
3+
from http import HTTPStatus
4+
5+
import requests
6+
from ansible.module_utils.basic import AnsibleModule
7+
8+
from ..module_utils.payload import sanitize_payload
9+
from ..module_utils.flagsmith import get_project_ids_from_names
10+
11+
import random
12+
13+
TAG_FIELDS = {
14+
"api_key": {"required": True, "type": "str", "no_log": True},
15+
"base_url": {"required": True, "type": "str"},
16+
"state": {"required": True, "choices": ["present", "absent"], "type": "str"},
17+
"project_name": {"required": True, "type": "str"},
18+
"label": {"required": True, "type": "str"},
19+
"color": {"required": False, "type": "str"},
20+
"description": {"required": False, "type": "str"},
21+
}
22+
23+
class FlagsmithTag:
24+
def __init__(self, module):
25+
self.module = module
26+
self.payload = module.params
27+
self.api_key = self.payload.pop("api_key")
28+
self.base_url = self.payload.pop("base_url")
29+
self.project_name = self.payload.pop("project_name")
30+
self.state = self.payload.pop("state")
31+
self.headers = {"Authorization": f"Token {self.api_key}", "Accept": "application/json"}
32+
self.id = None
33+
self.project_id = None
34+
self.retrieved_attributes = None
35+
36+
self.payload = sanitize_payload(self.payload)
37+
38+
def retrieve_id(self, api_url):
39+
""" Retrieve the id of a tag if it exists """
40+
response = requests.get(api_url, headers=self.headers)
41+
json_object = response.json()
42+
43+
for item in json_object["results"]:
44+
if item["label"] == self.payload["label"]:
45+
self.id = item["id"]
46+
self.retrieved_attributes = {
47+
"label": item["label"],
48+
"color": item["color"],
49+
"description": item["description"]
50+
}
51+
return
52+
53+
if json_object["next"] is not None:
54+
self.retrieve_id(json_object["next"])
55+
56+
def diff_attributes(self):
57+
""" Update the payload to only have the diff between the wanted and the existing attributes """
58+
diff_attributes = {}
59+
for key in self.payload:
60+
if key not in self.retrieved_attributes or self.retrieved_attributes[key] != self.payload[key]:
61+
diff_attributes[key] = self.payload[key]
62+
63+
self.payload = diff_attributes
64+
65+
def create(self):
66+
""" Create a new tag """
67+
resp = requests.post(f"{self.base_url}/projects/{self.project_id}/tags/", headers=self.headers, json=self.payload)
68+
if resp.status_code == HTTPStatus.CREATED:
69+
self.module.exit_json(changed=True)
70+
else:
71+
self.module.fail_json(msg=resp.content)
72+
73+
def update(self):
74+
""" Update an existing tag """
75+
self.diff_attributes()
76+
if not self.payload:
77+
self.module.exit_json(changed=False)
78+
79+
resp = requests.patch(f"{self.base_url}/projects/{self.project_id}/tags/{self.id}/", headers=self.headers, json=self.payload)
80+
81+
if resp.status_code == HTTPStatus.OK:
82+
self.module.exit_json(changed=True)
83+
else:
84+
self.module.fail_json(msg=resp.content)
85+
86+
def delete(self):
87+
""" Delete an existing tag """
88+
resp = requests.delete(f"{self.base_url}/projects/{self.project_id}/tags/{self.id}/", headers=self.headers)
89+
if resp.status_code == HTTPStatus.NO_CONTENT:
90+
self.module.exit_json(changed=True)
91+
else:
92+
self.module.fail_json(msg=resp.content)
93+
94+
def manage(self):
95+
""" Manage state of a tag """
96+
project_ids = get_project_ids_from_names(self.base_url, self.headers, [self.project_name])
97+
if len(project_ids) == 0:
98+
self.module.fail_json(msg="Project was not found")
99+
else:
100+
self.project_id = project_ids[0]
101+
102+
self.retrieve_id(f"{self.base_url}/projects/{self.project_id}/tags/")
103+
104+
if self.state == "present":
105+
if not self.id:
106+
if 'color' not in self.payload:
107+
self.payload['color'] = "#"+''.join([random.choice('0123456789ABCDEF') for j in range(6)])
108+
self.create()
109+
else:
110+
self.update()
111+
elif self.state == "absent":
112+
if not self.id:
113+
self.module.exit_json(changed=False, msg="No tag to delete")
114+
else:
115+
self.delete()
116+
117+
118+
def main():
119+
module = AnsibleModule(
120+
argument_spec=TAG_FIELDS,
121+
supports_check_mode=True,
122+
)
123+
124+
tag_object = FlagsmithTag(module)
125+
126+
if not module.check_mode:
127+
tag_object.manage()
128+
else:
129+
return module.exit_json(changed=False)
130+
131+
132+
if __name__ == "__main__":
133+
main()

0 commit comments

Comments
 (0)