Coverage for harbor_cli/commands/api/harbor_config.py: 34%
43 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from typing import Any
4from typing import Optional
6import typer
7from harborapi.models import Configurations
8from harborapi.models import ConfigurationsResponse
10from ...logs import logger
11from ...output.console import exit_err
12from ...output.render import render_result
13from ...state import state
14from ...utils.args import model_params_from_ctx
15from ...utils.commands import inject_help
17# Create a command group
18app = typer.Typer(
19 # Naming to remove ambiguity with the "cli-config" command group
20 # in ../cli/cli_config.py
21 name="harbor-config",
22 help="Manage Harbor configuration.",
23 no_args_is_help=True,
24)
27def flatten_config_response(response: ConfigurationsResponse) -> dict[str, Any]:
28 """Flattens a ConfigurationsResponse object to a single level, removing
29 any information about whether the fields are editable or not.
31 Examples
32 -------
33 >>> response = ConfigurationsResponse(
34 ... auth_mode=StringConfigItem(value="db_auth", editable=True),
35 ... )
36 >>> response.dict() # just to show the structure
37 {'auth_mode': {'value': 'db_auth', 'editable': True}}
38 >>> flatten_config_response(response)
39 {'auth_mode': 'db_auth'}
41 Parameters
42 ----------
43 response : ConfigurationsResponse
44 A ConfigurationsResponse object from the Harbor API.
45 Fields are either None or a {String,Int,Bool}ConfigItem object,
46 that each have the fields "value" and "editable".
48 Returns
49 -------
50 dict[str, Any]
51 A flattened dictionary with the "value" fields of the
52 {String,Int,Bool}ConfigItem objects.
53 """
54 # A ConfigurationsResponse contains {String,Int,Bool}ConfigItem objects
55 # which has the fields "value" and "editable".
56 c = response.dict()
57 for key, value in list(c.items()):
58 if value is None:
59 del c[key]
60 if not isinstance(value, dict):
61 # NOTE: continue or delete?
62 # add flag to keep or delete?
63 continue
64 v = value.get("value")
65 c[key] = v
66 return c
69@app.command("get")
70def get_config(
71 ctx: typer.Context,
72 flatten: bool = typer.Option(
73 False,
74 "--flatten",
75 help=(
76 "Flatten config fields. "
77 "Removes 'editable' field and replaces field value with 'value' field."
78 ),
79 ),
80) -> None:
81 """Fetch the Harbor configuration."""
82 system_info = state.run(state.client.get_config(), "Fetching system info...")
83 if flatten:
84 # In order to print a flattened response, we turn it from a
85 # ConfigurationsResponse to a Configurations object.
86 flattened = flatten_config_response(system_info)
87 c = Configurations.parse_obj(flattened)
88 render_result(c, ctx)
89 else:
90 render_result(system_info, ctx)
93# TODO: fix Optional[bool] options
94@app.command("update", no_args_is_help=True)
95@inject_help(Configurations)
96def update_config(
97 ctx: typer.Context,
98 auth_mode: Optional[str] = typer.Option(
99 None,
100 "--auth-mode",
101 ),
102 email_from: Optional[str] = typer.Option(
103 None,
104 "--email-from",
105 ),
106 email_host: Optional[str] = typer.Option(
107 None,
108 "--email-host",
109 ),
110 email_identity: Optional[str] = typer.Option(
111 None,
112 "--email-identity",
113 ),
114 email_insecure: Optional[bool] = typer.Option(
115 None,
116 "--email-insecure",
117 is_flag=False,
118 ),
119 email_password: Optional[str] = typer.Option(
120 None,
121 "--email-password",
122 ),
123 email_port: Optional[int] = typer.Option(
124 None,
125 "--email-port",
126 ),
127 email_ssl: Optional[bool] = typer.Option(
128 None,
129 "--email-ssl",
130 is_flag=False,
131 ),
132 email_username: Optional[str] = typer.Option(
133 None,
134 "--email-username",
135 ),
136 ldap_base_dn: Optional[str] = typer.Option(
137 None,
138 "--ldap-base-dn",
139 ),
140 ldap_filter: Optional[str] = typer.Option(
141 None,
142 "--ldap-filter",
143 ),
144 ldap_group_base_dn: Optional[str] = typer.Option(
145 None,
146 "--ldap-group-base-dn",
147 ),
148 ldap_group_admin_dn: Optional[str] = typer.Option(
149 None,
150 "--ldap-group-admin-dn",
151 ),
152 ldap_group_attribute_name: Optional[str] = typer.Option(
153 None,
154 "--ldap-group-attribute-name",
155 ),
156 ldap_group_search_filter: Optional[str] = typer.Option(
157 None,
158 "--ldap-group-search-filter",
159 ),
160 ldap_group_search_scope: Optional[int] = typer.Option(
161 None,
162 "--ldap-group-search-scope",
163 ),
164 ldap_scope: Optional[int] = typer.Option(
165 None,
166 "--ldap-scope",
167 ),
168 ldap_search_dn: Optional[str] = typer.Option(
169 None,
170 "--ldap-search-dn",
171 ),
172 ldap_search_password: Optional[str] = typer.Option(
173 None,
174 "--ldap-search-password",
175 ),
176 ldap_timeout: Optional[int] = typer.Option(
177 None,
178 "--ldap-timeout",
179 ),
180 ldap_uid: Optional[str] = typer.Option(
181 None,
182 "--ldap-uid",
183 ),
184 ldap_url: Optional[str] = typer.Option(
185 None,
186 "--ldap-url",
187 ),
188 ldap_verify_cert: Optional[bool] = typer.Option(
189 None,
190 "--ldap-verify-cert",
191 is_flag=False,
192 ),
193 ldap_group_membership_attribute: Optional[str] = typer.Option(
194 None,
195 "--ldap-group-membership-attribute",
196 ),
197 project_creation_restriction: Optional[str] = typer.Option(
198 None,
199 "--project-creation-restriction",
200 ),
201 read_only: Optional[bool] = typer.Option(
202 None,
203 "--read-only",
204 is_flag=False,
205 ),
206 self_registration: Optional[bool] = typer.Option(
207 None,
208 "--self-registration",
209 is_flag=False,
210 ),
211 token_expiration: Optional[int] = typer.Option(
212 None,
213 "--token-expiration",
214 ),
215 uaa_client_id: Optional[str] = typer.Option(
216 None,
217 "--uaa-client-id",
218 ),
219 uaa_client_secret: Optional[str] = typer.Option(
220 None,
221 "--ua",
222 ),
223 uaa_endpoint: Optional[str] = typer.Option(
224 None,
225 "--uaa-endpoint",
226 ),
227 uaa_verify_cert: Optional[bool] = typer.Option(
228 None,
229 "--uaa-verify-cert",
230 is_flag=False,
231 ),
232 http_authproxy_endpoint: Optional[str] = typer.Option(
233 None,
234 "--http-authproxy-endpoint",
235 ),
236 http_authproxy_tokenreview_endpoint: Optional[str] = typer.Option(
237 None,
238 "--http-authproxy-tokenreview-endpoint",
239 ),
240 http_authproxy_admin_groups: Optional[str] = typer.Option(
241 None,
242 "--http-authproxy-admin-groups",
243 ),
244 http_authproxy_admin_usernames: Optional[str] = typer.Option(
245 None,
246 "--http-authproxy-admin-usernames",
247 help=(
248 "The username of the user with admin privileges. "
249 "NOTE: ONLY ACCEPTS A SINGLE USERNAME DESPITE NAMING SCHEME IMPLYING OTHERWISE! "
250 ),
251 ),
252 http_authproxy_verify_cert: Optional[bool] = typer.Option(
253 None,
254 "--http-authproxy-verify-cert",
255 is_flag=False,
256 ),
257 http_authproxy_skip_search: Optional[bool] = typer.Option(
258 None,
259 "--http-authproxy-skip-search",
260 is_flag=False,
261 ),
262 http_authproxy_server_certificate: Optional[str] = typer.Option(
263 None,
264 "--http-authproxy-server-certificate",
265 ),
266 oidc_name: Optional[str] = typer.Option(
267 None,
268 "--oidc-name",
269 ),
270 oidc_endpoint: Optional[str] = typer.Option(
271 None,
272 "--oidc-endpoint",
273 ),
274 oidc_client_id: Optional[str] = typer.Option(
275 None,
276 "--oidc-client-id",
277 ),
278 oidc_client_secret: Optional[str] = typer.Option(
279 None,
280 "--oidc-client-secret",
281 ),
282 oidc_groups_claim: Optional[str] = typer.Option(
283 None,
284 "--oidc-groups-claim",
285 ),
286 oidc_admin_group: Optional[str] = typer.Option(
287 None,
288 "--oidc-admin-group",
289 ),
290 oidc_scope: Optional[str] = typer.Option(
291 None,
292 "--oidc-scope",
293 ),
294 oidc_user_claim: Optional[str] = typer.Option(
295 None,
296 "--oidc-user-claim",
297 ),
298 oidc_verify_cert: Optional[bool] = typer.Option(
299 None,
300 "--oidc-verify-cert",
301 is_flag=False,
302 ),
303 oidc_auto_onboard: Optional[bool] = typer.Option(
304 None,
305 "--oidc-auto-onboard",
306 is_flag=False,
307 ),
308 # TODO: fix spelling when we add alias in harborapi
309 oidc_extra_redirect_parms: Optional[str] = typer.Option(
310 None,
311 help=(
312 "Extra parameters to add when redirect request to OIDC provider. "
313 "WARNING: 'parms' not 'parAms', due to Harbor spelling parity (blame them)."
314 ),
315 ),
316 robot_token_duration: Optional[int] = typer.Option(
317 None,
318 "--robot-token-duration",
319 ),
320 robot_name_prefix: Optional[str] = typer.Option(
321 None,
322 "--robot-name-prefix",
323 ),
324 notification_enable: Optional[bool] = typer.Option(
325 None,
326 "--notifications",
327 is_flag=False,
328 ),
329 quota_per_project_enable: Optional[bool] = typer.Option(
330 None,
331 "--quota-per-project",
332 is_flag=False,
333 ),
334 storage_per_project: Optional[int] = typer.Option(
335 None,
336 "--storage-per-project",
337 ),
338 audit_log_forward_endpoint: Optional[str] = typer.Option(
339 None,
340 "--audit-log-forward-endpoint",
341 ),
342 skip_audit_log_database: Optional[bool] = typer.Option(
343 None,
344 "--skip-audit-log-database",
345 is_flag=False,
346 ),
347) -> None:
348 """Update the configuration of Harbor."""
349 logger.info("Updating configuration...")
350 params = model_params_from_ctx(ctx, Configurations)
351 if not params:
352 exit_err("No configuration parameters provided.")
354 current_config = state.run(
355 state.client.get_config(),
356 "Fetching current configuration...",
357 )
358 # get_config fetches a ConfigurationsResponse object, but we need
359 # to pass a Configurations object to update_config. To get the
360 # correct parameters to pass to Configurations, we need to flatten
361 # the dict representation of the ConfigurationsResponse object
362 # to create a dict of key:ConfigItem.value.
363 c = flatten_config_response(current_config)
364 c.update(params)
366 configuration = Configurations.parse_obj(c)
368 state.run(
369 state.client.update_config(configuration),
370 "Updating configuration...",
371 )
372 logger.info("Configuration updated.")