Coverage for harbor_cli/commands/api/replication.py: 56%

83 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-09 12:09 +0100

1from __future__ import annotations 

2 

3from enum import Enum 

4from typing import Optional 

5 

6import typer 

7from harborapi.models.models import ReplicationFilter 

8from harborapi.models.models import ReplicationPolicy 

9from harborapi.models.models import ReplicationTrigger 

10from harborapi.models.models import ReplicationTriggerSettings 

11 

12from ...logs import logger 

13from ...output.render import render_result 

14from ...state import state 

15from ...utils.args import model_params_from_ctx 

16from ...utils.commands import inject_help 

17from ...utils.commands import inject_resource_options 

18 

19# Create a command group 

20app = typer.Typer( 

21 name="replication", 

22 help="Registry replication execution and policy.", 

23 no_args_is_help=True, 

24) 

25policy_cmd = typer.Typer( 

26 name="policy", 

27 help="Manage replication policies.", 

28 no_args_is_help=True, 

29) 

30task_cmd = typer.Typer( 

31 name="task", 

32 help="Manage replication execution tasks.", 

33 no_args_is_help=True, 

34) 

35app.add_typer(task_cmd) 

36app.add_typer(policy_cmd) 

37 

38 

39# HarborAsyncClient.start_replication() 

40@app.command("start") 

41def start_replication_execution( 

42 ctx: typer.Context, 

43 policy_id: int = typer.Option( 

44 ..., 

45 help="The ID of the policy to start a execution for.", 

46 ), 

47) -> None: 

48 """Start a replication execution.""" 

49 replication_url = state.run( 

50 state.client.start_replication(policy_id), "Starting replication..." 

51 ) 

52 render_result(replication_url, ctx) 

53 logger.info(f"Replication started for policy {policy_id}.") 

54 

55 

56# HarborAsyncClient.stop_replication() 

57@app.command("stop") 

58def stop_replication_execution( 

59 ctx: typer.Context, 

60 execution_id: int = typer.Option( 

61 ..., 

62 help="The ID of the replication execution.", 

63 ), 

64) -> None: 

65 """Stop a replication execution.""" 

66 state.run( 

67 state.client.stop_replication(execution_id), 

68 f"Stopping replication execution...", 

69 ) 

70 logger.info(f"Stopped replication execution with ID {execution_id}.") 

71 

72 

73# HarborAsyncClient.get_replication() 

74@app.command("get") 

75def get_replication_execution( 

76 ctx: typer.Context, 

77 execution_id: int = typer.Argument( 

78 ..., 

79 help="The ID of the replication execution.", 

80 ), 

81) -> None: 

82 """Get information about a replication execution.""" 

83 execution = state.run( 

84 state.client.get_replication(execution_id), "Fetching replication execution..." 

85 ) 

86 render_result(execution, ctx) 

87 

88 

89# HarborAsyncClient.get_replications() 

90@app.command("list") 

91@inject_resource_options(use_defaults=False) 

92def list_replication_executions( 

93 ctx: typer.Context, 

94 sort: Optional[str], 

95 policy_id: Optional[int] = typer.Option( 

96 None, 

97 help="The ID of the policy to list executions for.", 

98 ), 

99 status: Optional[str] = typer.Option( 

100 None, 

101 help="The status of the executions to list.", 

102 ), 

103 trigger: Optional[str] = typer.Option( 

104 None, 

105 help="The trigger of the executions to list.", 

106 ), 

107 page: int = 1, 

108 page_size: int = 10, 

109) -> None: 

110 """List replication executions.""" 

111 executions = state.run( 

112 state.client.get_replications( 

113 sort=sort, 

114 trigger=trigger, 

115 policy_id=policy_id, 

116 status=status, 

117 page=page, 

118 page_size=page_size, 

119 ), 

120 "Fetching replication executions...", 

121 ) 

122 render_result(executions, ctx) 

123 

124 

125# HarborAsyncClient.get_replication_policy() 

126@policy_cmd.command("get") 

127def get_replication_policy( 

128 ctx: typer.Context, 

129 policy_id: int = typer.Option( 

130 ..., 

131 help="The ID of the replication policy.", 

132 ), 

133) -> None: 

134 """Get information about a replication policy.""" 

135 policy = state.run( 

136 state.client.get_replication_policy(policy_id), "Fetching replication policy..." 

137 ) 

138 render_result(policy, ctx) 

139 

140 

141class FilterMatchMode(Enum): 

142 """Filter match modes.""" 

143 

144 # The Web UI calls it "matching" and "excluding", 

145 # but the API uses the values "matches" and "excludes". 

146 # We follow the API naming scheme for simplicity. 

147 MATCHES = "matches" 

148 EXCLUDES = "excludes" 

149 

150 

151class FilterResourceMode(Enum): 

152 """Filter resource modes.""" 

153 

154 ALL = "all" 

155 IMAGE = "image" 

156 ARTIFACT = "artifact" 

157 

158 

159# HarborAsyncClient.create_replication_policy() 

160@policy_cmd.command("create") 

161@inject_help(ReplicationPolicy) 

162def create_replication_policy( 

163 ctx: typer.Context, 

164 name: str = typer.Argument(...), 

165 # Difference from spec: we take in the ID of registries instead of 

166 # the full registry definitions. In the Web UI, you can only select 

167 # existing registries, so this is more consistent. 

168 src_registry_id: int = typer.Argument( 

169 ..., 

170 help=( 

171 "The ID of registry to replicate from. " 

172 "Typically an external registry such as [green]'hub.docker.com'[/], [green]'ghcr.io'[/], etc." 

173 ), 

174 ), 

175 dest_registry_id: int = typer.Argument( 

176 ..., 

177 help="The ID of the registry to replicate to.", 

178 ), 

179 description: Optional[str] = typer.Option( 

180 None, 

181 "--description", 

182 ), 

183 dest_namespace: Optional[str] = typer.Option( 

184 None, 

185 "--dest-namespace", 

186 ), 

187 dest_namespace_replace_count: Optional[int] = typer.Option( 

188 None, 

189 "--dest-namespace-replace-count", 

190 ), 

191 replication_trigger_type: Optional[str] = typer.Option( 

192 None, 

193 "--trigger-type", 

194 help=ReplicationTrigger.__fields__["type"].field_info.description, 

195 ), 

196 replication_trigger_cron: Optional[str] = typer.Option( 

197 None, 

198 "--trigger-cron", 

199 help=ReplicationTriggerSettings.__fields__["cron"].field_info.description, 

200 ), 

201 filter_name: Optional[str] = typer.Option( 

202 None, 

203 "--filter-name", 

204 help=( 

205 "Filter the name of the resource. " 

206 "Leave empty or use [green]'**'[/] to match all. " 

207 "[green]'library/**'[/] only matches resources under [cyan]'library'[/]. " 

208 "For more patterns, please refer to the offical Harbor user guide." 

209 ), 

210 ), 

211 filter_tag: Optional[str] = typer.Option( 

212 None, "--filter-tag", help=("Filter the tag of the resource. ") 

213 ), 

214 filter_tag_mode: FilterMatchMode = typer.Option( 

215 FilterMatchMode.MATCHES.value, 

216 "--filter-tag-mode", 

217 help="Match or exclude the given tag", 

218 ), 

219 filter_label: Optional[str] = typer.Option( 

220 None, "--filter-label", help=("Filter the label of the resource. ") 

221 ), 

222 filter_label_mode: FilterMatchMode = typer.Option( 

223 FilterMatchMode.MATCHES.value, 

224 "--filter-label-mode", 

225 help="Match or exclude the given label", 

226 ), 

227 filter_resource: FilterResourceMode = typer.Option( 

228 FilterResourceMode.ALL.value, 

229 "--filter-resource", 

230 help="Filter the resource type to replicate.", 

231 ), 

232 replicate_deletion: Optional[bool] = typer.Option( 

233 None, 

234 "--replicate-deletion", 

235 is_flag=False, 

236 ), 

237 override: Optional[bool] = typer.Option( 

238 None, 

239 "--override", 

240 is_flag=False, 

241 ), 

242 enabled: Optional[bool] = typer.Option( 

243 None, 

244 "--enabled", 

245 is_flag=False, 

246 ), 

247 speed: Optional[int] = typer.Option(None, "--speed-limit"), 

248) -> None: 

249 """Create a replication policy.""" 

250 params = model_params_from_ctx(ctx, ReplicationPolicy) 

251 

252 # Get registries from API 

253 src_registry = state.run(state.client.get_registry(src_registry_id)) 

254 dest_registry = state.run(state.client.get_registry(dest_registry_id)) 

255 

256 # Create the filter objects 

257 filters = [] 

258 if filter_name: 

259 filters.append( 

260 ReplicationFilter( 

261 type="name", 

262 value=filter_name, 

263 decoration=None, 

264 ) 

265 ) 

266 if filter_tag: 

267 filters.append( 

268 ReplicationFilter( 

269 type="tag", 

270 value=filter_tag, 

271 decoration=filter_tag_mode.value, 

272 ) 

273 ) 

274 if filter_label: 

275 filters.append( 

276 ReplicationFilter( 

277 type="label", 

278 value=filter_label, 

279 decoration=filter_label_mode.value, 

280 ) 

281 ) 

282 

283 # create the replication policy 

284 policy = ReplicationPolicy( 

285 **params, 

286 trigger=ReplicationTrigger( 

287 type=replication_trigger_type, 

288 trigger_settings=ReplicationTriggerSettings( 

289 cron=replication_trigger_cron, 

290 ), 

291 ), 

292 src_registry=src_registry, 

293 dest_registry=dest_registry, 

294 filters=filters if filters else None, 

295 ) 

296 

297 policy_url = state.run( 

298 state.client.create_replication_policy(policy), 

299 f"Creating replication policy {policy}...", 

300 ) 

301 logger.info(f"Created replication policy: {policy_url}.") 

302 

303 

304# HarborAsyncClient.update_replication_policy() 

305# TODO: replication policy update 

306# It would be nice to expose the same API as create, but without the 

307# requirement of the source and destination registries. 

308# 3 options come to mind here: 

309# 1. Duplicate most of the code from create, but without the registry 

310# arguments. This is the easiest, plays best with static analysis, 

311# but is prone to errors and spec drift, since we have to maintain 

312# two copies of largely the same function definitions. 

313# 2. Use a decorator to inject the registry arguments to a function 

314# shared by both create and update. This is a bit more complex, 

315# but would allow us to maintain a single function instead of two. 

316# However, it would probably be a bit too "magical", and would break 

317# static analysis. 

318# 3. Have a single monolithic function that handles both create and 

319# update. This plays well with static analysis, but would also 

320# require a lot of conditional logic to handle the different, and 

321# sometimes conflicting, arguments. Furthermore, it would break the 

322# current pattern of having a separate function for each command. 

323# Users would expect `policy update`, but it would instead be 

324# `policy create --update` 

325 

326# HarborAsyncClient.delete_replication_policy() 

327@policy_cmd.command("delete") 

328def delete_replication_policy( 

329 ctx: typer.Context, 

330 policy_id: int = typer.Option( 

331 ..., 

332 help="The ID of the replication policy.", 

333 ), 

334) -> None: 

335 """Delete a replication policy.""" 

336 state.run( 

337 state.client.delete_replication_policy(policy_id), 

338 f"Deleting replication policy with ID {policy_id}...", 

339 ) 

340 logger.info(f"Deleted replication policy with ID {policy_id}.") 

341 

342 

343# HarborAsyncClient.get_replication_policies() 

344@policy_cmd.command("list") 

345@inject_resource_options(use_defaults=False) 

346def list_replication_policies( 

347 ctx: typer.Context, 

348 query: Optional[str], 

349 sort: Optional[str], 

350 page: int, 

351 page_size: int, 

352) -> None: 

353 """List replication policies.""" 

354 policies = state.run( 

355 state.client.get_replication_policies( 

356 query=query, 

357 sort=sort, 

358 page=page, 

359 page_size=page_size, 

360 ), 

361 f"Fetching replication policies...", 

362 ) 

363 render_result(policies, ctx) 

364 

365 

366# Tasks 

367 

368# HarborAsyncClient.get_replication_tasks() 

369@task_cmd.command("list") 

370@inject_resource_options(use_defaults=False) 

371def list_execution_tasks( 

372 ctx: typer.Context, 

373 sort: Optional[str], 

374 status: Optional[str] = typer.Option( 

375 None, "--status", help="Task status to filter by." 

376 ), 

377 resource_type: Optional[str] = typer.Option( 

378 None, "--resource-type", help="Task resource type to filter by." 

379 ), 

380 page: int = 1, 

381 page_size: int = 10, 

382 execution_id: int = typer.Argument( 

383 ..., help="The ID of the replication execution to list tasks for." 

384 ), 

385) -> None: 

386 """List replication tasks.""" 

387 tasks = state.run( 

388 state.client.get_replication_tasks( 

389 execution_id, 

390 sort=sort, 

391 page=page, 

392 page_size=page_size, 

393 status=status, 

394 resource_type=resource_type, 

395 ), 

396 f"Fetching replication tasks for execution {execution_id}...", 

397 ) 

398 render_result(tasks, ctx) 

399 

400 

401# HarborAsyncClient.get_replication_task_log() 

402@task_cmd.command("log") 

403def get_task_log( 

404 ctx: typer.Context, 

405 execution_id: int = typer.Argument( 

406 ..., help="The ID of the execution the task belongs to." 

407 ), 

408 task_id: int = typer.Argument(..., help="The ID of the task to get the log for."), 

409) -> None: 

410 """Get the log for a replication task.""" 

411 log = state.run( 

412 state.client.get_replication_task_log(execution_id, task_id), 

413 "Fetching replication logs...", 

414 ) 

415 render_result(log, ctx) 

416 logger.info( 

417 f"Fetched log for replication task {task_id} of replication execution {execution_id}" 

418 )