Skip to content
Draft
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
221 changes: 221 additions & 0 deletions docs/how-to/customize-workshops/design-interface-layout.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
.. _how_design_interface_layout:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this how-to should not go as is. Let's merge the other outstanding PRs and discuss this. Mainly, the issues are duplication (e.g. wiring) and going against the pattern in the workshop's how-to where we cover interface related topics from the specific interface perspective, e.g. forward a port, add a mount (both, address adding a missing plug to an SDK; there will be another section on how to add a secret which would use the same mechanics as these two and so on). Some topics from here should become such how-to sections. E.g. share content between two SDKs to demonstrate the connections sections.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, converting this to draft, then.


.. meta::
:description: Shape a workshop's interface topology with explicit connection
entries in the workshop definition: survey what auto-connects,
wire a consumer to a specific provider, graft a missing plug,
or rewire a running workshop with workshop connect.

How to design the interface layout of a workshop
================================================

.. @tests in tests/docs-how-to/design-interface-layout/task.yaml

.. @artefact interface connection
.. @artefact workshop definition

You can shape the topology of a workshop
by writing explicit plug-to-slot connections in the workshop definition.
Use explicit connections
when several SDKs in the workshop expose or consume the same interface
and you want to be specific about which provider satisfies which consumer,
when auto-connection lands a plug on a slot you did not intend,
or when a consumer SDK ships no plug for a capability you want it to use.
You need a workshop definition under :file:`.workshop/`
that lists at least two SDKs, one with a slot and one with a matching plug.

This is a different problem from same-interface plug conflicts,
where two plugs would compete over the same target.
For that case, see :ref:`how_resolve_plug_conflicts`,
which uses an inline :samp:`bind:` attribute to delegate one plug to another.


Survey the plugs and slots in scope
-----------------------------------

Launch the workshop once to see what |ws_markup| connects on its own.
The examples here use three in-project SDKs
that live under :file:`.workshop/` next to the definition:
:samp:`provider-a` and :samp:`provider-b`
each expose a mount slot named :samp:`data`,
and :samp:`consumer` declares a mount plug named :samp:`feed`.

.. code-block:: console

$ workshop launch dev
$ workshop connections dev

INTERFACE PLUG SLOT NOTES
mount - dev/provider-a:data -
mount - dev/provider-b:data -
mount dev/consumer:feed dev/system:mount -


The output lists every plug and slot in the workshop,
the slot each plug is connected to (if any),
and any notes on the connection.
:samp:`consumer:feed` landed on the system SDK's default mount slot,
not on either regular provider,
because mount plugs auto-connect to system SDK slots by default.
The :samp:`provider-a:data` and :samp:`provider-b:data` slots
stay listed but unconnected:
regular SDK mount slots are not reached by auto-connection by default,
even when a matching plug is in scope.
The result is a working workshop, but probably not the one you intended.
A regular SDK slot is wired
either by a top-level :samp:`connections:` entry in the definition
or manually with :command:`workshop connect`.


Wire a consumer to a specific provider
--------------------------------------

Add a top-level :samp:`connections:` list to the workshop definition,
pairing the plug with the slot you want it to use:

.. code-block:: yaml
:caption: .workshop/dev.yaml
:emphasize-lines: 8-10

name: dev
base: ubuntu@22.04
sdks:
- name: project-provider-a
- name: project-provider-b
- name: project-consumer

connections:
- plug: consumer:feed
slot: provider-b:data


Each entry uses the :samp:`<SDK-NAME>:<NAME>` form on both sides.
In-project SDKs take the :samp:`project-` prefix
in the :samp:`sdks:` list only;
:samp:`connections:` entries and CLI output use the bare name.
After :command:`workshop refresh`,
|ws_markup| applies the listed pairing
regardless of what other slots could have matched it:

.. code-block:: console

$ workshop refresh dev
$ workshop connections dev

INTERFACE PLUG SLOT NOTES
mount - dev/provider-a:data -
mount dev/consumer:feed dev/provider-b:data -


This decision is persistent;
re-launching the workshop or recreating it
applies the same pairing every time.
To inspect the resolved mount details, run :command:`workshop info dev`,
which lists each connected mount plug
along with the source path the slot exposed
and the target path inside the workshop.


Graft a missing plug onto a consumer SDK
----------------------------------------

Suppose a consumer SDK ships no plug for the capability you want it to use.
The workshop definition can add one:
declare the plug under the SDK's entry in :samp:`sdks`,
then connect it as before.
This example pairs :samp:`provider-sdk`,
which exposes a mount slot named :samp:`bin`,
with :samp:`consumer-sdk`, which ships no matching plug:

.. code-block:: yaml
:caption: .workshop/dev.yaml
:emphasize-lines: 5-9, 11-13

name: dev
base: ubuntu@22.04
sdks:
- name: project-provider-sdk
- name: project-consumer-sdk
plugs:
tools:
interface: mount
workshop-target: /home/workshop/.local/share/tools

connections:
- plug: consumer-sdk:tools
slot: provider-sdk:bin


This grafts a new plug onto :samp:`consumer-sdk`
without modifying the SDK itself.
The publisher does not need to ship every plug
their users might want;
the workshop author can add the missing piece locally.


Rewire a running workshop with workshop connect
-----------------------------------------------

For a one-off change, :command:`workshop connect` rewires a running workshop
without editing the workshop definition.
Pass the plug and the target slot explicitly:

.. code-block:: console

$ workshop disconnect dev/consumer:feed
$ workshop connect dev/consumer:feed dev/provider-a:data
$ workshop connections dev

INTERFACE PLUG SLOT NOTES
mount - dev/provider-b:data -
mount dev/consumer:feed dev/provider-a:data manual


The :samp:`manual` note in the :samp:`NOTES` column flags that the connection
came from a CLI invocation rather than the workshop definition
or the auto-connection mechanism.

The workshop definition on disk is unchanged,
and the runtime marks are not reconciled with it:
the next :command:`workshop refresh` that applies updates
drops connections made with :command:`workshop connect`,
while plugs disconnected with :command:`workshop disconnect`
stay disconnected,
unless the disconnection was made with :option:`!--forget`.
In the example above,
a refresh thus leaves :samp:`consumer:feed` unconnected:
the manual connection to :samp:`provider-a:data` is dropped,
and the definition's pairing with :samp:`provider-b:data` is not restored
because the plug was manually disconnected from it.
Running :command:`workshop remove` discards all runtime marks,
so a subsequent :command:`workshop launch` starts from the definition.

For a topology that survives refreshes
and travels with the project,
edit the workshop definition instead.


See also
--------

Explanation:

- :ref:`exp_in_project_sdk`
- :ref:`exp_interface_concepts`
- :ref:`exp_plug_bindings`
- :ref:`exp_plugs_slots`


How-to guides:

- :ref:`how_resolve_plug_conflicts`


Reference:

- :ref:`ref_workshop_connect`
- :ref:`ref_workshop_connections`
- :ref:`ref_workshop_definition`
- :ref:`ref_workshop_disconnect`
- :ref:`ref_workshop_info`
- :ref:`ref_workshop_refresh`
1 change: 1 addition & 0 deletions docs/how-to/customize-workshops/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ workshops in parallel:

Add actions to workshops <add-actions>
Add mounts <add-mounts>
Design the interface layout <design-interface-layout>
Forward ports <forward-ports>
Move projects around <move-projects>
Use multiple workshops <use-multiple-workshops>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: consumer
base: ubuntu@22.04
summary: Synthesized consumer for the ambiguity test
description: |
Declares a mount plug named "feed". With provider-a and provider-b both
present, autowiring is ambiguous and the workshop definition must specify
which slot to use.

sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00"

plugs:
feed:
interface: mount
workshop-target: /home/workshop/feed
13 changes: 13 additions & 0 deletions tests/docs-how-to/design-interface-layout/.workshop/graft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: graft
base: ubuntu@22.04
sdks:
- name: project-provider-a
- name: project-plainconsumer
plugs:
feed:
interface: mount
workshop-target: /home/workshop/feed

connections:
- plug: plainconsumer:feed
slot: provider-a:data
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: plainconsumer
base: ubuntu@22.04
summary: Synthesized consumer that ships no plug, for the graft scenario
description: |
Ships no mount plug. The graft workshop declares a "feed" plug inline under
this SDK's sdks entry to demonstrate grafting a plug the SDK did not ship.

sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
install -d -m 0755 -o 1000 -g 1000 /home/workshop/provider-a-data
echo "from-a" >/home/workshop/provider-a-data/marker
chown 1000:1000 /home/workshop/provider-a-data/marker
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: provider-a
base: ubuntu@22.04
summary: First synthesized provider for the ambiguity test
description: |
Exposes a mount slot named "data". Combined with provider-b in the same
workshop, this creates ambiguous autowiring for the consumer's "feed" plug.

sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00"

slots:
data:
interface: mount
workshop-source: /home/workshop/provider-a-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
install -d -m 0755 -o 1000 -g 1000 /home/workshop/provider-b-data
echo "from-b" >/home/workshop/provider-b-data/marker
chown 1000:1000 /home/workshop/provider-b-data/marker
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: provider-b
base: ubuntu@22.04
summary: Second synthesized provider for the ambiguity test
description: |
Exposes a mount slot named "data" with the same interface as provider-a,
creating the ambiguous autowiring scenario the how-to teaches you to resolve.

sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00"

slots:
data:
interface: mount
workshop-source: /home/workshop/provider-b-data
10 changes: 10 additions & 0 deletions tests/docs-how-to/design-interface-layout/dev.connected.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: dev
base: ubuntu@22.04
sdks:
- name: project-provider-a
- name: project-provider-b
- name: project-consumer

connections:
- plug: consumer:feed
slot: provider-b:data
10 changes: 10 additions & 0 deletions tests/docs-how-to/design-interface-layout/dev.reordered.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: dev
base: ubuntu@22.04
sdks:
- name: project-provider-b
- name: project-provider-a
- name: project-consumer

connections:
- plug: consumer:feed
slot: provider-b:data
6 changes: 6 additions & 0 deletions tests/docs-how-to/design-interface-layout/dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: dev
base: ubuntu@22.04
sdks:
- name: project-provider-a
- name: project-provider-b
- name: project-consumer
37 changes: 37 additions & 0 deletions tests/docs-how-to/design-interface-layout/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
summary: Test 'How to design the interface layout of a workshop' to ensure it's operational
restore: |
. "$TESTSLIB"/utils.sh
workshop_exec remove dev graft || true
execute: |
. "$TESTSLIB"/utils.sh
chown -R ubuntu:ubuntu .

echo "Stage 1: launch without connections; mount plugs do not auto-connect to regular SDK slots, so consumer:feed falls back to system:mount."
cp dev.yaml .workshop/dev.yaml
workshop_exec launch dev
workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/system:mount'

echo "Stage 2: add a connections entry pointing the plug at provider-b; refresh; verify the chosen slot connects and the marker is from-b."
cp dev.connected.yaml .workshop/dev.yaml
workshop_exec refresh dev
workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/provider-b:data'
workshop_exec exec dev -- cat /home/workshop/feed/marker | MATCH '^from-b$'

echo "Stage 3: disconnect then connect to provider-a at runtime; the runtime change is visible (manual) but the on-disk definition is unchanged."
EXPECTED_YAML_HASH=$(sha256sum .workshop/dev.yaml | cut -d' ' -f1)
workshop_exec disconnect dev/consumer:feed
workshop_exec connect dev/consumer:feed dev/provider-a:data
workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/provider-a:data.+manual'
workshop_exec exec dev -- cat /home/workshop/feed/marker | MATCH '^from-a$'
ACTUAL_YAML_HASH=$(sha256sum .workshop/dev.yaml | cut -d' ' -f1)
test "$EXPECTED_YAML_HASH" = "$ACTUAL_YAML_HASH"

echo "Stage 4: a refresh that applies updates drops the manual connection and keeps the manually disconnected pairing disconnected, so the plug ends up unconnected."
cp dev.reordered.yaml .workshop/dev.yaml
workshop_exec refresh dev
workshop_exec connections dev | MATCH '^mount +dev/consumer:feed +- +-'

echo "Stage 5: graft a plug onto an SDK that ships none; launch the graft workshop; the grafted plug connects to provider-a and reads from-a."
workshop_exec launch graft
workshop_exec connections graft | MATCH '^mount.+graft/plainconsumer:feed.+graft/provider-a:data'
workshop_exec exec graft -- cat /home/workshop/feed/marker | MATCH '^from-a$'
Loading