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

1from __future__ import annotations 

2 

3from typing import Any 

4from typing import Optional 

5 

6import typer 

7from harborapi.models import Configurations 

8from harborapi.models import ConfigurationsResponse 

9 

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 

16 

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) 

25 

26 

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. 

30 

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'} 

40 

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". 

47 

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 

67 

68 

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) 

91 

92 

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.") 

353 

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) 

365 

366 configuration = Configurations.parse_obj(c) 

367 

368 state.run( 

369 state.client.update_config(configuration), 

370 "Updating configuration...", 

371 ) 

372 logger.info("Configuration updated.")