Coverage for harbor_cli/main.py: 29%

84 statements  

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

1from __future__ import annotations 

2 

3import sys 

4from pathlib import Path 

5from typing import Optional 

6 

7import typer 

8 

9from . import commands 

10from . import harbor 

11from .app import app 

12from .config import env_var 

13from .config import HarborCLIConfig 

14from .deprecation import check_deprecated_option, Deprecated 

15from .exceptions import handle_exception 

16from .exceptions import HarborCLIError 

17from .format import OutputFormat 

18from .logs import disable_logging 

19from .logs import logger 

20from .logs import setup_logging 

21from .output.console import exit_err 

22from .output.console import success 

23from .output.formatting.path import path_link 

24from .state import state 

25from .style import help_config_override 

26 

27# Init subcommand groups here 

28for group in commands.ALL_GROUPS: 

29 app.add_typer(group) 

30 

31DEPRECATED_OPTIONS = { 

32 "--harbor-url": "--url", 

33 "--harbor-username": "--username", 

34 "--harbor-secret": "--secret", 

35} 

36 

37 

38# The callback defines global command options 

39@app.callback(no_args_is_help=True) 

40def main_callback( 

41 ctx: typer.Context, 

42 # Configuration options 

43 config_file: Optional[Path] = typer.Option( 

44 None, 

45 "--config", 

46 "-c", 

47 help="Path to config file.", 

48 envvar=env_var("config"), 

49 ), 

50 # Harbor options 

51 harbor_url: Optional[str] = typer.Option( 

52 None, 

53 "--url", 

54 "-u", 

55 Deprecated("--harbor-url", replacement="--url"), 

56 help=f"Harbor API URL. {help_config_override('harbor.url')}", 

57 envvar=env_var("url"), 

58 ), 

59 harbor_username: Optional[str] = typer.Option( 

60 None, 

61 "--username", 

62 "-U", 

63 Deprecated("--harbor-username", replacement="--username"), 

64 help=f"Harbor username. {help_config_override('harbor.username')}", 

65 envvar=env_var("username"), 

66 ), 

67 harbor_secret: Optional[str] = typer.Option( 

68 None, 

69 "--secret", 

70 "-S", 

71 Deprecated("--harbor-secret", replacement="--secret"), 

72 help=f"Harbor secret (password). {help_config_override('harbor.secret')}", 

73 envvar=env_var("secret"), 

74 ), 

75 harbor_basicauth: Optional[str] = typer.Option( 

76 None, 

77 "--basicauth", 

78 "-B", 

79 help=f"Harbor basic access credentials (base64). {help_config_override('harbor.basicauth')}", 

80 envvar=env_var("basicauth"), 

81 ), 

82 harbor_credentials_file: Optional[Path] = typer.Option( 

83 None, 

84 "--credentials-file", 

85 "-F", 

86 help=f"Path to Harbor JSON credentials file. {help_config_override('harbor.credentials_file')}", 

87 envvar=env_var("credentials_file"), 

88 ), 

89 # Formatting 

90 show_description: Optional[bool] = typer.Option( 

91 None, 

92 "--table-description/--no-table-description", 

93 help=( 

94 "Include field descriptions in tables. " 

95 f"{help_config_override('output.table.description')}" 

96 ), 

97 envvar=env_var("table_description"), 

98 ), 

99 max_depth: Optional[int] = typer.Option( 

100 None, 

101 "--table-max-depth", 

102 help=( 

103 "Maximum depth to print nested objects in tables. " 

104 f"{help_config_override('output.table.max_depth')}" 

105 ), 

106 envvar=env_var("table_max_depth"), 

107 ), 

108 compact: Optional[bool] = typer.Option( 

109 None, 

110 "--table-compact/--no-table-compact", 

111 help=( 

112 "Compact table output. Has no effect on other formats. " 

113 f"{help_config_override('output.table.compact')}" 

114 ), 

115 envvar=env_var("table_compact"), 

116 ), 

117 json_indent: Optional[int] = typer.Option( 

118 None, 

119 "--json-indent", 

120 help=f"Indentation level for JSON output. {help_config_override('output.json.indent')}", 

121 envvar=env_var("json_indent"), 

122 ), 

123 json_sort_keys: Optional[bool] = typer.Option( 

124 None, 

125 "--json-sort-keys/--no-json-sort-keys", 

126 help=f"Sort keys in JSON output. {help_config_override('output.json.sort_keys')}", 

127 envvar=env_var("json_sort_keys"), 

128 ), 

129 # Output options 

130 output_format: Optional[OutputFormat] = typer.Option( 

131 None, 

132 "--format", 

133 "-f", 

134 help=f"Specifies the output format to use. {help_config_override('output.format')}", 

135 envvar=env_var("output_format"), 

136 case_sensitive=False, 

137 ), 

138 output_file: Optional[Path] = typer.Option( 

139 None, 

140 "--output", 

141 "-o", 

142 help="Output file, by default None, which means output to stdout. If the file already exists, it will be overwritten.", 

143 ), 

144 no_overwrite: bool = typer.Option( 

145 False, 

146 "--no-overwrite", 

147 help="Do not overwrite the output file if it already exists.", 

148 ), 

149 # stdout/stderr options 

150 verbose: bool = typer.Option( 

151 False, "--verbose", "-v", help="Enable verbose output." 

152 ), 

153 with_stdout: bool = typer.Option( 

154 False, 

155 "--with-stdout", 

156 help="Output to stdout in addition to the specified output file, if any. Has no effect if no output file is specified.", 

157 ), 

158) -> None: 

159 """ 

160 Configuration options that affect all commands. 

161 """ 

162 setup_logging() 

163 check_deprecated_option(ctx) 

164 

165 # These commands don't require state management 

166 # and can be run without a config file or client. 

167 if ctx.invoked_subcommand in ["sample-config", "init", "find"]: 167 ↛ 176line 167 didn't jump to line 176, because the condition on line 167 was never false

168 return 

169 

170 # TODO: find a better way to do this 

171 # We don't want to run the rest of the callback if the user is asking 

172 # for help, so we check for the help option names and exit early if 

173 # any are present. The problem is that if the --help option is passed 

174 # to a subcommand, we can't access it through the ctx object here, 

175 # so we have to check the sys.argv list. 

176 if any(help_arg in sys.argv for help_arg in ctx.help_option_names): 

177 return 

178 

179 # if we're in the REPL, we don't want to load config again 

180 if not state.config_loaded: 

181 try: 

182 conf = HarborCLIConfig.from_file(config_file) 

183 except FileNotFoundError: 

184 # Create a new config file, but don't run wizard 

185 logger.info("Config file not found. Creating new config file.") 

186 conf = HarborCLIConfig.from_file(config_file, create=True) 

187 if conf.config_file is None: 

188 exit_err("Unable to create config file.") 

189 success(f"Created config file at {path_link(conf.config_file)}") 

190 logger.info("Proceeding with default configuration.") 

191 logger.info("Run 'harbor init' to configure Harbor CLI. ") 

192 state.add_config(conf) 

193 

194 # Set config overrides 

195 if harbor_url is not None: 

196 state.config.harbor.url = harbor_url 

197 if harbor_username is not None: 

198 state.config.harbor.username = harbor_username 

199 if harbor_secret is not None: 

200 state.config.harbor.secret = harbor_secret # type: ignore 

201 if harbor_basicauth is not None: 

202 state.config.harbor.basicauth = harbor_basicauth # type: ignore 

203 if harbor_credentials_file is not None: 

204 state.config.harbor.credentials_file = harbor_credentials_file 

205 if compact is not None: 

206 state.config.output.table.compact = compact 

207 if show_description is not None: 

208 state.config.output.table.description = show_description 

209 if max_depth is not None: 

210 state.config.output.table.max_depth = max_depth 

211 if json_indent is not None: 

212 state.config.output.JSON.indent = json_indent 

213 if json_sort_keys is not None: 

214 state.config.output.JSON.sort_keys = json_sort_keys 

215 if output_format is not None: 

216 state.config.output.format = output_format 

217 

218 # Set global options 

219 state.options.verbose = verbose 

220 state.options.output_file = output_file 

221 state.options.no_overwrite = no_overwrite 

222 state.options.with_stdout = with_stdout 

223 

224 # Instantiate the client 

225 client = harbor.setup_client(state.config) 

226 state.add_client(client) 

227 

228 # Run configuration based on config file 

229 configure_from_config(state.config) 

230 

231 

232def configure_from_config(config: HarborCLIConfig) -> None: 

233 """Configure the program from a config file.""" 

234 # TODO: Include more setup here 

235 if config.logging.enabled: 

236 setup_logging(config.logging.level) 

237 else: 

238 disable_logging() 

239 

240 

241def main() -> None: 

242 """Main entry point for the CLI.""" 

243 try: 

244 app() 

245 except HarborCLIError as e: 

246 # exceptions of this type are expected, and if they're 

247 # not handled internally (i.e. other function calls exit()), 

248 # we want to only display their message and exit with a 

249 # non-zero status code. 

250 exit_err(str(e)) 

251 except Exception as e: 

252 handle_exception(e)