Skip to content

Commit

Permalink
Improve Contribution docs about version guards and pulp-glue
Browse files Browse the repository at this point in the history
Done in the 'Architecture' section of Contribution docs:
- Add 'Pulp Glue' section and next sections 'Context' and
  'Version Dependent' under it.
- Add clarificatons about version-guard to options and body-params.
- Improve info about PulpEntityContext usage.
- Improve `pulp_option` docstrings w/ description and examples.
- Fix typos, re-phrasings and add some internal links.

Co-authored-by: Matthias Dellweg <2500@gmx.de>
closes pulp#836
  • Loading branch information
pedro-psb committed Dec 12, 2023
1 parent 9b2a709 commit 7c29099
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGES/836.doc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improved `Developer Material/Architecture` section by adding clarification about `pulp-glue`, version guards and `pulp_option` factory.
61 changes: 42 additions & 19 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The Pulp CLI architecture is described in this section.

The Pulp CLI is designed with a plugin structure. Plugins can either live in the pulp-cli package or be shipped independently.
By convention, all parts of the CLI are packages in the open namespace `pulpcore.cli`.
A plugin can register itself with the main app by specifying by specifying its main module as a `pulp_cli.plugins` entrypoint in `setup.py`.
A plugin can register itself with the main app by specifying its main module as a `pulp_cli.plugins` entrypoint in `setup.py`.

```python
entry_points={
Expand All @@ -16,7 +16,7 @@ entry_points={
}
```

The plugin can then attach subcommands to the `pulpcore.cli.common.main` command by providing a `mount` method in the main module.
The plugin should then attach subcommands to the `pulpcore.cli.common.main` command by providing a `mount` method in the main module.

```python
from pulpcore.cli.common.generic import pulp_command
Expand All @@ -30,12 +30,22 @@ def mount(main: click.Group, **kwargs: Any) -> None:
main.add_command(my_command)
```

### Contexts
### Pulp Glue

Pulp CLI provides the `pulp-glue` library as an abstraction layer that let's you perform high level operations in pulp.
Its goal is to abstract interacting with the rest api by parsing the api docs and waiting on tasks and task groups.
It is shipped as a separate python package to allow broad use across multiple projects, such as `pulp-squeezer` and `pulpcore`.
To this end, `pulp-glue` is the go to place for all known version dependent Pulp API subtleties and their corresponding fixes (see Version dependent codepaths below).

#### Contexts

In `click`, every subcommand is accompanied by a `click.Context`, and objects can be attached to them.
In this CLI we attach a `PulpContext` to the main command.
It acts as an abstraction layer that handles the communication to the pulp server through its `api` property.
Further we encourage the handling of communication with certain endpoints by subclassing the `PulpEntityContext`.
In this CLI we attach a [`PulpCLIContext`][pulpcore.cli.common.generic.PulpCLIContext] to the main command, which inherits from `pulp-glue`'s [`PulpContext`][pulp_glue.common.context.PulpContext].
This context handles the communication to the pulp server through its `api` property.

Further we encourage the handling of communication with certain endpoints by subclassing the [`PulpEntityContext`][pulp_glue.common.context.PulpEntityContext] or some of the resource-specific children, such as [PulpRepositoryContext][pulp_glue.common.context.PulpRepositoryContext].
Some examples of this can be found under `pulp_glue/{plugin-name}/context.py`.

By attaching them to the contexts of certain command groups, they are accessible to commands via the `pass_entity_context` decorator.
Those entity contexts should provide a common interface to the layer of `click` commands that define the user interaction.

Expand All @@ -54,14 +64,16 @@ def my_sub_command(entity_ctx):
entity_ctx.destroy(href)
```

### Version dependent code paths
#### Version dependent code paths

Each Pulp CLI release is designed to support multiple Pulp server versions and the CLI itself is versioned independently of any version of the Pulp server components.
It is supposed to be able to communicate with different combinations of server component versions at the same time.
Because of this, it might be necessary to guard certain features and workarounds by checking against the available server plugin version.
As a rule of thumb, all necessary workarounds should be implemented in the corresponding `Context` objects to provide a consistent interface to the command callbacks.
To facilitate diverting code paths depending on plugin versions, the `PulpContext` provides the `needs_plugin` and `has_plugin` methods.
While `has_plugin` will evaluete immediately, `needs_plugin` can be seen as a deferred assertion.

As a rule of thumb, all necessary workarounds should be implemented in the corresponding `Context` objects.
To facilitate diverting code paths depending on plugin versions, the `PulpContext` provides the `needs_plugin` and `has_plugin` methods, both of which accept a [`PluginRequirement`][pulp_glue.common.context.PluginRequirement] object to describe dependencies on server components.

While `has_plugin` will evaluate immediately, `needs_plugin` can be seen as a deferred assertion.
It will raise an error, once the first access to the server is attempted.

```python
Expand All @@ -82,17 +94,28 @@ def my_command(ctx, pulp_ctx):
ctx.obj = MyEntityContext(pulp_ctx)
```

The named tuple `PluginRequirement` is used to describe dependence on server components.
It needs the `name` of the plugin and accepts optionally an inclusive `min` version,
an exclusive `max` version, and an `inverted` flag for exclusion of a version range.
To declare version restrictions on *options*, the [`preprocess_entity`][pulp_glue.common.context.PulpEntityContext.preprocess_entity] method can be used to check if a given option is present in the request body and conditionally apply the requirements to the context.

In the following example, a guard is added because `my_option` was introduced to `MyPluginRepository` in version 3.24.0 of `"my_plugin"`:

Additionally, the `PulpOption` provides the `needs_plugins` keyword argument.
It accepts a list of `PluginRequirements` to error when used.
```python
class PulpMyPluginRepositoryContext(PulpRepositoryContext):
...

def preprocess_entity(self, body, partial) -> EntityDefinition:
body = super().preprocess_entity(body, partial=partial)
if "my_option" in body:
self.pulp_ctx.needs_plugin(
PluginRequirement("my_plugin", specifier=">=3.24.0", feature=_("my feature"))
)
return body
```

### Generics

For certain often repeated patterns like listing all entities of a particular kind,
we provide generic commands that use the underlying context objects.
The following example shows the use of the [`show_command`][pulpcore.cli.common.generic.show_command] generic.

```python
from pulpcore.cli.common.generic import name_option, show_command,
Expand All @@ -101,15 +124,15 @@ lookup_params = [name_option]
my_command.add_command(show_command(decorators=lookup_params))
```

Some of these commands understand extra arguments.
They can be attached by passing a list of `click.Options` via the `decorators` argument.
To add options to these subcommands, pass a list of [`PulpOption`][pulpcore.cli.common.generic.PulpOption] objects to the `decorators` argument.
Preferably these are created using the [`pulp_option`][pulpcore.cli.common.generic.pulp_option] factory.

```python
from pulpcore.cli.common.generic import list_command,

filter_params = [
click.option("--name"),
click.option("--name-contains", "name__contains"),
pulp_option("--name"),
pulp_option("--name-contains", "name__contains"),
]
my_command.add_command(list_command(decorators=filter_params))
```
28 changes: 27 additions & 1 deletion pulpcore/cli/common/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ def pulp_group(


class PulpOption(click.Option):
"""
Pulp-CLI specific subclass of `click.Option`.
The preferred way to use this is through the
[`pulp_option`][pulpcore.cli.common.generic.pulp_option] factory.
"""

def __init__(
self,
*args: t.Any,
Expand Down Expand Up @@ -551,7 +558,26 @@ def null_callback(


def pulp_option(*args: t.Any, **kwargs: t.Any) -> t.Callable[[FC], FC]:
"""Factory that creates a `PulpOption`."""
"""
Factory of [`PulpOption`][pulpcore.cli.common.generic.PulpOption] objects.
`PulpOption` provides extra features over `click.Option`, namely:
1. Define version constrains.
2. Support for template variables in the help message.
3. Limit the use of options to certain entity contexts.
Examples:
Define version constrains and custom help message:
```
pulp_option(
"--name",
needs_plugins=[PluginRequirement("rpm", specifier=">=3.12.0")],
help=_("Name of {entity}"),
allowed_with_contexts=(PulpRpmRepositoryContext,),
)
```
"""
kwargs["cls"] = PulpOption
return click.option(*args, **kwargs)

Expand Down

0 comments on commit 7c29099

Please sign in to comment.