-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[RFC] kci-click: add kci PoC using Click
Add a kci-click proof-of-concept replacement for kci using the Click package. It comes with a sample kernelci-click.toml settings file to illustrate how the values can be combined with the command line arguments. Some sample commands: $ KCI_SETTINGS=kernelci-click.toml ./kci-click foo bar --verbose FOO command group FOO BAR BAZ: 789 $ ./kci-click whoami --no-verbose { "id": "64ef04e7391d44b7fa620d13", "active": true, "profile": { "username": "admin", "hashed_password": "<hashed-password>", "groups": [ { "id": "6499aa9da02fef8143c1feb0", "name": "admin" } ], "email": "something@domain.com" } } Signed-off-by: Guillaume Tucker <guillaume.tucker@collabora.com>
- Loading branch information
Showing
2 changed files
with
242 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
#!/usr/bin/env python3 | ||
# | ||
# SPDX-License-Identifier: LGPL-2.1-or-later | ||
# | ||
# Copyright (C) 2023 Collabora Limited | ||
# Author: Guillaume Tucker <guillaume.tucker@collabora.com> | ||
|
||
"""KernelCI Command Line Tool | ||
This executable script is the entry point for all the new KernelCI command line | ||
tools which support the new API & Pipeline design. See the documentation for | ||
more details: https://kernelci.org/docs/api/. | ||
""" | ||
|
||
import json | ||
import os | ||
|
||
import toml | ||
import click | ||
|
||
import kernelci.api | ||
import kernelci.config | ||
|
||
|
||
class Settings: | ||
|
||
def __init__(self, path=None, default_group_name='DEFAULT'): | ||
"""TOML settings | ||
`path` is the path to the TOML settings file | ||
`default_group_name` is the name of the default group | ||
""" | ||
if path is None: | ||
default_paths = [ | ||
os.getenv('KCI_SETTINGS', ''), | ||
'kernelci.toml', | ||
os.path.expanduser('~/.config/kernelci/kernelci.toml'), | ||
'/etc/kernelci/kernelci.toml', | ||
'kernelci.conf', | ||
os.path.expanduser('~/.config/kernelci/kernelci.conf'), | ||
'/etc/kernelci/kernelci.conf', | ||
] | ||
for default_path in default_paths: | ||
if os.path.exists(default_path): | ||
path = default_path | ||
break | ||
self._path = path | ||
self._settings = toml.load(path) if os.path.exists(path or '') else {} | ||
self._default = self._settings.get(default_group_name, {}) | ||
self._group = {} | ||
|
||
@property | ||
def path(self): | ||
"""Path to the TOML settings file""" | ||
return self._path | ||
|
||
def set_group(self, path): | ||
self._group = self.get_raw(*path) or {} | ||
|
||
def get(self, key): | ||
"""Get a TOML settings value | ||
`key` is the name of the settings key | ||
`group_path` is the name of the group where to find the key | ||
""" | ||
value = self._group.get(key) | ||
if value is None: | ||
value = self._default.get(key) | ||
return value | ||
|
||
def get_section(self, section): | ||
"""Get a settings group for a particular config section | ||
`section` is the name of the configuration section e.g. 'api' | ||
""" | ||
section_name = self.get(section) | ||
if section_name is None: | ||
raise ValueError(f"No section name specified for {section}") | ||
section_group = self._settings.get(section, {}) | ||
return section_group.get(section_name, {}) | ||
|
||
def get_from_section(self, section, key): | ||
"""Get a settings value from a particular config section | ||
`section` is the name of the configuration section e.g. 'api' | ||
`key` is the name of the settings within that group | ||
""" | ||
return self.get_section(section).get(key) | ||
|
||
def get_raw(self, *args): | ||
"""Get a settings value from an arbitrary series of keys | ||
The *args are a series of strings for the path, e.g. ('foo', 'bar', | ||
'baz') will look for a foo.bar.baz value or baz within [foo.bar] etc. | ||
""" | ||
data = self._settings | ||
for arg in args: | ||
data = data.get(arg, {}) | ||
return data | ||
|
||
|
||
class CommandSettings: | ||
"""Settings object passed to commands via the context""" | ||
|
||
class SectionFinder: | ||
"""Helper class to find a secrets section""" | ||
class Group: | ||
"""Helper class to find a key within a group""" | ||
def __init__(self, group): | ||
self._group = group | ||
|
||
def __getattr__(self, key): | ||
return self._group.get(key) | ||
|
||
def __init__(self, settings): | ||
self._settings = settings | ||
|
||
def __getattr__(self, section): | ||
return self.Group(self._settings.get_section(section)) | ||
|
||
def __init__(self, settings_path): | ||
self._settings = Settings(settings_path) | ||
self._secrets = self.SectionFinder(self.settings) | ||
|
||
def __getattr__(self, key): | ||
"""Get a settings value for the current command group""" | ||
return self.get(key) | ||
|
||
@property | ||
def secrets(self): | ||
"""Shortcut to get a secrets section""" | ||
return self._secrets | ||
|
||
@property | ||
def settings(self): | ||
"""TOML Settings object""" | ||
return self._settings | ||
|
||
def set_group(self, path): | ||
"""Set the group based on the current command name""" | ||
self._settings.set_group(path) | ||
|
||
def get(self, key): | ||
"""Get a settings value like __getattr__()""" | ||
return self._settings.get(key) | ||
|
||
|
||
class KciCommand(click.Command): | ||
"""Wrapper command to load settings and populate default values""" | ||
|
||
kci_secrets = False | ||
|
||
def _walk_name(self, ctx): | ||
name = (ctx.info_name,) | ||
if ctx.parent: | ||
return self._walk_name(ctx.parent) + name | ||
return name | ||
|
||
def invoke(self, ctx): | ||
ctx.obj.set_group(self._walk_name(ctx)) | ||
if self.kci_secrets: | ||
ctx.params['secrets'] = ctx.obj.secrets | ||
for key, value in ctx.params.items(): | ||
if value is None: | ||
ctx.params[key] = ctx.obj.get(key) | ||
super().invoke(ctx) | ||
|
||
|
||
class KciSecrets(KciCommand): | ||
kci_secrets = True | ||
|
||
|
||
@click.group() | ||
@click.option('--settings', type=str, help="Path to the TOML settings file") | ||
@click.pass_context | ||
def kci(ctx, settings): | ||
"""Entry point for the kci command line tool""" | ||
ctx.info_name = 'kci' # HACK because this file is called kci-click... | ||
ctx.obj = CommandSettings(settings) | ||
|
||
|
||
class Args: | ||
"""Standard command line arguments""" | ||
api = click.option('--api', type=str, help="Name of the API config entry") | ||
config = click.option('--config', type=str, help="Path to the YAML config") | ||
verbose = click.option('--verbose/--no-verbose', type=bool, default=None) | ||
|
||
|
||
@kci.command(cls=KciSecrets, help="whoami with API authentication") | ||
@click.option('--config', type=str, help="Path to the YAML config") | ||
@click.option('--api', type=str, help="Name of the API config entry") | ||
def whoami(config, api, secrets): | ||
configs = kernelci.config.load(config) | ||
api_config = configs['api_configs'][api] | ||
api = kernelci.api.get_api(api_config, secrets.api.token) | ||
data = api.whoami() | ||
click.echo(json.dumps(data, indent=2)) | ||
|
||
|
||
@kci.command(cls=KciCommand) | ||
@Args.verbose | ||
@click.option('--bingo', type=int) | ||
def hack(verbose, bingo): | ||
click.echo(f"HACK VERBOSE {verbose}") | ||
click.echo(f"HACK BINGO {bingo}") | ||
|
||
|
||
@kci.group() | ||
def foo(): | ||
click.echo("FOO command group") | ||
|
||
|
||
@foo.command(cls=KciCommand) | ||
@click.option('--baz', type=int) | ||
@Args.verbose | ||
def bar(baz, verbose): | ||
if verbose: | ||
click.echo(f"FOO BAR BAZ: {baz}") | ||
else: | ||
click.echo(baz) | ||
|
||
|
||
if __name__ == '__main__': | ||
kci() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[DEFAULT] | ||
verbose = true | ||
api = 'early-access' | ||
storage = 'staging-azure' | ||
|
||
[kci.hack] | ||
bingo = 1234 | ||
|
||
[api] | ||
early-access.token = '1234abcde' | ||
|
||
# Alternative: | ||
# [api.early-access] | ||
# token = '1234abcd' | ||
|
||
[kci.foo.bar] | ||
baz = 789 | ||
verbose = false |