ligoj-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ ligoj_cli-1.0.0.dist-info/licenses/LICENSE,sha256=L2YkZq7C9_D6o9V3rYPyP1voevszDMzu8Y4DMgXuVL8,1062
2
+ ligojcli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ligojcli/ligoj.py,sha256=BSKbLBHw_miqKrsRRMxFUA1vkqdZUCBpxonjrIjk6i8,3836
4
+ ligojcli/plugins/__init__.py,sha256=UMUcOsjtTJ-xapSGPQDYgKDKeNzoZkYxbfyIW2pQ9Bs,78
5
+ ligojcli/plugins/alfresco.py,sha256=DbWhIViZfHyo6942pRX88OzLtg1RLfxpEOoMfmubyaA,12405
6
+ ligojcli/plugins/argocd.py,sha256=S_PH2KKqhfJmWaanijkLaLbFe0HL-FZ4cj2Cq9Sx92w,9218
7
+ ligojcli/plugins/bootstrap.py,sha256=F-1n-vME_A4fePG1uGaGMz9C_G8KRhfKArEFriZVKmk,40374
8
+ ligojcli/plugins/gitlab.py,sha256=V3rbL4WlO6PykYwXlFUarVOPylcgE3Q443_RiwY98cE,12447
9
+ ligojcli/plugins/harbor.py,sha256=tW8DqF7WGZCeaLJHw5YJtHtcqDp2hyqmqrvoE1Ex1_E,7384
10
+ ligojcli/plugins/jenkins.py,sha256=PPz4atF7-Jx4bG-uuuvBYuEvq3fbQauOYFWiNuoHQtc,28164
11
+ ligojcli/plugins/ligoj.py,sha256=9Vj4BAzsTQ-nzTGDundfMMDsZ1ID-DpmR9sb2YWUl64,90090
12
+ ligojcli/plugins/nexus.py,sha256=EwHbDEtrl_hgoshqwXN8LDjfB0xEDdPCGTcRK4fCKUo,11309
13
+ ligojcli/plugins/sonarqube.py,sha256=vRjJV0GI-t-kUEVUIvE2cbBR5qWJRTS2pr_M51qVmH8,14818
14
+ ligojcli/plugins/ssl.py,sha256=JQma6-T4kHk__FMp6Jq0Dd0-Y1zYQoUtzy_M8x44DpY,5193
15
+ ligojcli/plugins/utils.py,sha256=v-n5sR0WODytOJMev3ahV3NP_WgoBkodWzV7p039aak,16690
16
+ ligoj_cli-1.0.0.dist-info/METADATA,sha256=ubEo2zrpf-Aet_-q93Zo8-G6kudvN5qpETKPElv8Kig,84209
17
+ ligoj_cli-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ ligoj_cli-1.0.0.dist-info/entry_points.txt,sha256=su1lFQ2Dk1a2S-3FhgOIpwnAQek0Q9RYv57TODt1EyM,47
19
+ ligoj_cli-1.0.0.dist-info/top_level.txt,sha256=sufJCWe9-z4wUZlj6vJu5kcwwWv3y3yjd0Ag3LHcUKk,9
20
+ ligoj_cli-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ligoj = ligojcli.ligoj:ligoj
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ligoj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ ligojcli
ligojcli/__init__.py ADDED
File without changes
ligojcli/ligoj.py ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python
2
+ #
3
+ # Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
4
+ #
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from ligojcli.plugins import (
10
+ alfresco,
11
+ argocd,
12
+ bootstrap,
13
+ gitlab,
14
+ harbor,
15
+ jenkins,
16
+ nexus,
17
+ sonarqube,
18
+ utils,
19
+ )
20
+ from ligojcli.plugins import ligoj as ligoj_plugin
21
+
22
+
23
+ def main():
24
+ (parser, subparser_service) = utils.init()
25
+ argocd.configure(subparser_service)
26
+ alfresco.configure(subparser_service)
27
+ bootstrap.configure(subparser_service)
28
+ gitlab.configure(subparser_service)
29
+ harbor.configure(subparser_service)
30
+ jenkins.configure(subparser_service)
31
+ ligoj_plugin.configure(subparser_service)
32
+ nexus.configure(subparser_service)
33
+ sonarqube.configure(subparser_service)
34
+
35
+ (args, output) = utils.configure(parser)
36
+
37
+ if args["version"] is True:
38
+ service = "info"
39
+ action = "version"
40
+ operation = None
41
+ else:
42
+ service = args.get("service")
43
+ action = args.get("action")
44
+ operation = args.get("operation")
45
+ if action is None or service is None:
46
+ parser.print_usage()
47
+ return
48
+
49
+ ligoj_plugin.parse_remote_args(args)
50
+
51
+ result = execute_action(service, action, operation, args)
52
+ if result is False:
53
+ # Ignore output
54
+ return
55
+
56
+ if isinstance(result, str):
57
+ result = {"id": result}
58
+ elif result is not None:
59
+ try:
60
+ result = result.json()
61
+ except BaseException:
62
+ try:
63
+ result = {"id": result.text}
64
+ except BaseException:
65
+ pass
66
+
67
+ if output == "json":
68
+ try:
69
+ print(json.dumps(result))
70
+ except Exception:
71
+ print("Unable to display result as JSON", result)
72
+
73
+ elif output == "text":
74
+ if isinstance(result, str):
75
+ print(result)
76
+ elif result is None or (isinstance(result, dict) and len(result.keys()) == 0):
77
+ print("")
78
+ elif isinstance(result, dict) and len(result.keys()) == 1:
79
+ print(result[list(result.keys())[0]])
80
+ elif isinstance(result, dict):
81
+ for key in result.keys():
82
+ print(result[key])
83
+ else:
84
+ print(result)
85
+
86
+
87
+ def execute_action(service, action, operation, args):
88
+ utils.check_endpoint(utils.not_none(ligoj_plugin.ligoj_endpoint, "endpoint"), "ligoj")
89
+ utils.debug(
90
+ f"[ligoj] Ligoj CLI '{service}/{action}' profile '{utils.ini_profile}', user '{ligoj_plugin.ligoj_api_user}'{'' if ligoj_plugin.ligoj_api_run_as_user is None else (' as ' + ligoj_plugin.ligoj_api_run_as_user)} on endpoint '{ligoj_plugin.ligoj_endpoint}'"
91
+ )
92
+
93
+ return (
94
+ ligoj_plugin.execute_action(service, action, operation, args)
95
+ or alfresco.execute_action(service, action, operation, args)
96
+ or argocd.execute_action(service, action, operation, args)
97
+ or jenkins.execute_action(service, action, operation, args)
98
+ or bootstrap.execute_action(service, action, operation, args)
99
+ or harbor.execute_action(service, action, operation, args)
100
+ or nexus.execute_action(service, action, operation, args)
101
+ or sonarqube.execute_action(service, action, operation, args)
102
+ or gitlab.execute_action(service, action, operation, args)
103
+ or False
104
+ )
105
+
106
+
107
+ def ligoj() -> int:
108
+ """Entry point for the ligoj CLI."""
109
+ try:
110
+ main()
111
+ except ValueError as e:
112
+ utils.error(str(e))
113
+ sys.exit(os.EX_SOFTWARE)
114
+ return os.EX_SOFTWARE
115
+ except SystemExit as e:
116
+ raise e
117
+ except BaseException as e:
118
+ template = "An exception of type {0} occurred. Arguments:\n{1!r}"
119
+ error_message = template.format(type(e).__name__, e.args)
120
+ utils.error(error_message)
121
+ raise e
122
+ return 0
123
+
124
+
125
+ if __name__ == "__main__":
126
+ ligoj()
@@ -0,0 +1,3 @@
1
+ #
2
+ # Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
3
+ #
@@ -0,0 +1,246 @@
1
+ #
2
+ # Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
3
+ #
4
+ import base64
5
+
6
+ from unidecode import unidecode
7
+
8
+ from ligojcli.plugins import ligoj, utils
9
+
10
+ PLUGIN_NAME = "alfresco"
11
+ alfresco_endpoint: str | None = None
12
+ alfresco_ticket: str | None = None
13
+ alfresco_user: str | None = None
14
+ alfresco_password: str | None = None
15
+
16
+
17
+ def configure(subparser_service):
18
+ # alfresco
19
+ subparser_action = subparser_service.add_parser("alfresco", help="Alfresco related operations").add_subparsers(title="action", help="Action", dest="action")
20
+ subparser_service2 = subparser_action.add_parser("site", help="Site operations").add_subparsers(title="action", help="Action", dest="operation")
21
+ subparser_service2.add_parser("list", help="List sites")
22
+ parser_action = subparser_service2.add_parser("create", help="Create site")
23
+ parser_action.add_argument("--id", "-i", help="Site name")
24
+ parser_action.add_argument("--title", help="Site title")
25
+ parser_action.add_argument("--visibility", help="Visibility", choices=["PUBLIC", "PRIVATE"])
26
+ parser_action.add_argument("--description", help="Description")
27
+ parser_action = subparser_service2.add_parser("get", help="Get site")
28
+ parser_action.add_argument("--id", help="Site name")
29
+ parser_action = subparser_service2.add_parser("add-group", help="Add group to site with a role")
30
+ parser_action.add_argument("--id", help="Site name")
31
+ parser_action.add_argument("--group", help="Group name")
32
+ parser_action.add_argument("--role", help="Role name", choices=["SiteCollaborator", "SiteConsumer", "SiteManager"])
33
+ parser_action = subparser_service2.add_parser("remove-group", help="Remove group from site")
34
+ parser_action.add_argument("--id", help="Site name")
35
+ parser_action.add_argument("--group", help="Group name")
36
+ subparser_service2 = subparser_action.add_parser("group", help="Group operations").add_subparsers(title="action", help="Action", dest="operation")
37
+ parser_action = subparser_service2.add_parser("create", help="Create group")
38
+ parser_action.add_argument("--id", "-i", help="Group name")
39
+ parser_action.add_argument("--display-name", help="Group display name")
40
+ parser_action.add_argument("--parents", help="Parent group identifiers", nargs="*", default=["admin"])
41
+ parser_action = subparser_service2.add_parser("delete", help="Delete group")
42
+ parser_action.add_argument("--id", "-i", help="Group name")
43
+ parser_action = subparser_service2.add_parser("get", help="Get group")
44
+ parser_action.add_argument("--id", "-i", help="Group name")
45
+
46
+
47
+ def execute_action(service, action, operation, args):
48
+ if service == "alfresco":
49
+ parse_remote_args(args)
50
+
51
+ if action == "site" and operation == "create":
52
+ return alfresco_create_site(args.get("id"), args.get("title"), args.get("description"), args.get("visibility"))
53
+ if action == "site" and operation == "list":
54
+ return alfresco_list_sites()
55
+ if action == "site" and operation == "get":
56
+ return alfresco_get_site(args.get("id"))
57
+ if action == "site" and operation == "add-group":
58
+ return alfresco_set_permissions(args.get("group"), [args.get("role")], args.get("id"))
59
+ if action == "site" and operation == "remove-group":
60
+ return alfresco_remove_permissions(args.get("group"), args.get("id"))
61
+ if action == "group" and operation == "create":
62
+ return alfresco_create_group(args.get("id"), args.get("display_name"), args.get("parents"))
63
+ if action == "group" and operation == "get":
64
+ return alfresco_get_group(args.get("id"))
65
+ if action == "group" and operation == "get":
66
+ return alfresco_delete_group(args.get("id"))
67
+ return None
68
+
69
+
70
+ # Extract from args the parameters related to remote access API of Alfresco
71
+ def parse_remote_args(args):
72
+ global alfresco_endpoint
73
+ global alfresco_ticket
74
+ global alfresco_user
75
+ global alfresco_password
76
+ alfresco_endpoint = utils.get_config(args, "alfresco_endpoint", "ALFRESCO_ENDPOINT", None)
77
+ alfresco_ticket = utils.get_secret(args, "alfresco_ticket", "ALFRESCO_TICKET", None)
78
+ alfresco_user = utils.get_secret(args, "alfresco_user", "ALFRESCO_USER", None)
79
+ alfresco_password = utils.get_secret(args, "alfresco_password", "ALFRESCO_PASSWORD", None)
80
+
81
+
82
+ def call_alfresco_api(method, definition, url, **kwargs):
83
+ global alfresco_ticket
84
+ if not alfresco_ticket:
85
+ if not alfresco_user or not alfresco_password:
86
+ raise ValueError("[alfresco] No ticket and no user/password provided")
87
+ alfresco_ticket = alfresco_login(alfresco_user, alfresco_password)
88
+ return utils.call_rest_api(
89
+ method, "alfresco", f"{alfresco_endpoint}/{definition}/versions/1/", url, None, kwargs | {"headers": {"Authorization": f"Basic {base64.b64encode(alfresco_ticket.encode('ascii')).decode()}"}}
90
+ )
91
+
92
+
93
+ def alfresco_login(user, password):
94
+ utils.info(f"[alfresco] Login of user '{user}' ...")
95
+ return utils.call_rest_api("POST", "alfresco", f"{alfresco_endpoint}/authentication/versions/1/", "tickets", None, {"data": {"userId": user, "password": password}}).json()["entry"]["id"]
96
+
97
+
98
+ def alfresco_internal_group_name(name):
99
+ return name if name.startswith("GROUP_") else f"GROUP_{name}"
100
+
101
+
102
+ def alfresco_get_group(name):
103
+ utils.info(f"[alfresco] Get group {name} ...")
104
+ name = alfresco_internal_group_name(name)
105
+ response = call_alfresco_api("GET", "alfresco", f"groups/{name}", ignore_error=True)
106
+ return response and response.json() or None
107
+
108
+
109
+ def alfresco_delete_group(name):
110
+ utils.info(f"[alfresco] Delete group {name} ...")
111
+ name = alfresco_internal_group_name(name)
112
+ response = call_alfresco_api("DELETE", "alfresco", f"groups/{name}", ignore_error=True)
113
+ return response and response.json() or None
114
+
115
+
116
+ def alfresco_list_groups():
117
+ utils.info("[alfresco] List groups ...")
118
+ return call_alfresco_api("GET", "alfresco", "groups")
119
+
120
+
121
+ def alfresco_create_group(name: str, display_name: str, parents=None):
122
+ utils.info(f"[alfresco] Create group '{name}' ...")
123
+ if alfresco_get_group(name):
124
+ utils.debug(f"[alfresco] Group {name} already exists ...")
125
+ else:
126
+ call_alfresco_api("POST", "alfresco", "groups", data={"id": name, "displayName": display_name, "parentIds": parents or []})
127
+
128
+
129
+ def alfresco_list_sites():
130
+ utils.info("[alfresco] List sites ...")
131
+ return call_alfresco_api("GET", "alfresco", "sites")
132
+
133
+
134
+ def alfresco_get_site(name):
135
+ utils.info(f"[alfresco] Get site {name} ...")
136
+ response = call_alfresco_api("GET", "alfresco", f"sites/{name}", ignore_error=True)
137
+ return response and response.json()["entry"] or None
138
+
139
+
140
+ def alfresco_create_site(name, title, description, visibility="PRIVATE"):
141
+ if name != unidecode(name):
142
+ raise ValueError("[alfresco] Site identifier can contain only ASCII chars")
143
+
144
+ utils.info(f"[alfresco] Create site '{name}', '{title}' ({visibility}) ...")
145
+ site_details = alfresco_get_site(name)
146
+ if not title:
147
+ title = name
148
+ if site_details is None:
149
+ call_alfresco_api("POST", "alfresco", "sites", data={"id": name, "title": title, "description": description, "visibility": visibility})
150
+ return
151
+
152
+ utils.info(f"[alfresco] Site {name} already exists")
153
+ current_title = site_details.get("title", "")
154
+ current_description = site_details.get("description", "")
155
+ current_visibility = site_details["visibility"]
156
+ if current_title != (title or "") or current_description != (description or "") or current_visibility != visibility:
157
+ # Update to the new key
158
+ utils.debug(f"[alfresco] Update site '{name}' title, description or visibility ...")
159
+ call_alfresco_api("PUT", "alfresco", f"sites/{name}", data={"title": title, "description": description, "visibility": visibility})
160
+
161
+
162
+ def alfresco_delete_site(site_id, site_details, remove_from_trash):
163
+ utils.info(f"[alfresco] Delete site {site_id} ...")
164
+ if site_details is None:
165
+ utils.info(f"[alfresco] Site {site_id} does not exist, ignore")
166
+ else:
167
+ call_alfresco_api("DELETE", "alfresco", f"sites/{site_id}")
168
+
169
+ if remove_from_trash:
170
+ nodes = call_alfresco_api("GET", "alfresco", "deleted-nodes", data={"maxItems": 100000}).json()["list"]["entries"]
171
+ node_entry = next(filter(lambda x: x["entry"]["nodeType"] == "st:site" and x["entry"]["name"] == site_id, nodes), None)
172
+ if node_entry:
173
+ # Also remote site from trash
174
+ node_id = node_entry["entry"]["id"]
175
+ utils.info(f"[alfresco] Delete site {site_id} from trash, node_id={node_id} ...")
176
+ call_alfresco_api("DELETE", "alfresco", f"deleted-nodes/{node_id}")
177
+ elif site_details:
178
+ utils.warn(f"[alfresco] Site {site_id} has been deleted, however it is not found in trash")
179
+
180
+
181
+ def alfresco_create_site_roles(groups_by_name: dict[str, str], definition):
182
+ utils.info("[alfresco] Create Alfresco roles for sites ...")
183
+
184
+ for alfresco_site in definition.get("sites", []):
185
+ alfresco_site_id = alfresco_site.get("id", "")
186
+ alfresco_site_title = alfresco_site.get("title", "")
187
+ alfresco_site_description = alfresco_site.get("description", "")
188
+ alfresco_site_visibility = alfresco_site.get("visibility", "public").upper()
189
+ if alfresco_site_id == "":
190
+ raise ValueError("[alfresco] Missing Alfresco site id")
191
+ if alfresco_site_title == "":
192
+ alfresco_site_title = alfresco_site_id
193
+ if alfresco_site_visibility not in ["PUBLIC", "PRIVATE"]:
194
+ raise ValueError("[alfresco] Invalid Alfresco visibility in site '{alfresco_site_id}'")
195
+ alfresco_create_site(alfresco_site_id, alfresco_site_title, alfresco_site_description, alfresco_site_visibility)
196
+ alfresco_create_roles_scope(groups_by_name, alfresco_site, alfresco_site_id)
197
+
198
+
199
+ def alfresco_delete_site_roles(groups_by_name: dict[str, str], definition, with_data: bool):
200
+ utils.info("[alfresco] Delete Alfresco roles for sites ...")
201
+
202
+ for alfresco_site in definition.get("sites", []):
203
+ alfresco_site_id = alfresco_site.get("id", "")
204
+ if alfresco_site_id == "":
205
+ raise ValueError("[alfresco] Missing Alfresco site id")
206
+ site_details = alfresco_get_site(alfresco_site_id)
207
+
208
+ if site_details:
209
+ alfresco_delete_roles_scope(groups_by_name, alfresco_site, alfresco_site_id)
210
+ if with_data:
211
+ alfresco_delete_site(alfresco_site_id, site_details, True)
212
+
213
+
214
+ def alfresco_create_roles_scope(groups_by_name: dict[str, str], definition, site_id):
215
+ for alfresco_group in definition.get("roles", {}).keys():
216
+ ldap_group = ligoj.get_ldap_group("alfresco", groups_by_name, alfresco_group)
217
+ role_definition = definition["roles"][alfresco_group]
218
+ permissions = role_definition.get("permissions", [])
219
+ if len(permissions) == 0:
220
+ raise ValueError(f"[alfresco] Missing Alfresco permissions in role '{alfresco_group}'")
221
+ alfresco_create_group(ldap_group, ldap_group)
222
+ alfresco_set_permissions(ldap_group, permissions, site_id)
223
+
224
+
225
+ def alfresco_delete_roles_scope(groups_by_name: dict[str, str], definition, site_id):
226
+ for alfresco_group in definition.get("roles", {}).keys():
227
+ ldap_group = ligoj.get_ldap_group("alfresco", groups_by_name, alfresco_group)
228
+ alfresco_remove_permissions(ldap_group, site_id)
229
+ alfresco_delete_group(ldap_group)
230
+
231
+
232
+ def alfresco_set_permissions(group, permissions, site_id):
233
+ for permission in permissions:
234
+ utils.info(f"[alfresco] Set permission '{permission}' to group '{group}' on site '{site_id}' ...")
235
+ group = alfresco_internal_group_name(group)
236
+ member = call_alfresco_api("GET", "alfresco", f"sites/{site_id}/group-members/{group}", ignore_400=True)
237
+ if member:
238
+ call_alfresco_api("PUT", "alfresco", f"sites/{site_id}/group-members/{group}", data={"role": permission})
239
+ else:
240
+ call_alfresco_api("POST", "alfresco", f"sites/{site_id}/group-members", data={"id": group, "role": permission})
241
+
242
+
243
+ def alfresco_remove_permissions(group, site_id):
244
+ utils.info(f"[alfresco] Remove group '{group}' from site '{site_id}' ...")
245
+ group = alfresco_internal_group_name(group)
246
+ call_alfresco_api("DELETE", "alfresco", f"sites/{site_id}/group-members/{group}", ignore_409=True)
@@ -0,0 +1,205 @@
1
+ #
2
+ # Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
3
+ #
4
+ import json
5
+
6
+ from unidecode import unidecode
7
+
8
+ from ligojcli.plugins import ligoj, utils
9
+
10
+ PLUGIN_NAME = "argocd"
11
+ ARGOCD_ALLOWED_ACTIONS = ["get", "create", "sync", "override", "delete", "*"]
12
+ argocd_endpoint: str | None = None
13
+ argocd_token: str | None = None
14
+ argocd_user: str | None = None
15
+ argocd_password: str | None = None
16
+
17
+
18
+ def configure(subparser_service):
19
+
20
+ # argocd
21
+ subparser_action = subparser_service.add_parser("argocd", help="Argo CD related operations").add_subparsers(title="action", help="Action", dest="action")
22
+ subparser_service2 = subparser_action.add_parser("project", help="Project operations").add_subparsers(title="action", help="Action", dest="operation")
23
+ subparser_service2.add_parser("list", help="List projects")
24
+ parser_action = subparser_service2.add_parser("upsert", help="Create or update a project")
25
+ parser_action.add_argument("--name", "-i", help="Project name")
26
+ parser_action.add_argument("--description", help="Description")
27
+ parser_action = subparser_service2.add_parser("get", help="Get project")
28
+ parser_action.add_argument("--name", help="Project name")
29
+
30
+
31
+ def execute_action(service, action, operation, args):
32
+
33
+ # argocd
34
+ if service == "argocd":
35
+ parse_remote_args(args)
36
+ if action == "project" and operation == "upsert":
37
+ return argocd_upsert_project(args.get("name"), args.get("description"))
38
+ if action == "project" and operation == "list":
39
+ return argocd_list_projects()
40
+ if action == "project" and operation == "get":
41
+ return argocd_get_project(args.get("name"))
42
+ return None
43
+
44
+
45
+ # Extract from args the parameters related to remote access API of ArgoCD
46
+ def parse_remote_args(args):
47
+ global argocd_endpoint
48
+ global argocd_token
49
+ global argocd_user
50
+ global argocd_password
51
+ argocd_endpoint = utils.get_config(args, "argocd_endpoint", "ARGOCD_ENDPOINT", None)
52
+ argocd_token = utils.get_secret(args, "argocd_token", "ARGOCD_TOKEN", None)
53
+ argocd_user = utils.get_secret(args, "argocd_user", "ARGOCD_API_TOKEN", None)
54
+ argocd_password = utils.get_secret(args, "argocd_password", "ARGOCD_PASSWORD", None)
55
+
56
+
57
+ def argocd_list_projects():
58
+ utils.info("[argocd] List projects ...")
59
+ return call_argocd_api("GET", "projects").json()["items"]
60
+
61
+
62
+ def argocd_get_project(name):
63
+ utils.info(f"[argocd] Get project {name} ...")
64
+ response = call_argocd_api("GET", f"projects/{name}/detailed", ignore_error=True)
65
+ return response and response.json() or None
66
+
67
+
68
+ def argocd_upsert_project(name, description: str | None = None):
69
+ if name != unidecode(name):
70
+ raise ValueError("[argocd] Project identifier can contain only ASCII chars")
71
+
72
+ utils.info(f"[argocd] Create project '{name}' ...")
73
+ project_details = argocd_get_project(name)
74
+ if not description:
75
+ description = name
76
+ if project_details is None:
77
+ call_argocd_api("POST", "projects", data={"project": {"metadata": {"name": name}, "spec": {"description": description}}})
78
+ return argocd_get_project(name)
79
+
80
+ utils.info(f"[argocd] Project '{name}' already exists")
81
+ spec = project_details["project"].get("spec")
82
+ if not spec:
83
+ spec = {}
84
+ project_details["spec"] = spec
85
+ current_description = spec.get("description")
86
+ if current_description != (description or ""):
87
+ # Update to the new key
88
+ utils.debug(f"[argocd] Update project '{name}' description ...")
89
+ spec["description"] = description
90
+ call_argocd_api("PUT", f"projects/{name}", data=project_details)
91
+ return project_details
92
+
93
+
94
+ def argocd_login(user, password):
95
+ utils.info(f"[argocd] Login of user '{user}' ...")
96
+ return utils.call_rest_api("POST", "argocd", f"{argocd_endpoint}/api/v1", "session", None, {"username": user, "password": password}).json()["token"]
97
+
98
+
99
+ def call_argocd_api(method, url, **kwargs):
100
+ global argocd_token
101
+ if not argocd_token:
102
+ if not argocd_user or not argocd_password:
103
+ raise ValueError("[argocd] No token and no user/password provided")
104
+ argocd_token = argocd_login(argocd_user, argocd_password)
105
+ return utils.call_rest_api(method, "argocd", f"{argocd_endpoint}/api/v1", url, None, kwargs | {"headers": {"Content-Type": "application/json"}, "cookies": {"argocd.token": argocd_token}})
106
+
107
+
108
+ def argocd_create_project_roles(groups_by_name: dict[str, str], definition):
109
+ utils.info("[argocd] Create ArgoCD roles for projects ...")
110
+
111
+ for argocd_project in definition.get("projects", []):
112
+ argocd_project_id = argocd_project.get("name", "")
113
+ argocd_project_description = argocd_project.get("description", "")
114
+ if argocd_project_id == "":
115
+ raise ValueError("[argocd] Missing ArgoCD project name")
116
+ if argocd_project_description == "":
117
+ argocd_project_description = argocd_project_id
118
+ project_details = argocd_upsert_project(argocd_project_id, argocd_project_description)
119
+ argocd_create_roles_scope(groups_by_name, argocd_project, argocd_project_id, project_details)
120
+
121
+
122
+ def argocd_delete_project_roles(groups_by_name: dict[str, str], definition, with_data):
123
+ utils.info("[argocd] Delete ArgoCD roles for projects ...")
124
+
125
+ for argocd_project in definition.get("projects", []):
126
+ argocd_project_id = argocd_project.get("name", "")
127
+ if argocd_project_id == "":
128
+ raise ValueError("[argocd] Missing ArgoCD project name")
129
+ project_details = argocd_get_project(argocd_project_id)
130
+ if project_details:
131
+ argocd_delete_roles_scope(groups_by_name, argocd_project, argocd_project_id, project_details)
132
+
133
+ if with_data:
134
+ call_argocd_api("DELETE", f"projects/{argocd_project_id}")
135
+ else:
136
+ utils.info(f"[argocd] Project {argocd_project_id} does not exist, ignore")
137
+
138
+
139
+ def argocd_create_roles_scope(groups_by_name: dict[str, str], definition, project_id, project_details):
140
+ current_project_json = json.dumps(project_details)
141
+ for argocd_group in definition.get("roles", {}).keys():
142
+ ldap_group = ligoj.get_ldap_group("argocd", groups_by_name, argocd_group)
143
+ role_definition = definition["roles"][argocd_group]
144
+ permissions = role_definition.get("permissions", [])
145
+ if len(permissions) == 0:
146
+ raise ValueError(f"[argocd] Missing ArgoCD permissions in role '{argocd_group}'")
147
+ argocd_role = argocd_complete_role(ldap_group, project_details)
148
+ argocd_complete_permissions(ldap_group, permissions, project_id, argocd_role)
149
+
150
+ new_project_json = json.dumps(project_details)
151
+ if new_project_json != current_project_json:
152
+ # at least one change in this project
153
+ utils.debug(f"[argocd] Update project '{project_id}' permissions ...")
154
+ call_argocd_api("PUT", f"projects/{project_id}", data=project_details)
155
+
156
+
157
+ def argocd_delete_roles_scope(groups_by_name: dict[str, str], definition, project_id, project_details):
158
+ current_project_json = json.dumps(project_details)
159
+ for argocd_group in definition.get("roles", {}).keys():
160
+ ldap_group = ligoj.get_ldap_group("argocd", groups_by_name, argocd_group)
161
+ argocd_complete_role_delete(ldap_group, project_details)
162
+
163
+ new_project_json = json.dumps(project_details)
164
+ if new_project_json != current_project_json:
165
+ # at least one change in this project
166
+ utils.debug(f"[argocd] Update project '{project_id}', delete permissions ...")
167
+ call_argocd_api("PUT", f"projects/{project_id}", data=project_details)
168
+
169
+
170
+ def argocd_complete_role(group, project_details):
171
+ roles = project_details["project"]["spec"].get("roles")
172
+ if not roles:
173
+ roles = []
174
+ project_details["project"]["spec"]["roles"] = roles
175
+
176
+ role = next(filter(lambda r: r["name"] == group, roles), None)
177
+ if not role:
178
+ # Add a new role
179
+ role = {"name": group}
180
+ roles.append(role)
181
+ return role
182
+
183
+
184
+ def argocd_complete_role_delete(group, project_details):
185
+ roles = project_details["project"]["spec"].get("roles", [])
186
+ project_details["project"]["spec"]["roles"] = list(filter(lambda r: r["name"] != group, roles))
187
+
188
+
189
+ def argocd_complete_permissions(group, permissions, project_id, argocd_role):
190
+ policies = []
191
+ for permission in permissions:
192
+ if "action" not in permission:
193
+ raise ValueError("[argocd] Missing ArgoCD permission action")
194
+ action = permission["action"]
195
+ if action not in ARGOCD_ALLOWED_ACTIONS:
196
+ raise ValueError(f"[argocd] Invalid ArgoCD permission action, allowed: {ARGOCD_ALLOWED_ACTIONS}")
197
+ action_permission = permission.get("permission", "allow")
198
+ if action_permission != "allow" and action_permission != "deny":
199
+ raise ValueError("[argocd] Invalid ArgoCD permission type, allowed: deny/allow")
200
+ application = permission.get("application", "*")
201
+ policies.append(f"p, proj:{project_id}:{group}, applications, {action}, {project_id}/{application}, {action_permission}")
202
+
203
+ # Replace previous groups and policies
204
+ argocd_role["groups"] = [group.lower()]
205
+ argocd_role["policies"] = policies