Coverage for harbor_cli/commands/cli/init.py: 25%

116 statements  

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

1from __future__ import annotations 

2 

3from pathlib import Path 

4from typing import Optional 

5 

6import typer 

7 

8from ...app import app 

9from ...config import create_config 

10from ...config import DEFAULT_CONFIG_FILE 

11from ...config import HarborCLIConfig 

12from ...config import load_config 

13from ...config import save_config 

14from ...exceptions import ConfigError 

15from ...exceptions import OverwriteError 

16from ...format import output_format_emoji 

17from ...format import output_format_repr 

18from ...format import OutputFormat 

19from ...logs import logger 

20from ...logs import LogLevel 

21from ...output.console import console 

22from ...output.console import success 

23from ...output.formatting import path_link 

24from ...output.prompts import bool_prompt 

25from ...output.prompts import int_prompt 

26from ...output.prompts import path_prompt 

27from ...output.prompts import str_prompt 

28 

29 

30TITLE_STYLE = "bold" # Style for titles used by each config category 

31MAIN_TITLE_STYLE = "bold underline" # Style for the main title 

32 

33 

34@app.command("init") 

35def init( 

36 ctx: typer.Context, 

37 path: Optional[Path] = typer.Option( 

38 None, 

39 help="Path to create config file.", 

40 ), 

41 overwrite: bool = typer.Option( 

42 False, 

43 help="Overwrite existing config file.", 

44 is_flag=True, 

45 ), 

46 wizard: bool = typer.Option( 

47 True, 

48 "--wizard", 

49 help="Run the configuration wizard after creating the config file.", 

50 is_flag=True, 

51 ), 

52) -> None: 

53 """Initialize Harbor CLI configuration file. 

54 

55 Runs the configuration wizard by default unless otherwise specified. 

56 """ 

57 

58 logger.debug("Initializing Harbor CLI...") 

59 try: 

60 config_path = create_config(path, overwrite=overwrite) 

61 except OverwriteError: 

62 if not wizard: 

63 raise typer.Exit() 

64 # TODO: verify that this path is always correct 

65 p = path or DEFAULT_CONFIG_FILE 

66 console.print( 

67 f"WARNING: Config file already exists ({path_link(p)})", style="yellow" 

68 ) 

69 wizard = bool_prompt( 

70 "Are you sure you want to run the configuration wizard?", 

71 default=False, 

72 ) 

73 config_path = None 

74 else: 

75 logger.info(f"Created config file at {config_path}") 

76 

77 if wizard: 

78 run_config_wizard(config_path) 

79 

80 

81def run_config_wizard(config_path: Optional[Path] = None) -> None: 

82 """Loads the config file, and runs the configuration wizard. 

83 

84 Delegates to subroutines for each config category, that modify 

85 the loaded config object in-place.""" 

86 conf_exists = config_path is None 

87 

88 config = load_config(config_path) 

89 assert config.config_file is not None 

90 

91 console.print() 

92 console.rule(":sparkles: Harbor CLI Configuration Wizard :mage:") 

93 

94 # We only ask the user to configure mandatory sections if the config 

95 # file existed prior to running wizard. 

96 # Otherwise, we force the user to configure Harbor settings, because 

97 # we can't do anything without them. 

98 if not conf_exists or bool_prompt("\nConfigure harbor settings?", default=False): 

99 init_harbor_settings(config) 

100 console.print() 

101 

102 # These categories are optional, and as such we always ask the user 

103 if bool_prompt("Configure output settings?", default=False): 

104 init_output_settings(config) 

105 console.print() 

106 

107 if bool_prompt("Configure logging settings?", default=False): 

108 init_logging_settings(config) 

109 console.print() 

110 

111 conf_path = config_path or config.config_file 

112 if not conf_path: 

113 raise ConfigError("Could not determine config file path.") 

114 save_config(config, conf_path) 

115 console.print("Configuration complete! :tada:") 

116 success(f"Saved config to {path_link(conf_path)}") 

117 

118 

119def init_harbor_settings(config: HarborCLIConfig) -> None: 

120 """Initialize Harbor settings.""" 

121 console.print("\n:ship: Harbor Configuration", style=TITLE_STYLE) 

122 

123 hconf = config.harbor 

124 config.harbor.url = str_prompt( 

125 "Harbor API URL (e.g. https://harbor.example.com/api/v2.0)", 

126 default=hconf.url, 

127 show_default=True, 

128 ) 

129 

130 base_msg = "Authentication method [bold magenta](\[u]sername/password, \[b]asic auth, \[f]ile, \[s]kip)[/]" 

131 choices = ["u", "b", "f", "s"] 

132 

133 auth_method = str_prompt(base_msg, choices=choices, default="s", show_choices=False) 

134 if auth_method == "u": 

135 hconf.username = str_prompt( 

136 "Harbor username", 

137 default=hconf.username, 

138 empty_ok=False, 

139 ) 

140 hconf.secret = str_prompt( 

141 "Harbor secret", 

142 default=hconf.secret, 

143 password=True, 

144 empty_ok=False, 

145 ) # type: ignore # pydantic.SecretStr 

146 elif auth_method == "b": 

147 hconf.basicauth = str_prompt( 

148 f"Harbor Base64 Basic Auth token", 

149 default=hconf.basicauth, 

150 password=True, 

151 empty_ok=False, 

152 ) # type: ignore # pydantic.SecretStr 

153 elif auth_method == "f": 

154 hconf.credentials_file = path_prompt( 

155 "Harbor credentials file", 

156 default=hconf.credentials_file, 

157 show_default=True, 

158 must_exist=True, 

159 exist_ok=True, 

160 ) 

161 

162 # Explain what will happen if no auth method is provided 

163 if not hconf.has_auth_method: 

164 console.print( 

165 ":warning: No authentication info provided. " 

166 "You will be prompted for username and password when required.", 

167 style="yellow", 

168 ) 

169 

170 

171def init_logging_settings(config: HarborCLIConfig) -> None: 

172 """Initialize logging settings.""" 

173 console.print("\n:mag: Logging Configuration", style=TITLE_STYLE) 

174 

175 lconf = config.logging 

176 

177 lconf.enabled = bool_prompt( 

178 "Enable logging?", default=lconf.enabled, show_default=True 

179 ) 

180 

181 loglevel = str_prompt( 

182 "Logging level", 

183 choices=[lvl.value.lower() for lvl in LogLevel], 

184 default=lconf.level.value.lower(), # use lower to match choices 

185 show_default=True, 

186 ) 

187 lconf.level = LogLevel(loglevel.upper()) 

188 

189 

190def init_output_settings(config: HarborCLIConfig) -> None: 

191 """Initialize output settings.""" 

192 console.print("\n:desktop_computer: Output Configuration", style=TITLE_STYLE) 

193 

194 oconf = config.output 

195 

196 # Output format configuration has numerous sub-options, 

197 # so we delegate to a separate function. 

198 _init_output_format(config) 

199 

200 oconf.paging = bool_prompt( 

201 "Show output in pager? (requires 'less' or other pager to be installed and configured)", 

202 default=oconf.paging, 

203 show_default=True, 

204 ) 

205 # Custom pager support NYI (see: OutputSettings) 

206 if False and oconf.paging: 

207 use_custom = bool_prompt( 

208 "Use custom pager command?", default=False, show_default=True 

209 ) 

210 if use_custom: 

211 oconf.pager = str_prompt( 

212 "Pager command", 

213 default=oconf.pager, 

214 show_default=True, 

215 empty_ok=False, 

216 ) 

217 

218 

219def _init_output_format(config: HarborCLIConfig) -> None: 

220 oconf = config.output 

221 fmt_in = str_prompt( 

222 "Default output format", 

223 choices=[f.value for f in OutputFormat], 

224 default=oconf.format.value, 

225 ) 

226 oconf.format = OutputFormat(fmt_in) 

227 

228 def conf_fmt(fmt: OutputFormat) -> None: 

229 if fmt == OutputFormat.JSON: 

230 _init_output_json_settings(config) 

231 elif fmt == OutputFormat.TABLE: 

232 _init_output_table_settings(config) 

233 else: 

234 logger.error(f"Unknown configuration format {fmt.value}") 

235 

236 # Configure the chosen format first 

237 conf_fmt(oconf.format) 

238 

239 # Optionally configure other formats afterwards 

240 formats = [f for f in OutputFormat if f != oconf.format] 

241 for fmt in formats: 

242 if bool_prompt( 

243 f"Configure {output_format_repr(fmt)} output settings?", default=False 

244 ): 

245 conf_fmt(fmt) 

246 

247 

248def _init_output_json_settings(config: HarborCLIConfig) -> None: 

249 """Initialize JSON output settings.""" 

250 _print_output_title(OutputFormat.JSON) 

251 

252 oconf = config.output.JSON 

253 

254 oconf.indent = int_prompt( 

255 "Indentation", 

256 default=oconf.indent, 

257 show_default=True, 

258 ) 

259 

260 oconf.sort_keys = bool_prompt( 

261 "Sort keys", 

262 default=oconf.sort_keys, 

263 show_default=True, 

264 ) 

265 

266 

267def _init_output_table_settings(config: HarborCLIConfig) -> None: 

268 """Initialize table output settings.""" 

269 _print_output_title(OutputFormat.TABLE) 

270 

271 oconf = config.output.table 

272 

273 oconf.max_depth = int_prompt( 

274 "Max number of subtables [bold magenta](0 or omit for unlimited)[/]", 

275 default=oconf.max_depth, 

276 show_default=True, 

277 ) 

278 

279 oconf.description = bool_prompt( 

280 "Show descriptions", 

281 default=oconf.description, 

282 show_default=True, 

283 ) 

284 

285 oconf.compact = bool_prompt( 

286 "Compact tables", 

287 default=oconf.compact, 

288 show_default=True, 

289 ) 

290 

291 

292def _print_output_title(fmt: OutputFormat) -> None: 

293 fmt_repr = output_format_repr(fmt) 

294 emoji = output_format_emoji(fmt) 

295 console.print( 

296 f"\n:desktop_computer: {emoji} Output Configuration ({fmt_repr})", 

297 style=TITLE_STYLE, 

298 )