Workshop definition¶
A project which defines a single workshop can store a definition file
named workshop.yaml or .workshop.yaml (the latter is hidden)
in the project directory.
Filename convention¶
When multiple workshops are defined,
their definition files must be stored in the .workshop/ subdirectory.
The workshop name must also match the file name
(without the .yaml extension).
Workshop names start with a lowercase letter and may include only lowercase letters, digits or hyphens.
Structure¶
The definition in the file is written in YAML and includes a number of mandatory and optional keys:
Key |
Value |
Description |
|---|---|---|
|
string |
Workshop’s name, used to reference the workshop itself. For workshops defined in the |
|
string |
Workshop’s base image that provides the underlying OS capabilities. It can be |
|
array |
List of individual SDKs from the SDK Store to include in the workshop. Each entry points to an existing SDK and specifies its retrieval channel. The SDKs are installed in the order they appear in this list; the exception is the system SDK which is always installed first. |
|
array |
List of connections made by the workshop; each links a plug to a slot. Any entry in |
|
object |
List of shell actions to be used with workshop run. These are copied into the workshop
before being executed by bash.
The options Arguments passed to workshop run
are available inside the script
through the standard bash positional parameters:
|
Each SDK is described with the following keys:
Key |
Value |
Description |
|---|---|---|
|
string |
Name of an existing SDK, typically from the SDK Store.
|
|
string |
SDK version to retrieve during launch and refresh operations. It uses a
snap-like format
of Only applies to SDKs from the Store. |
|
object |
Lists plug bindings or additional plug definitions under the SDK.
|
|
object |
Defines additional slots under the SDK;
each entry must specify the |
System SDK¶
The system SDK is built into every workshop
to expose resources provided by the host system in a consistent way.
It’s not available in the SDK Store,
so channel isn’t relevant and can be omitted.
Technically, the system SDK is of system type,
whereas all other SDKs are of regular type,
but this detail isn’t exposed in the definition files.
Several interfaces expose resources that are host-based and singular by nature;
the system SDK has default eponymous slots for these interfaces:
system:camera, system:desktop, system:gpu,
system:mount, and system:ssh-agent.
No other SDKs can declare slots for these interfaces, except for mount.
The system:mount slot is still unique
because it’s the only one that provides access to the host filesystem,
whereas slots under regular SDKs only expose locations in the workshop.
If additional slots for interfaces like tunnel or mount
are defined for the system SDK,
they won’t be auto-connected at launch or refresh,
largely due to security considerations,
because the system SDK exposes sensitive host system resources.
To the contrary, plugs added under the system SDK can be auto-connected
because they expose workshop internals.
Trying out SDKs¶
The sdkcraft try command makes SDKs available locally without having to publish them in the Store.
Workshops consume these SDKs
using names like try-<NAME>;
a channel is not required in this case.
In-project SDKs¶
Projects can define their own SDKs
to configure the workshop in a project-specific way.
These SDKs are defined by files named like .workshop/<NAME>/sdk.yaml,
relative to the project directory.
Accordingly, SDK hooks are stored under .workshop/<NAME>/hooks/.
Workshops within the project consume these SDKs
using names like project-<NAME>;
a channel is not required in this case.
Camera interface¶
Camera interface plugs must be named camera
and can’t belong to the system SDK.
They have no attributes.
The only camera interface slot is system:camera.
Desktop interface¶
Desktop interface plugs must be named desktop
and can’t belong to the system SDK.
They have no attributes.
The only desktop interface slot is system:desktop.
GPU interface¶
GPU interface plugs must be named gpu
and can’t belong to the system SDK.
They have no attributes.
The only GPU interface slot is system:gpu.
Mount interface¶
Mount interface plugs can’t belong to the system SDK. They are described by the following attributes:
Key |
Value |
Description |
|---|---|---|
|
string |
A path inside the workshop
to be used as the plug’s target directory;
|
|
integer |
File permissions to use when creating |
|
integer |
User ID to apply when creating |
|
integer |
Group ID to use when creating |
|
Boolean |
Whether the target directory should be read-only. |
The only mount interface slot in the system SDK
is system:mount.
It has a single dynamic attribute named host-source,
which can be only configured at remount.
Regular SDKs can declare additional mount interface slots. They are described by the following attributes:
Key |
Value |
Description |
|---|---|---|
|
string |
A path inside the workshop
to be used as the slot’s source directory;
|
SSH interface¶
SSH interface plugs must be named ssh-agent
and can’t belong to the system SDK.
They have no attributes.
The only SSH interface slot is system:ssh-agent.
Tunnel interface¶
Tunnel interface plugs and slots are described by the following attributes:
Key |
Value |
Description |
|---|---|---|
|
string |
A network address or Unix domain socket to be used as one end of the tunnel. |
Endpoints are formatted as follows:
Type |
Format |
|---|---|
Endpoint |
|
Address |
|
Protocol |
Either |
Host |
An IPv4 or IPv6 address. If a port is supplied, IPv6 addresses must be enclosed in square brackets. Supported aliases: The default is |
Port |
A TCP or UDP port number (1–65535). May be omitted, but only on one side of a connection. For such connections, both sides use the same port. For security reasons, tunnel interface plugs in the system SDK cannot use privileged ports (1–1023). |
Path |
An absolute path to a Unix domain socket.
For security reasons, tunnel interface plugs in the system SDK cannot listen on sockets outside these two directories. |
String |
An abstract socket name. |
The default endpoint is the default network address (localhost/tcp).
Endpoints which start with [ or @
need to be quoted in YAML:
endpoint: '[::1]:8080/tcp'
endpoint: '@abstract.sock'
JSON Schema¶
The following JSON Schema formalizes the description above:
Workshop definition schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://canonical.com/workshop.yaml",
"title": "Workshop",
"description": "Workshop definition.",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the workshop.",
"pattern": "^[a-z](?:-?[a-z0-9])*$",
"maxLength": 40,
"errorMessage": "A workshop's name must start with a letter and can only include digits, lowercase letters, and hyphens joining them."
},
"base": {
"type": "string",
"description": "Base system for the workshop.",
"enum": [
"ubuntu@20.04",
"ubuntu@22.04",
"ubuntu@24.04",
"ubuntu@26.04"
],
"errorMessage": "The base must be one of the supported values: ubuntu@20.04, ubuntu@22.04, ubuntu@24.04, ubuntu@26.04."
},
"sdks": {
"type": "array",
"description": "List of SDKs used in the workshop.",
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the SDK. Optionally prefix with 'try-' or 'project-' exactly once to reference those sources.",
"if": {
"pattern": "^try-"
},
"then": {
"maxLength": 44,
"pattern": "^(?!(?:try-|project-)?agent$)try-(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$"
},
"else": {
"if": {
"pattern": "^project-"
},
"then": {
"maxLength": 48,
"pattern": "^(?!(?:try-|project-)?agent$)project-(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$"
},
"else": {
"maxLength": 40,
"pattern": "^(?!(?:try-|project-)?agent$)(?!try-|project-)(?:[a-z0-9]-?)*[a-z](?:-?[a-z0-9])*$"
}
},
"errorMessage": "An SDK's name must contain a letter, may have a single 'try-' or 'project-' prefix, must not chain prefixes, and the underlying name cannot be 'agent'."
},
"channel": {
"type": "string",
"description": "Channel used for the SDK. Default is latest/stable unless the SDK is installed from a local source.",
"pattern": "^(?:(?:[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9])*/)?(?:stable|candidate|beta|edge)(?:/[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9])?|[a-zA-Z0-9](?:[_.-]?[a-zA-Z0-9])*|)$",
"errorMessage": "Channel must look like [<track>/]<risk>[/<branch>] or [<track>]."
},
"plugs": {
"type": "object",
"description": "Plugs for the SDK.",
"patternProperties": {
"^[a-z](?:-?[a-z0-9])*$": {
"description": "Plug definition. Provide any YAML value for inline plug attributes, or set 'bind' to reference another plug. When 'bind' is set, no other attributes may be defined.",
"properties": {
"bind": {
"type": "string",
"description": "Reference to a plug in the form [<sdk>:]<plug>. If omitted, the SDK is 'system'.",
"pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
"errorMessage": "Bind reference must follow the pattern [<sdk>:]<plug>."
}
},
"if": {
"type": "object",
"required": [
"bind"
]
},
"then": {
"type": "object",
"additionalProperties": false,
"errorMessage": "When 'bind' is set, no other attributes are allowed on the plug."
}
}
}
},
"slots": {
"type": "object",
"description": "Slots available for the SDK.",
"additionalProperties": true
}
},
"required": [
"name"
],
"errorMessage": {
"required": {
"name": "Each SDK must specify a name."
}
},
"additionalProperties": false
}
},
"connections": {
"type": "array",
"description": "List of connections between plugs and slots.",
"uniqueItems": true,
"items": {
"type": "object",
"properties": {
"plug": {
"type": "string",
"description": "Reference to a plug in the form [<sdk>:]<plug>. If omitted, the SDK is 'system'.",
"pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
"errorMessage": "Plug reference must follow the pattern [<sdk>:]<plug>."
},
"slot": {
"type": "string",
"description": "Reference to a slot in the form [<sdk>:]<slot>. If omitted, the SDK is 'system'.",
"pattern": "^(([a-z0-9]-?)*[a-z](-?[a-z0-9])*)?:[a-z](-?[a-z0-9])*$",
"errorMessage": "Slot reference must follow the pattern [<sdk>:]<slot>."
}
},
"required": [
"plug",
"slot"
],
"errorMessage": {
"required": {
"plug": "Each connection must specify a plug.",
"slot": "Each connection must specify a slot."
}
},
"additionalProperties": false
}
},
"actions": {
"type": "object",
"description": "List of actions to be run in the workshop.",
"patternProperties": {
"^[a-z](?:-?[a-z0-9])*$": {
"type": "string",
"description": "Shell script."
}
},
"additionalProperties": false,
"errorMessage": "Action names must be unique and only appear once."
}
},
"required": [
"name",
"base"
],
"additionalProperties": false,
"errorMessage": {
"required": {
"name": "The 'name' field is required.",
"base": "The 'base' field is required."
}
}
}
Examples¶
This YAML file defines a golang workshop
with a single go SDK
from the 1.26/stable channel,
and some useful actions:
name: golang
base: ubuntu@22.04
sdks:
- name: go
channel: 1.26
actions:
lint: |
go vet
golangci-lint run
tests: go test "$@"
This YAML file defines a go-dev workshop
that uses the go SDK from the Store
and an in-project SDK named tunnel;
the data plug defined by the tunnel SDK
is bound to the mod-cache plug of the go SDK:
name: go-dev
base: ubuntu@22.04
sdks:
- name: go
channel: edge
- name: project-tunnel
plugs:
data:
bind: go:mod-cache
This YAML file,
besides using the fictional
tensorflow, imagenet and cuda SDKs,
defines an additional slot under the imagenet SDK,
a plug under tensorflow
and two connections:
One that connects the
tensorflow:imagesplug to the newly definedimagenet:imagesslot.Another that connects the
tensorflow:cudaplug to the preexistingcuda:libs.
base: ubuntu@22.04
name: digits-cuda
sdks:
- name: tensorflow
plugs:
cuda:
interface: mount
workshop-target: /usr/local/cuda/lib64
- name: imagenet
slots:
images:
interface: mount
workshop-source: $SDK/images
- name: cuda
connections:
- plug: tensorflow:cuda
slot: cuda:libs
- plug: tensorflow:images
slot: imagenet:images
See also¶
Explanation:
Reference: