/
features.md
418 lines (311 loc) · 24.3 KB
/
features.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
---
layout: implementors
title: "Dev Container Features reference"
shortTitle: "Features"
author: Microsoft
index: 5
---
Development container "Features" are self-contained, shareable units of installation code and development container configuration. The name comes from the idea that referencing one of them allows you to quickly and easily add more tooling, runtime, or library "Features" into your development container for you or your collaborators to use.
Feature metadata is captured by a `devcontainer-feature.json` file in the root folder of the feature.
> **Note:** While Features may be installed on top of any base image, the implementation of a Feature might restrict it to a subset of possible base images. For example, some Features may be authored to work with a certain Linux distro (e.g. debian-based images that use the `apt` package manager).
>
> This section covers details on the Features specification. If you are looking for summarized information on creating your own Features, check out the [quick start](https://github.com/devcontainers/feature-starter) and [core Features](https://github.com/devcontainers/features) repositories.
## <a href="#folder-structure" name="folder-structure" class="anchor"> Folder Structure </a>
A Feature is a self contained entity in a folder with at least a `devcontainer-feature.json` and `install.sh` entrypoint script. Additional files are permitted and are packaged along side the required files.
```
+-- feature
| +-- devcontainer-feature.json
| +-- install.sh
| +-- (other files)
```
## <a href="#devcontainer-feature-json-properties" name="devcontainer-feature-json-properties" class="anchor"> devcontainer-feature.json properties </a>
The `devcontainer-feature.json` file defines information about the Feature to be used by any supporting tools and the way the Feature will be executed.
[devContainerFeature.schema.json](https://github.com/devcontainers/spec/blob/main/schemas/devContainerFeature.schema.json) defines the schema for the `devcontainer-feature.json` file.
The properties of the file are as follows:
| Property | Type | Description |
| :--- | :--- | :--- |
| `id` | string | ID of the feature/definition. The `id` should be unique in the context of the repository/published package where the feature exists and must match the name of the directory where the `devcontainer-feature.json` resides. |
| `version` | string | The semantic version of the Feature. |
| `name` | string | Name of the feature/definition. |
| `description` | string | Description of the feature/definition. |
| `documentationURL` | string | Url that points to the documentation of the Feature. |
| `licenseURL` | string | Url that points to the license of the Feature. |
| `keywords` | array | List of strings relevant to a user that would search for this definition/Feature. |
| `options` | object | A map of options that will be passed as environment variables to the execution of the script. |
| `containerEnv` | object | A set of name value pairs that sets or overrides environment variables. |
| `privileged` | boolean | Sets [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) for the container (required by things like docker-in-docker) when the feature is used. |
| `init` | boolean | Adds the [tiny init](https://github.com/RKrahl/tiny-init) process to the container (`--init`) when the Feature is used. |
| `capAdd` | array | Adds container [capabilities](https://docs.docker.com/engine/security/#linux-kernel-capabilities) when the Feature is used. |
| `securityOpt` | array | Sets container security options like updating the [seccomp profile](https://docs.docker.com/engine/security/seccomp/) when the Feature is used. |
| `entrypoint` | string | Set if the feature requires an "entrypoint" script that should fire at container start up. |
| `customizations` | object | Product specific properties, each namespace under `customizations` is treated as a separate set of properties. For each of this sets the object is parsed, values are replaced while arrays are set as a union. |
| `installsAfter` | array | Array of ID's of Features (omitting a version tag) that should execute before this one. Allows control for Feature authors on soft dependencies between different Features. |
| `legacyIds` | array | Array of old IDs used to publish this Feature. The property is useful for renaming a currently published Feature within a single namespace. |
| `deprecated` | boolean | Indicates that the Feature is deprecated, and will not receive any further updates/support. This property is intended to be used by the supporting tools for highlighting Feature deprecation. |
| `mounts` | object | Defaults to unset. Cross-orchestrator way to add additional mounts to a container. Each value is an object that accepts the same values as the [Docker CLI `--mount` flag](https://docs.docker.com/engine/reference/commandline/run/#add-bind-mounts-or-volumes-using-the---mount-flag). The Pre-defined [devcontainerId](/_implementors/json_reference.md#variables-in-devcontainerjson) variable may be referenced in the value. For example:<br />`"mounts": [{ "source": "dind-var-lib-docker", "target": "/var/lib/docker", "type": "volume" }]` |
{: .table .table-bordered .table-responsive}
### <a href="#options-property" name="options-property" class="anchor"> The `options` property </a>
The options property contains a map of option IDs and their related configuration settings. The ID becomes the name of the environment variable in all caps. See [option resolution](#option-resolution) for more details. For example:
```json
{
"options": {
"optionIdGoesHere": {
"type": "string",
"description": "Description of the option",
"proposals": ["value1", "value2"],
"default": "value1"
}
}
}
```
| Property | Type | Description |
| :--- | :--- | :--- |
| `optionId` | string | ID of the option that is converted into an all-caps environment variable with the selected value in it. |
| `optionId.type` | string | Type of the option. Valid types are currently: `boolean`, `string` |
| `optionId.proposals` | array | A list of suggested string values. Free-form values **are** allowed. Omit when using `optionId.enum`. |
| `optionId.enum` | array | A strict list of allowed string values. Free-form values are **not** allowed. Omit when using `optionId.proposals`. |
| `optionId.default` | string or boolean | Default value for the option. |
| `optionId.description` | string | Description for the option. |
{: .table .table-bordered .table-responsive}
### <a href="#user-env-var" name="user-env-var" class="anchor"> User environment variables </a>
Feature scripts run as the `root` user and sometimes need to know which user account the dev container will be used with.
`_REMOTE_USER` and `_CONTAINER_USER` environment variables are passsed to the Features scripts with `_CONTAINER_USER` being the container's user and `_REMOTE_USER` being the configured `remoteUser`. If no `remoteUser` is configured, `_REMOTE_USER` is set to the same value as `_CONTAINER_USER`.
Additionally, the home folders of the two users are passed to the Feature scripts as `_REMOTE_USER_HOME` and `_CONTAINER_USER_HOME` environment variables.
The container user can be set with `containerUser` in the devcontainer.json and image metadata, `user` in the docker-compose.yml, `USER` in the Dockerfile, and can be passed down from the base image.
### <a href="#dev-container-id" name="dev-container-id" class="anchor"> Dev Container ID </a>
An identifier will be referred to as `${devcontainerId}` in the devcontainer.json and the Feature metadata and that will be replaced with the dev container's id. It should only be used in parts of the configuration and metadata that is not used for building the image because that would otherwise prevent pre-building the image at a time when the dev container's id is not known yet. Excluding boolean, numbers and enum properties the properties supporting `${devcontainerId}` in the Feature metadata are: `entrypoint`, `mounts`, `customizations`.
Implementations can choose how to compute this identifier. They must ensure that it is unique among other dev containers on the same Docker host and that it is stable across rebuilds of dev containers. The identifier must only contain alphanumeric characters. We describe a way to do this below.
#### <a href="#label-based" name="label-based" class="anchor"> Label-based Implementation </a>
The following assumes that a dev container can be identified among other dev containers on the same Docker host by a set of labels on the container. Implementations may choose to follow this approach.
The identifier is derived from the set of container labels uniquely identifying the dev container. It is up to the implementation to choose these labels. E.g., if the dev container is based on a local folder the label could be named `devcontainer.local_folder` and have the local folder's path as its value.
E.g., the [`ghcr.io/devcontainers/features/docker-in-docker` Feature](https://github.com/devcontainers/features/blob/main/src/docker-in-docker/devcontainer-feature.json) could use the dev container id with:
```jsonc
{
"id": "docker-in-docker",
"version": "1.0.4",
// ...
"mounts": [
{
"source": "dind-var-lib-docker-${devcontainerId}",
"target": "/var/lib/docker",
"type": "volume"
}
]
}
```
#### <a href="#label-based-comp" name="label-based-comp" class="anchor"> Label-based Computation </a>
- Input the labels as a JSON object with the object's keys being the label names and the object's values being the labels' values.
- To ensure implementations get to the same result, the object keys must be sorted and any optional whitespace outside of the keys and values must be removed.
- Compute a SHA-256 hash from the UTF-8 encoded input string.
- Use a base-32 encoded representation left-padded with '0' to 52 characters as the result.
JavaScript implementation taking an object with the labels as argument and returning a string as the result:
```js
const crypto = require('crypto');
function uniqueIdForLabels(idLabels) {
const stringInput = JSON.stringify(idLabels, Object.keys(idLabels).sort()); // sort properties
const bufferInput = Buffer.from(stringInput, 'utf-8');
const hash = crypto.createHash('sha256')
.update(bufferInput)
.digest();
const uniqueId = BigInt(`0x${hash.toString('hex')}`)
.toString(32)
.padStart(52, '0');
return uniqueId;
}
```
## <a href="#devcontainer-json-properties" name="devcontainer-json-properties" class="anchor"> devcontainer.json properties </a>
Features are referenced in a user's [`devcontainer.json`](../json_reference) under the top level `features` object.
A user can specify an arbitrary number of Features. At build time, these Features will be installed in an order defined by a combination of the [installation order rules and implementation](#Installation-Order).
A single Feature is provided as a key/value pair, where the key is the Feature identifier, and the value is an object containing "options" (or empty for "default"). Each key in the feature object must be unique.
These options are sourced as environment variables at build-time, as specified in [Option Resolution](#Option-Resolution).
Below is a valid `features` object provided as an example.
```jsonc
"features": {
"ghcr.io/user/repo/go": {},
"ghcr.io/user/repo1/go:1": {},
"ghcr.io/user/repo2/go:latest": {},
"https://github.com/user/repo/releases/devcontainer-feature-go.tgz": {
"optionA": "value"
},
"./myGoFeature": {
"optionA": true,
"optionB": "hello",
"version" : "1.0.0"
}
}
```
> **Note:** The `:latest` version annotation is added implicitly if omitted. To pin to a specific package version ([example](https://github.com/devcontainers/features/pkgs/container/features/go/versions)), append it to the end of the Feature.
An option's value can be provided as either a `string` or `boolean`, and should match what is expected by the feature in the `devcontainer-feature.json` file.
As a shorthand, the value of the `features` property can be provided as a single string. This string is mapped to an option called `version`. In the example below, both examples are equivalent.
```jsonc
"features": {
"ghcr.io/owner/repo/go": "1.18"
}
```
```jsonc
"features": {
"ghcr.io/owner/repo/go": {
"version": "1.18"
}
}
```
### <a href="#referencing-a-feature" name="referencing-a-feature" class="anchor"> Referencing a feature </a>
The `id` format specified dicates how a supporting tool will locate and download a given feature. `id` is one of the following:
| Type | Description | Example |
| :--- | :--- | :--- |
| `<oci-registry>/<namespace>/<feature>[:<semantic-version>]` | Reference to feature in OCI registry(*) | `ghcr.io/user/repo/go` <br> `ghcr.io/user/repo/go:1` <br> `ghcr.io/user/repo/go:latest`|
| `https://<uri-to-feature-tgz>` | Direct HTTPS URI to a tarball. | `https://github.com/user/repo/releases/devcontainer-feature-go.tgz` |
| `./<path-to-feature-dir>`| A relative directory(**) to folder containing a devcontainer-feature.json. | `./myGoFeature` |
{: .table .table-bordered .table-responsive}
(*) OCI registry must implement the [OCI Artifact Distribution Specification](https://github.com/opencontainers/distribution-spec). Some implementors can be [found here](https://oras.land/implementors/).
(**) The provided path is always relative to the folder containing the `devcontainer.json`. Further requirements are outlined in the [Locally Referenced Addendum](../features-distribution#addendum-locally-referenced).
## <a href="#versioning" name="versioning" class="anchor"> Versioning </a>
Each Feature is individually [versioned according to the semver specification](https://semver.org/). The `version` property in the respective `devcontainer-feature.json` file is updated to increment the Feature's version.
Tooling that handles releasing Features will not republish Features if that exact version has already been published; however, tooling must republish major and minor versions in accordance with the semver specification.
## <a href="#authoring" name="authoring" class="anchor"> Authoring </a>
Features can be authored in a number of languages, the most straightforward being bash scripts. If a Feature is authored in a different language, information about it should be included in the metadata so that users can make an informed choice about it.
Reference information about the application required to execute the Feature should be included in `devcontainer-feature.json` in the metadata section.
Applications should default to `/bin/sh` for Features that do not include this information.
If the Feature is included in a folder as part of the repository that contains `devcontainer.json`, no other steps are necessary.
## <a href="#release" name="release" class="anchor"> Release </a>
_For information on distributing Features, see [the dev container Features distribution page](../features-distribution)._
## <a href="#execution" name="execution" class="anchor"> Execution </a>
### <a href="#invoking-installsh" name="invoking-installsh" class="anchor"> Invoking `install.sh` </a>
The `install.sh` script for each Feature should be executed as `root` during a container image build. This allows the script to add needed OS dependencies or settings that could not otherwise be modified. This also allows the script to switch into another user's context using the `su` command (e.g., `su ${USERNAME} -c "command-goes-here"`). In combination, this allows both root and non-root image modifications to occur even if `sudo` is not present in the base image for security reasons.
To ensure that the appropriate shell is used, the execute bit should be set on `install.sh` and the file invoked directly (e.g. `chmod +x install.sh && ./install.sh`).
> **Note:** It is recommended that Feature authors write `install.sh` using a shell available by default in their supported distributions (e.g., `bash` in Debian/Ubuntu or Fedora, `sh` in Alpine). In the event a different shell is required (e.g., `fish`), `install.sh` can be used to boostrap by checking for the presence of the desired shell, installing it if needed, and then invoking a secondary script using the shell.
>
> The `install.sh` file can similarly be used to bootstrap something written in a compiled language like Go. Given the increasing likelihood that a Feature needs to work on both x86_64 and arm64-based devices (e.g., Apple Silicon Macs), `install.sh` can detect the current architecture (e.g., using something like `uname -m` or `dpkg --print-architecture`), and then invoke the right executable for that architecture.
### <a href="#installation-order" name="installation-order" class="anchor"> Installation order </a>
By default, Features are installed on top of a base image in an order determined as optimal by the implementing tool.
If any of the following properties are provided in the Feature's `devcontainer-feature.json`, or the user's `devcontainer.json`, the order indicated by these propert(ies) are respected (with decreasing precedence).
1. The `overrideFeatureInstallOrder` property in user's `devcontainer.json`. Allows users to control the order of execution of their Features.
2. The `installsAfter` property defined as part of a Feature's `devcontainer-feature.json`.
#### <a href="#overrideFeatureInstallOrder" name="overrideFeatureInstallOrder" class="anchor"> (1) The `overrideFeatureInstallOrder` property </a>
This property is declared by the user in their `devcontainer.json` file.
Any **un-versioned** Feature IDs listed in this array will be installed before all other Features, in the provided order. Any omitted Features will be installed in an order selected by the implementing tool, or ordered via the `installsAfter` property _after_ any Features listed in the `overrideFeatureInstallOrder` array, if applicable.
All un-versioned Feature `id`s provided in `overrideFeatureInstallOrder` must also exist in the `features` property of a user's `devcontainer.json`. For instance, all the Features which follows the OCI registry format would include everything except for the label that contains the version (`<oci-registry>/<namespace>/<feature>` without the `:<semantic-version>`).
Example:
```
"features": {
"ghcr.io/devcontainers/features/java:1",
"ghcr.io/devcontainers/features/node:1",
},
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/node"
]
```
| Property | Type | Description |
| :--- | :--- | :--- |
| `overrideFeatureInstallOrder` | array | Array consisting of the Feature `id` (without the semantic version) of Features in the order the user wants them to be installed. |
{: .table .table-bordered .table-responsive}
#### <a href="#installsAfter" name="installsAfter" class="anchor"> (2) The `installsAfter` Feature property </a>
This property is defined in an individual feature's `devcontainer-feature.json` file by the feature author. `installsAfter` allows an author to provide the tooling hints on loose dependencies between Features.
> This property is mostly useful for optimizing build time (by reordering the Feature installation to reduce installing a required CLI twice, for example). Ideally, all Features should be able to fully install themselves without requiring another Feature to be pre-installed.
After `overrideFeatureInstallOrder` is resolved, any remaining Features that declare an `installsAfter` must be installed after the Features declared in the property, provided that the features have also been declared in the `features` property.
| Property | Type | Description |
| :--- | :--- | :--- |
| `installsAfter` | array | Array consisting of the Feature `id` (omitting a version tag) that should be installed before the given Feature |
{: .table .table-bordered .table-responsive}
### <a href="#option-resolution" name="option-resolution" class="anchor"> Option Resolution </a>
A Feature's 'options' - specified as the value of a single Feature key/value pair in the user's `devcontainer.json` - are passed to the Feature as environment variables.
A supporting tool will parse the `options` object provided by the user. If a value is provided for a Feature, it will be emitted to a file named `devcontainer-features.env` following the format `<OPTION_NAME>=<value>`.
To ensure a option that is valid as an environment variable, the follow substitutions are performed.
```javascript
(str: string) => str
.replace(/[^\w_]/g, '_')
.replace(/^[\d_]+/g, '_')
.toUpperCase();
```
This file is sourced at build-time for the feature `install.sh` entrypoint script to handle.
Any options defined by a feature's `devcontainer-feature.json` that are omitted in the user's `devcontainer.json` will be implicitly exported as its default value.
### <a href="#option-resolution-example" name="option-resolution-example" class="anchor"> Option resolution example </a>
Suppose a `python` Feature has the following `options` parameters declared in the `devcontainer-feature.json` file:
```jsonc
// ...
"options": {
"version": {
"type": "string",
"enum": ["latest", "3.10", "3.9", "3.8", "3.7", "3.6"],
"default": "latest",
"description": "Select a Python version to install."
},
"pip": {
"type": "boolean",
"default": true,
"description": "Installs pip"
},
"optimize": {
"type": "boolean",
"default": true,
"description": "Optimize python installation"
}
}
```
The user's `devcontainer.json` declared the python Feature like so:
```jsonc
"features": {
"python": {
"version": "3.10",
"pip": false
}
}
```
The emitted environment variables will be:
```bash
VERSION="3.10"
PIP="false"
OPTIMIZE="true"
```
These will be sourced and visible to the `install.sh` entrypoint script. The following `install.sh`...
```bash
#!/usr/bin/env bash
echo "Version is $VERSION"
echo "Pip? $PIP"
echo "Optimize? $OPTIMIZE"
```
... outputs the following:
```
Version is 3.10
Pip? false
Optimize? true
```
### <a href="#steps-to-rename-a-feature" name="steps-to-rename-a-feature" class="anchor"> Steps to rename a Feature </a>
1. Update the Feature [source code](./features-distribution.md#source-code) folder and the `id` property in the [devcontainer-feature.json properties](#devcontainer-featurejson-properties) to reflect the new `id`. Other properties (`name`, `documentationUrl`, etc.) can optionally be updated during this step.
2. Add or update the `legacyIds` property to the Feature, including the previously used `id`.
3. Bump the semantic version of the Feature.
4. Rerun the `devcontainer features publish` command, or equivalent tool that implements the [Features distribution specification](./features-distribution.md#distribution).
#### Example: Renaming a Feature
Let's say we currently have a `docker-from-docker` Feature 👇
Current `devcontainer-feature.json` :
```jsonc
{
"id": "docker-from-docker",
"version": "2.0.1",
"name": "Docker (Docker-from-Docker)",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-from-docker",
....
}
```
We'd want to rename this Feature to `docker-outside-of-docker`. The source code folder of the Feature will be updated to `docker-outside-of-docker` and the updated `devcontainer-feature.json` will look like 👇
```jsonc
{
"id": "docker-outside-of-docker",
"version": "2.0.2",
"name": "Docker (Docker-outside-of-Docker)",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker",
"legacyIds": [
"docker-from-docker"
]
....
}
```
**Note** - The semantic version of the Feature defined by the `version` property should be **continued** and should not be restarted at `1.0.0`.
### <a href="#implementation-notes" name="implementation-notes" class="anchor"> Implementation notes </a>
There are several things to keep in mind for an application that implements Features:
- The order of execution of Features is determined by the application, based on the `installsAfter` property used by Feature authors. It can be overridden by users if necessary with the `overrideFeatureInstallOrder` in `devcontainer.json`.
- Features are used to create an image that can be used to create a container or not.
- Parameters like `privileged`, `init` are included if just 1 feature requires them.
- Parameters like `capAdd`, `securityOp` are concatenated.
- `containerEnv` is added before the feature is executed as `ENV` commands in the Dockerfile.
- Each Feature script executes as its own layer to aid in caching and rebuilding.