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
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3import sys
4from pathlib import Path
5from typing import Optional
7import typer
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
27# Init subcommand groups here
28for group in commands.ALL_GROUPS:
29 app.add_typer(group)
31DEPRECATED_OPTIONS = {
32 "--harbor-url": "--url",
33 "--harbor-username": "--username",
34 "--harbor-secret": "--secret",
35}
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)
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
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
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)
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
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
224 # Instantiate the client
225 client = harbor.setup_client(state.config)
226 state.add_client(client)
228 # Run configuration based on config file
229 configure_from_config(state.config)
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()
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)