Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pylint:
pylint --reports=y \
kci \
kernelci.api \
kernelci.cli \
kernelci.config.api \
kernelci.config.job \
kernelci.config.runtime \
Expand All @@ -46,7 +47,6 @@ unit-tests:
python3 -m pytest tests

validate-yaml:
./kci config validate
./kci_build validate
./kci_test validate
./kci_data validate
Expand Down
39 changes: 24 additions & 15 deletions kci
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,37 @@
#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Copyright (C) 2022 Collabora Limited
# Copyright (C) 2022, 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/.
This executable module is the entry point for the kci command line tool. It
provides a way to manually interact with the KernelCI API and its related
features. See the online documentation for more details:
https://kernelci.org/docs/api/.
"""

import argparse
import sys
import json

import kernelci.legacy.cli
import click

from kernelci.cli import Args, kci
import kernelci.api
import kernelci.config


@kci.command(secrets=True)
@Args.config
@Args.api
def whoami(config, api, secrets):
"""Use whoami to get current user info with API authentication"""
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))


if __name__ == '__main__':
parser = argparse.ArgumentParser("KernelCI Command Line Tools")
parser.add_argument(
'command',
choices=kernelci.legacy.cli.list_command_names(),
help="Command to run"
)
args = parser.parse_args(sys.argv[1:2])
kernelci.legacy.cli.call(args.command, sys.argv[2:])
kci() # pylint: disable=no-value-for-parameter
131 changes: 131 additions & 0 deletions kernelci/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Copyright (C) 2022, 2023 Collabora Limited
# Author: Guillaume Tucker <guillaume.tucker@collabora.com>
# Author: Jeny Sadadia <jeny.sadadia@collabora.com>

"""KernelCI command line utilities

This module provides base features for KernelCI command line tools. Based on
the Click framework, it adds support for loading default values and secrets
from TOML settings.
"""

import click

import kernelci.settings


class Args: # pylint: disable=too-few-public-methods
"""Common command line arguments"""
api = click.option(
'--api',
help="Name of the API config entry"
)
config = click.option(
'-c', '--yaml-config', 'config',
help="Path to the YAML pipeline configuration"
)
indent = click.option(
'--indent', type=int,
help="Intentation level for structured data output"
)
settings = click.option(
'-s', '--toml-settings', 'settings',
help="Path to the TOML user settings"
)
verbose = click.option(
'-v', '--verbose/--no-verbose', default=None,
help="Print more details output"
)


class CommandSettings:
"""Settings object passed to commands via the context"""

def __init__(self, settings_path):
self._settings = kernelci.settings.Settings(settings_path)

def __getattr__(self, *args):
"""Get a settings value for the current command group"""
return self.get(*args)

@property
def settings(self):
"""TOML Settings object"""
return self._settings

@property
def secrets(self):
"""Secrets loaded from TOML settings"""
return self._secrets

def get(self, *args):
"""Get a settings value like __getattr__()"""
return self._settings.get(*args)

def get_secrets(self, params, root):
"""Get a Secrets object with the secrets loaded from the settings"""
return kernelci.settings.Secrets(self._settings, params, root)


class Kci(click.Command):
"""Wrapper command to load settings and populate default values"""

def __init__(self, *args, kci_secrets: bool = False, **kwargs):
super().__init__(*args, **kwargs)
self._kci_secrets = kci_secrets

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):
path = self._walk_name(ctx)
for key, value in ctx.params.items():
if value is None:
ctx.params[key] = ctx.obj.get(*path, key)
if self._kci_secrets:
root = (path[0], 'secrets')
ctx.params['secrets'] = ctx.obj.get_secrets(ctx.params, root)
super().invoke(ctx)


class KciS(Kci):
"""Wrapper command with `secrets` as additional function argument"""

def __init__(self, *args, **kwargs):
super().__init__(*args, kci_secrets=True, **kwargs)


# pylint: disable=not-callable
class KciGroup(click.core.Group):
"""Click group to create commands with Kci or KciS classes"""

def command(self, *args, cls=Kci, secrets=False, **kwargs):
"""Wrapper for the command decorator to use Kci or KciS

If secrets is set to True, the class used for the command is always
KciS. Otherwise, Kci is used by default.
"""
kwargs['cls'] = KciS if secrets is True else cls
decorator = super().command(**kwargs)
return decorator if not args else decorator(args[0])

def group(self, *args, cls=None, **kwargs):
"""Wrapper to use KciGroup for sub-groups"""
kwargs['cls'] = cls or self.__class__
decorator = super().group(**kwargs)
return decorator if not args else decorator(args[0])


@click.group(cls=KciGroup)
@Args.settings
@click.pass_context
def kci(ctx, settings):
"""Entry point for the kci command line tool"""
ctx.obj = CommandSettings(settings)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
azure-storage-file-share==12.13.0
click==8.1.3
cloudevents==1.9.0
docker==6.1.2
jinja2==3.1.2
Expand Down
5 changes: 5 additions & 0 deletions tests/kernelci-cli.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
["python -m pytest".hey]
hello = 123

["python -m pytest".secrets]
foo.bar.baz = 'FooBarBaz'
62 changes: 62 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# Copyright (C) 2023 Collabora Limited
# Author: Guillaume Tucker <guillaume.tucker@collabora.com>

"""Unit test for the KernelCI command line tools"""

import click

import kernelci.cli
import kernelci.settings


def test_command_settings_init():
"""Verify that the CommandSettings class can be initialised"""
ctx = kernelci.cli.CommandSettings('tests/kernelci-cli.toml')
assert ctx is not None
secrets = ctx.get_secrets({}, ())
assert secrets is not None
assert isinstance(secrets, kernelci.settings.Secrets)


def test_kci_command():
"""Test the kernelci.cli.Kci base class"""

@kernelci.cli.kci.command(cls=kernelci.cli.Kci, help="Unit test")
@click.option('--hello', type=int)
@click.option('--hack', type=str)
def hey(hello, hack):
assert isinstance(hello, int)
assert hello == 123
assert isinstance(hack, str)
assert hack == 'Hack'

try:
kernelci.cli.kci(args=[ # pylint: disable=no-value-for-parameter
'--toml-settings', 'tests/kernelci-cli.toml',
'hey', '--hack', 'Hack'
])
except SystemExit as exc:
if exc.code != 0:
raise exc


def test_kci_command_with_secrets():
"""Test the kernelci.cli.KciS class"""

@kernelci.cli.kci.command(cls=kernelci.cli.KciS, help="With secrets")
@click.option('--foo', type=str)
def cmd(foo, secrets): # pylint: disable=disallowed-name
assert isinstance(foo, str)
assert foo == 'bar'
assert secrets.foo.baz == 'FooBarBaz'

try:
kernelci.cli.kci(args=[ # pylint: disable=no-value-for-parameter
'--toml-settings', 'tests/kernelci-cli.toml',
'cmd', '--foo', 'bar'
])
except SystemExit as exc:
if exc.code != 0:
raise exc