Skip to content

Commit 3d0edb8

Browse files
committed
Spin Improvement Proposal: Modularizing spin.toml
This PR adds a SIP describing changes that'd allow developers to split the `spin.toml` manifest up into a main application manifest and sub-manifests for individual contained components. Signed-off-by: Till Schneidereit <[email protected]>
1 parent 7a11e82 commit 3d0edb8

File tree

1 file changed

+264
-0
lines changed

1 file changed

+264
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
title = "SIP 022 - Modularizing spin.toml"
2+
template = "main"
3+
date = "2025-03-31T12:00:00Z"
4+
5+
---
6+
7+
Summary: Support moving component definit ions into separate Spin.toml files, similar to Cargo workspaces.
8+
9+
10+
11+
Created: March 31, 2025
12+
13+
# Background
14+
15+
As Spin applications grow in complexity, their `spin.toml` manifest files can become large and unwieldy. Large applications with many triggers and components can result in manifest files that span hundreds of lines, making them difficult to navigate, understand, and maintain. It’s very hard to keep related settings grouped, making the application architecture harder to follow.
16+
17+
The current monolithic nature of `spin.toml` files also means that tend to be very conservative when it comes to adding additional features that’d require additions to the manifest. This proposal is directly motivated by the desire to add features that’d make `spin.toml` files in their current form substantially more unwieldy. Those will be described in separate proposals.
18+
19+
Besides specific current goals, moving to a modular manifest format will bring Spin into alignment with related other formats, such as Rust’s [Cargo.toml](https://doc.rust-lang.org/cargo/reference/workspaces.html) or npm’s [package.json](https://docs.npmjs.com/cli/v11/using-npm/workspaces).
20+
21+
Modularizing `spin.toml` files has come up previously, with an attempt at implementing them [here](https://github.com/spinframework/spin/pull/2396).
22+
23+
# Proposal
24+
25+
`spin.toml` files contain multiple different kinds of items: application-level metadata, variables, trigger definitions, and component definitions. I propose to support modularizing manifests in the following way:
26+
27+
- Manifests can only be nested one level deep: an application can have a single top-level manifest, and zero or more direct sub-manifests, which cannot have any sub-manifests themselves
28+
- Application-level metadata must fully be contained in the application’s top-level manifest
29+
- Triggers must be defined in the top-level manifest
30+
- Variables can be contained in all manifests
31+
- A variable defined in the top-level manifest is visible in all manifests
32+
- A variable defined in a sub-manifest is only visible in that manifest
33+
- Components can be defined in the top-level, and in sub-manifests
34+
- Only a single component can be defined in each sub-manifest
35+
36+
## Example
37+
38+
To give a high-level overview of the proposal’s impact before discussing the details of the syntax, let’s look at an [existing](https://github.com/fermyon/ai-examples/blob/main/sentiment-analysis-ts/spin.toml) `spin.toml` file first:
39+
40+
```toml
41+
# (Source: https://github.com/fermyon/ai-examples/blob/main/sentiment-analysis-ts/spin.toml)
42+
spin_manifest_version = 2
43+
44+
[application]
45+
name = "sentiment-analysis"
46+
version = "0.1.0"
47+
authors = ["Caleb Schoepp <[email protected]>"]
48+
description = "A sentiment analysis API that demonstrates using LLM inference and KV stores together"
49+
50+
[[trigger.http]]
51+
route = "/api/..."
52+
component = "sentiment-analysis"
53+
54+
[component.sentiment-analysis]
55+
source = "target/spin-http-js.wasm"
56+
allowed_outbound_hosts = []
57+
exclude_files = ["**/node_modules"]
58+
key_value_stores = ["default"]
59+
ai_models = ["llama2-chat"]
60+
61+
[component.sentiment-analysis.build]
62+
command = "npm run build"
63+
watch = ["src/**/*", "package.json", "package-lock.json"]
64+
65+
[[trigger.http]]
66+
route = "/..."
67+
component = "ui"
68+
69+
[component.ui]
70+
source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.0.1/spin_static_fs.wasm", digest = "sha256:650376c33a0756b1a52cad7ca670f1126391b79050df0321407da9c741d32375" }
71+
allowed_outbound_hosts = []
72+
files = [{ source = "../sentiment-analysis-assets", destination = "/" }]
73+
74+
[variables]
75+
kv_explorer_user = { required = true }
76+
kv_explorer_password = { required = true }
77+
78+
[[trigger.http]]
79+
component = "kv-explorer"
80+
route = "/internal/kv-explorer/..."
81+
82+
[component.kv-explorer]
83+
source = { url = "https://github.com/fermyon/spin-kv-explorer/releases/download/v0.10.0/spin-kv-explorer.wasm", digest = "sha256:65bc286f8315746d1beecd2430e178f539fa487ebf6520099daae09a35dbce1d" }
84+
allowed_outbound_hosts = ["redis://*:*", "mysql://*:*", "postgres://*:*"]
85+
# add or remove stores you want to explore here
86+
key_value_stores = ["default"]
87+
88+
[component.kv-explorer.variables]
89+
kv_credentials = "{{ kv_explorer_user }}:{{ kv_explorer_password }}"
90+
```
91+
92+
Here’s what a modular version of this manifest could look like:
93+
94+
```toml
95+
# [project]/spin.toml
96+
97+
spin_manifest_version = ?
98+
99+
[application]
100+
name = "sentiment-analysis"
101+
version = "0.1.0"
102+
authors = ["Caleb Schoepp <[email protected]>"]
103+
description = "A sentiment analysis API that demonstrates using LLM inference and
104+
105+
# Components defined in sub-manifests must be included in this list.
106+
# Entries are treated as folder names, relative to the folder containing the application manifest.
107+
components = ["sentiment-analysis", "kv-explorer"]
108+
109+
[[trigger.http]]
110+
route = "/api/..."
111+
component = "sentiment-analysis"
112+
113+
[[trigger.http]]
114+
route = "/..."
115+
component = "ui"
116+
117+
[[trigger.http]]
118+
component = "kv-explorer"
119+
route = "/internal/kv-explorer/..."
120+
121+
# The UI component is defined inline, because it's small.
122+
[component.ui]
123+
source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.0.1/spin_static_fs.wasm", digest = "sha256:650376c33a0756b1a52cad7ca670f1126391b79050df0321407da9c741d32375" }
124+
allowed_outbound_hosts = []
125+
files = [{ source = "../sentiment-analysis-assets", destination = "/" }]
126+
```
127+
128+
```toml
129+
# [project]/sentiment-analysis/spin.toml
130+
131+
# Sections and keys don't need to be scoped explicitly.
132+
[component]
133+
name = "sentiment-analysis"
134+
description = "Component-specific description"
135+
# Certain keys can be inherited from the application manifest
136+
authors.application = true
137+
version.application = true
138+
source = "target/spin-http-js.wasm"
139+
allowed_outbound_hosts = []
140+
exclude_files = ["**/node_modules"]
141+
key_value_stores = ["default"]
142+
ai_models = ["llama2-chat"]
143+
144+
# Additional sections don't need to be namespaced under [component]
145+
[build]
146+
command = "npm run build"
147+
watch = ["src/**/*", "package.json", "package-lock.json"]
148+
```
149+
150+
```toml
151+
# [project]/kv-explorer/Spin.toml
152+
153+
[component]
154+
name = "kv-explorer"
155+
source = { url = "https://github.com/fermyon/spin-kv-explorer/releases/download/v0.10.0/spin-kv-explorer.wasm", digest = "sha256:65bc286f8315746d1beecd2430e178f539fa487ebf6520099daae09a35dbce1d" }
156+
allowed_outbound_hosts = ["redis://*:*", "mysql://*:*", "postgres://*:*"]
157+
# add or remove stores you want to explore here
158+
key_value_stores = ["default"]
159+
160+
# Locally defined variables don't need to be namespaced manually.
161+
[variables]
162+
user = { required = true }
163+
password = { required = true }
164+
165+
# Variables exposed to content (this needs bikeshedding!)
166+
[variables.exposed]
167+
kv_credentials = "{{ user }}:{{ password }}"
168+
```
169+
170+
As can be seen in this example, this modular structure allows us to not only isolate aspects of individual components in their respective manifests, but also reduce repetition, since keys and sections don’t have to be explicitly scoped to the component anymore.
171+
172+
## Details
173+
174+
### Sub-manifests for components
175+
176+
In this proposal, only components (and variables only accessible locally) can be defined in sub-manifests; triggers still need to be defined in the top-level manifest.
177+
178+
There are multiple reasons for this constraint:
179+
180+
1. Triggers are application-level concerns; they describe the application’s structure and high-level organization. As such, keeping them “at the surface” seems right.
181+
2. Trigger definitions tend to be very short, so moving them into sub-manifests individually as is done for components seems very boilerplate-y.
182+
3. Trigger definitions don’t come with their own assets, so any folder containing a sub-manifest for them would only ever contain the manifest, and nothing else.
183+
4. If, on the other hand, we were to allow defining triggers together with components in the same sub-manifest, that’d make it very hard to reason about the application’s structure—see the first point above.
184+
1. It’s common to have multiple triggers using the same component, e.g. to expose it under multiple routes using multiple http triggers. That means we’d want to allow multiple trigger definitions per sub-manifest, making the previous point even stronger.
185+
186+
### One component per component manifest
187+
188+
Similar to other tools such as cargo or npm, under this proposal Spin would only support a single component to be defined in each sub-manifest. This allows reducing boilerplate in each manifest, because now sections such as `[build]` are unambiguously scoped and don’t need to be expanded to `[component.foo.build]`. It also allows grasping the application’s general structure just by looking at the application manifest, instead of having to look at each sub-manifest to find the full list of components.
189+
190+
*Note that this doesn’t preclude future additions for defining nested components visible only in the scope of an individual top-level component, and destined for composition into said top-level component.*
191+
192+
### Support for defining multiple components in the application manifest
193+
194+
This proposal would not take away support for defining components using the existing syntax; it’d purely add support for the modular approach. The reason is that there are component definitions that are very light-weight; moving them into separate folders would add boilerplate and cognitive overhead, not reduce it.
195+
196+
### Inheriting definitions from the application manifest
197+
198+
Similar to [crates in a Cargo workspace](https://doc.rust-lang.org/1.85.1/cargo/reference/workspaces.html#the-package-table), component manifests can inherit certain definitions from the application manifest, such as the version, description, and authors:
199+
200+
```toml
201+
# [project]/Spin.toml
202+
203+
[application]
204+
version = "0.1.0"
205+
...
206+
207+
# [project]/my-component/Spin.toml
208+
...
209+
# Inherit the `version` field from the application
210+
version.application = true
211+
```
212+
213+
### Handling of variables
214+
215+
Variables are among the most tricky aspects to sort out in this approach. The proposal aims to make it easy to use both application-wide and component-scoped variables. An additional design goal is to support adoption without changes to deployment pipelines and their handling of variables.
216+
217+
To achieve these goals, the proposal
218+
219+
- treats application-level variables as inheritable in the same way as the definitions mentioned above
220+
- namespaces component-level variables by prefixing them with the component name when interpreting manifests
221+
222+
To illustrate this, consider these manifests:
223+
224+
```toml
225+
# [project]/Spin.toml
226+
227+
...
228+
[variables]
229+
my_variable = { default = "my-value" }
230+
...
231+
232+
components = ["my-component"]
233+
```
234+
235+
```toml
236+
# [project]/my-component/Spin.toml
237+
238+
...
239+
# Inherit the `my_variable` variable from the application
240+
[variables]
241+
my_variable.application = true
242+
my_other_variable = { default = "some-value" }
243+
...
244+
```
245+
246+
Combined, they’d be interpreted like this:
247+
248+
```toml
249+
...
250+
[variables]
251+
my_variable = { default = "my-value" }
252+
my_component_my_other_variable = { default = "some-value" }
253+
...
254+
```
255+
256+
Any uses in template strings are adapted to use the prefixed variable names.
257+
258+
### Exposing variables to content
259+
260+
Currently, manifests have the `[variables]` table for defining variables available to use in the manifest itself, e.g. as part of template strings. They also have `[component.name.variables]` tables for exposing variables to content.
261+
262+
In component manifests, the role of the latter is taken by the new `[variables.exposed]` table.
263+
264+
*Note: we should evaluate whether this table is really needed. Can we instead just expose all variables in a component manifest’s `[variables]` table to content directly?*

0 commit comments

Comments
 (0)