Skip to content

Commit

Permalink
[RFC] kci-click: add kci PoC using Click
Browse files Browse the repository at this point in the history
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
gctucker committed Sep 21, 2023
1 parent 77c16d9 commit f24dd7c
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 0 deletions.
224 changes: 224 additions & 0 deletions kci-click
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()
18 changes: 18 additions & 0 deletions kernelci-click.toml
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

0 comments on commit f24dd7c

Please sign in to comment.