Skip to content

[proposal] Composite Features #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
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
122 changes: 122 additions & 0 deletions proposals/features-composite-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Composite Features

Reference: https://github.com/devcontainers/spec/issues/109

## Motivation

We've seen significant interest in the ability to "reuse" or "extend" a given Feature with one or more additional Features. Often in software a given tool depends on another (or several) tool(s)/framework(s). As the dev container Feature ecosystem has grown, there has been a growing need to reduce redundant code in Features by first installing a set of dependent Features.



## Goal

The solution shall provide a way to publish a Feature that "depends" on >= 1 other published Features. Dependent Features will be installed by the orchestrating tool, with the version and order set by the author if necessary.

The solution outlined shall not only execute the installation scripts, but also merge the additional development container config, as outlined in the documented [merging logic.](https://containers.dev/implementors/spec/#merge-logic)

A non-goal is to require the use or implementation of a full-blown dependency management system (such as `npm` or `apt`). The solution should not encourage authorship of individual Features that do not continue to operate as "self-contained, shareable units of installation code and development container configuration"[(1)](https://containers.dev/implementors/features/).

Composing Features should provide an alternative to existing community solutions, code duplication, and "hacky" means of installing a dependent Feature before another.

## Definitions

- `Standalone Feature` - The existing dev container Feature format, as outlined in the [spec](https://containers.dev/implementors/features/), and defined by a `devcontainer-feature.json` metadata file. A Feature that is self-contained, shareable unit of installation code and development container configuration.
- `Composite Feature` - A Feature that depends on >= 1 other Features, defined by a `devcontainer-feature.composite.json` metadata file.

## Existing community solutions

### @danielBraun89 + '@devcontainers-contrib'

This community repository containing 100+ Features provides a custom solution for dependencies, introducing an additional `feature-definition.json` file - superset of the `devcontainer-feature.json` with a [`dependencies` object](https://github.com/devcontainers-contrib/features/blob/db45f607e733f3d560f6527d89b6a9a85b3b806c/feature_definitions/elixir-asdf/feature-definition.json#L29-L50). Their [custom CLI has command named `install`](https://github.com/devcontainers-contrib/cli/blob/0768a6f9a75934e4915739ad3b43f6feb5ec515e/dcontainer/cli/install/install_feature.py) that will use python to pull and execute the `install.sh` of the given Feature. This strategy doesn't merge in the other dev container configuration properties that a Feature may declare.

### Direct curl

We've seen instances where users directly `curl` a Feature's `install.sh` script to `bash`. This strategy also doesn't merge in the other dev container configuration properties that a Feature may declare.

### Additional inspiration

Inspiration was taken from [this spec issue on the topic](https://github.com/devcontainers/spec/issues/109), the repositories listed above, [the buildpack specification](https://docs.cloudfoundry.org/buildpacks/understand-buildpacks.html), and [VS Code extension packs](https://code.visualstudio.com/blogs/2017/03/07/extension-pack-roundup).

## Specification

Introduce a new file type `devcontainer-feature.composite.json` with the following properties.

| Property | Type | Description |
|----------|------|-------------|
| `id` | `string` | The ID of the Feature. This follows the same semantics of the `id` property in the `devcontainer-feature.json` file. |
| `version` | `string` | The version of the Feature. This follows the same semantics of the `version` property in the `devcontainer-feature.json` file. |
| `features` | `array` | An array of objects (in installation order) that define the Feature(s) that compose this Feature. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the correct installation order depend on each of the install script's requirements?

Copy link
Member Author

@joshspicer joshspicer Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd want to peek at each Feature's installsAfter, i think. That was the primary reason that I didn't want to (for now) enable nesting composite Features. This spec does NOT let you nest one composite feature in another, making this process easier to resolve (we can "flatten" the installation from composite Features into one install order.

This is something to discuss and finalize in the spec, but we could have installation follow installsAfter order with precedence within a composite Feature first, and then some algorithm for ordering composite Features based on that? Perhaps the other way around?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are already thinking about composite features depending on each other: Wouldn't it be simpler to avoid the additional artifact type of a "composite feature" and instead allow features to have hard dependencies in addition to the soft dependencies supported with "installAfter"? We already compute the dependency graph including cycle detection for "installAfter". (Other package management systems also seem to have only one package type that can carry code/binaries and dependencies. Do we have an example that keeps these apart?)

We could add "dependencies" to the feature metadata as an object like "features" in the devcontainer.json, so installation order is decided by the same dependency graph algorithm. A dependency would implicitly go into the "installAfter" list of the feature depending on it.

| `features.id` | `string` | The ID of the Feature that this Feature depends on. A Feature here can be from the same, or different namespaces/repos. |
| `features.version` | `string` | The version of the Feature that this Feature depends on. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if a different version is configured in the devcontainer.json?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this brings us back to similar discussion over at #44.

Maybe it would be helpful to define behavior for installing a Feature more than once. I think a goal of Features should be that they are idempotent - perhaps this would be a good time to also define some "Feature best practices" to guide authors toward the behavior we want Features to exhibit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. We already have that when you use a base image with a feature installed and you then install that feature again in a second devcontainer.json.

If the version and options objects are equal, we can optimize by running the feature only once.

Maybe a detail: The install order algorithm currently uses the feature ids to sort features that do not require a specific install order to arrive at a stable install order. With the same feature being allowed multiple times in the same install pass, we will need to include also the feature version and options object in this sorting to ensure a stable install order.

| `features.detect` | `string` | A command that will be executed in a shell to determine if the Feature should be installed. If the command returns a non-zero exit code, the Feature will be installed. If the command returns a zero exit code, the remaining install steps will be skipped. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "remaining install steps will be skipped" mean, the remaining features listed in the array will be skipped?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will clarify this - I meant that each Feature will have a detect that will cause the further installation and merging of dev container config for that Feature. Remaining Features in the array will still be executed (and their detect phase run, too).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the examples we want to cover with this? Would this run after all previous features have detected & installed, immediately before this feature's install script would run?

| `features.options` | `object` | An object of key/value pairs that will be passed to the Feature's `install.sh` script. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are these merged when the same feature is installed by the devcontainer.json?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to #208 (comment)


#### Example `devcontainer-feature.composite.json`

```jsonc
{
"id": "ghcr.io/devcontainers/features/composite",
"version": "1.0.0",
"features": [
{
"id": "ghcr.io/devcontainers/features/a", // Must be a standalone Feature. (A composite Feature cannot depend on a composite Feature).
"version": "1.2.3", // An exact version is required. We do not permit pinning to a major or minor version.
"detect": "a --version && cat /etc/a/.markerfile", // Only continue installation of this Feature if detect returns non-zero
"options": {
"bar": true
}
},
{
"id": "ghcr.io/microsoft/features/b",
"version": "sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2", // SHA of the published artifact is OK
"detect": undefined, // Omit or set as 'undefined' to always install this Feature.
"options": {
"zip": "zap"
}
}
]
}
```

An optional `finalize.sh` script can be included, and will be fired after all Features have been installed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have good examples for that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be omitted in the first pass if we find it to be unnecessary or without justification yet. I'll leave for discussion

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that we are discussing various options. We should aim for as much simplicity as we can while still covering the important cases we have. It will be easier to later add to a simple design than a complex one.


A composite Feature will be published following the same process as a [standalone dev container Feature](https://containers.dev/implementors/features) into the same namespace - following the pattern outlined in [the Features distribution spec](https://containers.dev/implementors/features-distribution/). Dependencies of a composite Feature can be published to the same or different namespaces.

An example repository structure for a repo with one composite Feature and a standalone Feature can be found below:

```
$ tree

├── src
│   ├── composite
│   │   ├── README.md
│   │   ├── devcontainer-feature.composite.json
│   │   ├── finalize.sh
│   ├── a
│   │   ├── README.md
│   │   ├── devcontainer-feature.json
│   │   └── install.sh
...
```

### Notes:

- Composite Features cannot depend on other composite Features. This is to prevent circular dependencies and deep dependency chains.
- The `detect` property is optional. If omitted, the Feature will always be installed.
- The `options` property is optional. If omitted, the default options will be passed to the Feature's `install.sh` script, as defined in the Feature's `devcontainer-feature.json` file.
- A composite Feature can optionally include a `finalize.sh` script. This script will be executed after all of the dependencies have been installed. This is useful for Features that need to perform some action after all of the dependencies have been installed.


## Advantages

- Composite Features can be used to distribute a single Feature that depends on other Features.
- Composite Features can pin all of their dependencies to a specific version, ensuring that the Feature can be tested and will work as expected.
- Composite Features prevent the complexity that arises with deeply nested dependencies or circular dependencies.

## Disadvantages

- Composite features are not as flexible as other possible dependency models.
- Eg:
- Other models may let composite Features depend on other composite Features (dependency resolution)
- Other models may let composite Features depend on a range of versions of a Feature.
- Composite Features requires all component 'standalone' Features be published ahead of time.