GeezerVison is showing github.com — feat(cli): allow options from args and environment variables · python-gitlab/python-gitlab@c6de2f5
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: docs/cli-usage.rst +50-7Lines changed: 50 additions & 7 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | #################### | 4 | 4 |
| 5 | 5 | ``python-gitlab`` provides a :command:`gitlab` command-line tool to interact | 6 | | -with GitLab servers. It uses a configuration file to define how to connect to | 7 | | -the servers. Without a configuration file, ``gitlab`` will default to | 8 | | -https://gitlab.com and unauthenticated requests. | | 6 | +with GitLab servers. | | 7 | + | | 8 | +This is especially convenient for running quick ad-hoc commands locally, easily | | 9 | +interacting with the API inside GitLab CI, or with more advanced shell scripting | | 10 | +when integrating with other tooling. | 9 | 11 |
| 10 | 12 | .. _cli_configuration: | 11 | 13 |
| 12 | 14 | Configuration | 13 | 15 | ============= | 14 | 16 |
| 15 | | -Files | 16 | | ------ | | 17 | +``gitlab`` allows setting configuration options via command-line arguments, | | 18 | +environment variables, and configuration files. | | 19 | + | | 20 | +For a complete list of global CLI options and their environment variable | | 21 | +equivalents, see :doc:`/cli-objects`. | | 22 | + | | 23 | +With no configuration provided, ``gitlab`` will default to unauthenticated | | 24 | +requests against `GitLab.com <https://gitlab.com>`__. | | 25 | + | | 26 | +With no configuration but running inside a GitLab CI job, it will default to | | 27 | +authenticated requests using the current job token against the current instance | | 28 | +(via ``CI_SERVER_URL`` and ``CI_JOB_TOKEN`` environment variables). | | 29 | + | | 30 | +.. warning:: | | 31 | + Please note the job token has very limited permissions and can only be used | | 32 | + with certain endpoints. You may need to provide a personal access token instead. | | 33 | + | | 34 | +When you provide configuration, values are evaluated with the following precedence: | | 35 | + | | 36 | +1. Explicitly provided CLI arguments, | | 37 | +2. Environment variables, | | 38 | +3. Configuration files: | | 39 | + | | 40 | + a. explicitly defined config files: | | 41 | + | | 42 | + i. via the ``--config-file`` CLI argument, | | 43 | + ii. via the ``PYTHON_GITLAB_CFG`` environment variable, | | 44 | + | | 45 | + b. user-specific config file, | | 46 | + c. system-level config file, | | 47 | + | | 48 | +4. Environment variables always present in CI (CI_SERVER_URL, CI_JOB_TOKEN). | | 49 | + | | 50 | +Additionally, authentication will take the following precedence | | 51 | +when multiple options or environment variables are present: | | 52 | + | | 53 | +1. Private token, | | 54 | +2. OAuth token, | | 55 | +3. CI job token. | | 56 | + | | 57 | + | | 58 | +Configuration files | | 59 | +------------------- | 17 | 60 |
| 18 | 61 | ``gitlab`` looks up 3 configuration files by default: | 19 | 62 |
| @@ -35,8 +78,8 @@ You can use a different configuration file with the ``--config-file`` option. | 35 | 78 | If the environment variable is defined and the target file cannot be accessed, | 36 | 79 | ``gitlab`` will fail explicitly. | 37 | 80 |
| 38 | | -Content | 39 | | -------- | | 81 | +Configuration file format | | 82 | +------------------------- | 40 | 83 |
| 41 | 84 | The configuration file uses the ``INI`` format. It contains at least a | 42 | 85 | ``[global]`` section, and a specific section for each GitLab server. For | |
Collapse file Copy file name to clipboardExpand all lines: gitlab/cli.py +103-5Lines changed: 103 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 19 | 19 |
| 20 | 20 | import argparse | 21 | 21 | import functools | | 22 | +import os | 22 | 23 | import re | 23 | 24 | import sys | 24 | 25 | from types import ModuleType | @@ -112,17 +113,25 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser: | 112 | 113 | "-v", | 113 | 114 | "--verbose", | 114 | 115 | "--fancy", | 115 | | - help="Verbose mode (legacy format only)", | | 116 | + help="Verbose mode (legacy format only) [env var: GITLAB_VERBOSE]", | 116 | 117 | action="store_true", | | 118 | + default=os.getenv("GITLAB_VERBOSE"), | 117 | 119 | ) | 118 | 120 | parser.add_argument( | 119 | | - "-d", "--debug", help="Debug mode (display HTTP requests)", action="store_true" | | 121 | + "-d", | | 122 | + "--debug", | | 123 | + help="Debug mode (display HTTP requests) [env var: GITLAB_DEBUG]", | | 124 | + action="store_true", | | 125 | + default=os.getenv("GITLAB_DEBUG"), | 120 | 126 | ) | 121 | 127 | parser.add_argument( | 122 | 128 | "-c", | 123 | 129 | "--config-file", | 124 | 130 | action="append", | 125 | | - help="Configuration file to use. Can be used multiple times.", | | 131 | + help=( | | 132 | + "Configuration file to use. Can be used multiple times. " | | 133 | + "[env var: PYTHON_GITLAB_CFG]" | | 134 | + ), | 126 | 135 | ) | 127 | 136 | parser.add_argument( | 128 | 137 | "-g", | @@ -151,7 +160,86 @@ def _get_base_parser(add_help: bool = True) -> argparse.ArgumentParser: | 151 | 160 | ), | 152 | 161 | required=False, | 153 | 162 | ) | | 163 | + parser.add_argument( | | 164 | + "--server-url", | | 165 | + help=("GitLab server URL [env var: GITLAB_URL]"), | | 166 | + required=False, | | 167 | + default=os.getenv("GITLAB_URL"), | | 168 | + ) | | 169 | + parser.add_argument( | | 170 | + "--ssl-verify", | | 171 | + help=( | | 172 | + "Whether SSL certificates should be validated. [env var: GITLAB_SSL_VERIFY]" | | 173 | + ), | | 174 | + required=False, | | 175 | + default=os.getenv("GITLAB_SSL_VERIFY"), | | 176 | + ) | | 177 | + parser.add_argument( | | 178 | + "--timeout", | | 179 | + help=( | | 180 | + "Timeout to use for requests to the GitLab server. " | | 181 | + "[env var: GITLAB_TIMEOUT]" | | 182 | + ), | | 183 | + required=False, | | 184 | + default=os.getenv("GITLAB_TIMEOUT"), | | 185 | + ) | | 186 | + parser.add_argument( | | 187 | + "--api-version", | | 188 | + help=("GitLab API version [env var: GITLAB_API_VERSION]"), | | 189 | + required=False, | | 190 | + default=os.getenv("GITLAB_API_VERSION"), | | 191 | + ) | | 192 | + parser.add_argument( | | 193 | + "--per-page", | | 194 | + help=( | | 195 | + "Number of entries to return per page in the response. " | | 196 | + "[env var: GITLAB_PER_PAGE]" | | 197 | + ), | | 198 | + required=False, | | 199 | + default=os.getenv("GITLAB_PER_PAGE"), | | 200 | + ) | | 201 | + parser.add_argument( | | 202 | + "--pagination", | | 203 | + help=( | | 204 | + "Whether to use keyset or offset pagination [env var: GITLAB_PAGINATION]" | | 205 | + ), | | 206 | + required=False, | | 207 | + default=os.getenv("GITLAB_PAGINATION"), | | 208 | + ) | | 209 | + parser.add_argument( | | 210 | + "--order-by", | | 211 | + help=("Set order_by globally [env var: GITLAB_ORDER_BY]"), | | 212 | + required=False, | | 213 | + default=os.getenv("GITLAB_ORDER_BY"), | | 214 | + ) | | 215 | + parser.add_argument( | | 216 | + "--user-agent", | | 217 | + help=( | | 218 | + "The user agent to send to GitLab with the HTTP request. " | | 219 | + "[env var: GITLAB_USER_AGENT]" | | 220 | + ), | | 221 | + required=False, | | 222 | + default=os.getenv("GITLAB_USER_AGENT"), | | 223 | + ) | 154 | 224 |
| | 225 | + tokens = parser.add_mutually_exclusive_group() | | 226 | + tokens.add_argument( | | 227 | + "--private-token", | | 228 | + help=("GitLab private access token [env var: GITLAB_PRIVATE_TOKEN]"), | | 229 | + required=False, | | 230 | + default=os.getenv("GITLAB_PRIVATE_TOKEN"), | | 231 | + ) | | 232 | + tokens.add_argument( | | 233 | + "--oauth-token", | | 234 | + help=("GitLab OAuth token [env var: GITLAB_OAUTH_TOKEN]"), | | 235 | + required=False, | | 236 | + default=os.getenv("GITLAB_OAUTH_TOKEN"), | | 237 | + ) | | 238 | + tokens.add_argument( | | 239 | + "--job-token", | | 240 | + help=("GitLab CI job token [env var: CI_JOB_TOKEN]"), | | 241 | + required=False, | | 242 | + ) | 155 | 243 | return parser | 156 | 244 |
| 157 | 245 |
| @@ -243,13 +331,23 @@ def main() -> None: | 243 | 331 | "whaction", | 244 | 332 | "version", | 245 | 333 | "output", | | 334 | + "fields", | | 335 | + "server_url", | | 336 | + "ssl_verify", | | 337 | + "timeout", | | 338 | + "api_version", | | 339 | + "pagination", | | 340 | + "user_agent", | | 341 | + "private_token", | | 342 | + "oauth_token", | | 343 | + "job_token", | 246 | 344 | ): | 247 | 345 | args_dict.pop(item) | 248 | 346 | args_dict = {k: _parse_value(v) for k, v in args_dict.items() if v is not None} | 249 | 347 |
| 250 | 348 | try: | 251 | | - gl = gitlab.Gitlab.from_config(gitlab_id, config_files) | 252 | | - if gl.private_token or gl.oauth_token or gl.job_token: | | 349 | + gl = gitlab.Gitlab.merge_config(options, gitlab_id, config_files) | | 350 | + if gl.private_token or gl.oauth_token: | 253 | 351 | gl.auth() | 254 | 352 | except Exception as e: | 255 | 353 | die(str(e)) | |
Collapse file Copy file name to clipboardExpand all lines: gitlab/client.py +83Lines changed: 83 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 16 | 16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 | 17 | """Wrapper for the GitLab API.""" | 18 | 18 |
| | 19 | +import os | 19 | 20 | import time | | 21 | +from argparse import Namespace | 20 | 22 | from typing import Any, cast, Dict, List, Optional, Tuple, TYPE_CHECKING, Union | 21 | 23 |
| 22 | 24 | import requests | @@ -256,6 +258,87 @@ def from_config( | 256 | 258 | retry_transient_errors=config.retry_transient_errors, | 257 | 259 | ) | 258 | 260 |
| | 261 | + @classmethod | | 262 | + def merge_config( | | 263 | + cls, | | 264 | + options: Namespace, | | 265 | + gitlab_id: Optional[str] = None, | | 266 | + config_files: Optional[List[str]] = None, | | 267 | + ) -> "Gitlab": | | 268 | + """Create a Gitlab connection by merging configuration with | | 269 | + the following precedence: | | 270 | +
| | 271 | + 1. Explicitly provided CLI arguments, | | 272 | + 2. Environment variables, | | 273 | + 3. Configuration files: | | 274 | + a. explicitly defined config files: | | 275 | + i. via the `--config-file` CLI argument, | | 276 | + ii. via the `PYTHON_GITLAB_CFG` environment variable, | | 277 | + b. user-specific config file, | | 278 | + c. system-level config file, | | 279 | + 4. Environment variables always present in CI (CI_SERVER_URL, CI_JOB_TOKEN). | | 280 | +
| | 281 | + Args: | | 282 | + options list[str]: List of options provided via the CLI. | | 283 | + gitlab_id (str): ID of the configuration section. | | 284 | + config_files list[str]: List of paths to configuration files. | | 285 | + Returns: | | 286 | + (gitlab.Gitlab): A Gitlab connection. | | 287 | +
| | 288 | + Raises: | | 289 | + gitlab.config.GitlabDataError: If the configuration is not correct. | | 290 | + """ | | 291 | + config = gitlab.config.GitlabConfigParser( | | 292 | + gitlab_id=gitlab_id, config_files=config_files | | 293 | + ) | | 294 | + url = ( | | 295 | + options.server_url | | 296 | + or config.url | | 297 | + or os.getenv("CI_SERVER_URL") | | 298 | + or gitlab.const.DEFAULT_URL | | 299 | + ) | | 300 | + private_token, oauth_token, job_token = cls._get_auth_from_env(options, config) | | 301 | + | | 302 | + return cls( | | 303 | + url=url, | | 304 | + private_token=private_token, | | 305 | + oauth_token=oauth_token, | | 306 | + job_token=job_token, | | 307 | + ssl_verify=options.ssl_verify or config.ssl_verify, | | 308 | + timeout=options.timeout or config.timeout, | | 309 | + api_version=options.api_version or config.api_version, | | 310 | + per_page=options.per_page or config.per_page, | | 311 | + pagination=options.pagination or config.pagination, | | 312 | + order_by=options.order_by or config.order_by, | | 313 | + user_agent=options.user_agent or config.user_agent, | | 314 | + ) | | 315 | + | | 316 | + @staticmethod | | 317 | + def _get_auth_from_env( | | 318 | + options: Namespace, config: gitlab.config.GitlabConfigParser | | 319 | + ) -> Tuple: | | 320 | + """ | | 321 | + Return a tuple where at most one of 3 token types ever has a value. | | 322 | + Since multiple types of tokens may be present in the environment, | | 323 | + options, or config files, this precedence ensures we don't | | 324 | + inadvertently cause errors when initializing the client. | | 325 | +
| | 326 | + This is especially relevant when executed in CI where user and | | 327 | + CI-provided values are both available. | | 328 | + """ | | 329 | + private_token = options.private_token or config.private_token | | 330 | + oauth_token = options.oauth_token or config.oauth_token | | 331 | + job_token = options.job_token or config.job_token or os.getenv("CI_JOB_TOKEN") | | 332 | + | | 333 | + if private_token: | | 334 | + return (private_token, None, None) | | 335 | + if oauth_token: | | 336 | + return (None, oauth_token, None) | | 337 | + if job_token: | | 338 | + return (None, None, job_token) | | 339 | + | | 340 | + return (None, None, None) | | 341 | + | 259 | 342 | def auth(self) -> None: | 260 | 343 | """Performs an authentication using private token. | 261 | 344 |
| |
Collapse file Copy file name to clipboardExpand all lines: gitlab/config.py +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 23 | 23 | from pathlib import Path | 24 | 24 | from typing import List, Optional, Union | 25 | 25 |
| 26 | | -from gitlab.const import DEFAULT_URL, USER_AGENT | | 26 | +from gitlab.const import USER_AGENT | 27 | 27 |
| 28 | 28 | _DEFAULT_FILES: List[str] = [ | 29 | 29 | "/etc/python-gitlab.cfg", | @@ -119,7 +119,7 @@ def __init__( | 119 | 119 | self.retry_transient_errors: bool = False | 120 | 120 | self.ssl_verify: Union[bool, str] = True | 121 | 121 | self.timeout: int = 60 | 122 | | - self.url: str = DEFAULT_URL | | 122 | + self.url: Optional[str] = None | 123 | 123 | self.user_agent: str = USER_AGENT | 124 | 124 |
| 125 | 125 | self._files = _get_config_files(config_files) | |
|