|
| 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